分类: 文章

长期文章、教程和个人写作。

  • ComfyUI的批处理(下):一篇文章搞懂For循环

    接上篇ComfyUI的批处理(上):如何实现多Prompt自动生图?,今天正经来聊聊For循环。

    For循环节点其实挺抽象的,不少会用的都说不清楚。

    但好在它抽象的不是For循环的编程概念——For循环的编程概念就是一个带次数的循环——而是怎样去理解这个节点运作逻辑。

    只要搞清楚节点上的每一个连接点代表什么、怎么输入的、怎么输出的、怎么循环的,并不难懂。

    有朋友跟我说,他跟别人解释For循环,先像下面这样一连,组成一个最简单的循环,然后就开始想尽办法使用各种奇妙比喻,什么传送带、丢沙包……但是再怎么具象比喻,还是自己都觉得抽象,听的人也不一定全部绕得过来。

    我觉得如果真要足够好懂,还是得用笨法子,拆开了把每一个连接点都解释清楚,整个循环自然也就解释清楚了。

    [以下内容比较细,可酌情跳跃观看]

    首先,必须要搞清楚一个问题:怎么样就算形成循环了?

    答案非常简单:在循环起始和循环结束节点之间围出一个圈,就形成循环了。

    不管是像上面的截图,从起始节点的值出发,连接到结束节点闭环:

    还是从起始节点的索引出发,连接到结束节点闭环:

    有了循环圈,都可以形成循环。

    当然这种最简单的循环,起终点之间没有接入任何有效内容,只是单纯地转圈圈,没啥实际应用意义。但是,我们必须先搞清楚的几个东西,已经都有了。

    其实一共就5项,其中两项还不太需要解释。

    流:起始节点输出的流一定要连接到结束节点输入的流,这样才能形成循环;

    总量:就是需要循环的次数,数字填写几就循环几次。

    于是现在只剩下3项:

    (节点的翻译有点不全,initial_value=初始值,value=值,别看分别是中英文,其实是一个意思。当初始值1/值1连接点被使用时,节点会自动出现初始值2/值2。暂时我们还不会用到initial_value2和value2,在用到它们之前,接下来我提到的「初始值」均默认指「初始值1」,「值」均默认指「值1」。)

    正式开始之前,我先介绍两个节点:

    一个叫「展示任何」,可以把输入的东西展示出来,然后原样输出;另一个叫「延迟执行」,就是执行到这个节点,就停顿多少秒,然后继续执行。

    这两个节点,对学习陌生的工作流,非常有用。它们都是原样输入原样输出,除了它们能提供展示/延迟功能外,你可以把它们等效成连接线,只当它们不存在。

    我们先来搭一个从值出发连接到结束节点的工作流,然后对索引、值、初始值,分别添加3个「展示任何」节点来观察它们的内容, 中间加一个2秒的延迟防止运行太快看不清楚。就像下面这样:

    总量设置成5次,运行。

    从运行里面可以观察到:

    1. 「索引」的值从0开始,在每次运行完一个循环后,都会自动加1。循环总量是5次,「索引」的值就是从0到4。
    2. 5轮循环,「值」和「初始值」都是null(空)。同时,结束节点右边的「值」输出整个工作流运行完才显示null。

    下面再保持展示和延迟节点,再来搭一个从索引出发连接到结束节点的工作流,像下面这样:

    同样运行5次。

    这次可以观察到:

    1. 「索引」的值依旧是从0开始,在每次运行完一个循环后,都会自动加1。循环总量是5次,「索引」的值就是从0到4。
    2. 结束节点的「初始值」随索引的变化而变化,整个工作流运行完后,结束节点右边的「值」输出了「索引」的最后值4。
    3. 起始节点「值」一开始是null,但从第2次循环起,被自动赋值为上一次循环的「索引」值,也就是同一轮循环中结束节点的「初始值」。

    两相对比,「索引」这一项我们已经可以搞明白了,它索引的就是循环的次数,只是索引值从0开始。第1次循环索引值为0,第2次循环索引值为1,以此类推。

    同时,可以得到一个小知识点,For循环-结束节点右侧端点的「值」,在整个循环结束后才会输出;以及,起始节点的「值」,似乎是随结束节点的「初始值」变化。

    我们继续,还是同样的工作流,我们在左边加一个「整数常量」节点,连接到初始值,为它赋一个初始值。

    按照ComfyUI的基础逻辑,起始节点「整数常量」只会执行1次,加上前面的小知识点,最后结束节点的输出也只在结束时输出1次。所以真正参与循环的部分是下图绿框的部分,绿框外的都不参与循环。

    分别以初始值为0和为3运行一遍。

    当赋的初始值为0时,后面的值一直为0;当赋的初始值为3时,后面的值一直为3。

    所以现在对于起始节点,我们可以得出结论:

    当工作流开始运行时,如果没有为起始节点的初始值赋值,那这里输出的值就是null(空);如果我们提前赋值了初始值,则输出的值就是初始值。

    那么,结束节点的初始值又是什么呢?

    我们在前面的工作流中再插入一个「简易运算」节点。

    简单说明一下,这个节点实现的是,把前面传递过来的值赋给a,然后执行运算a+2,再输出到后面的节点。

    执行一下。

    我用一个简单的表格记录下执行结果:

    结束节点的「初始值」,确实就是参与下一轮循环的「值」,也可以说等同于下一轮起始节点的「初始值」。

    再去掉工作流中多余的展示和等待。

    这个循环背后赋值的逻辑就很清晰明了了:

    1. 如有初始值输入,则起始节点以这个值参与第1次循环,否则以空值参与;
    2. 第1次循环的结果,作为第2次循环的初始值参与第2次循环,以此类推;
    3. 直到执行完设置好的循环次数(总量),结束节点输出最终的结果。

    如果还有初始值2/值2,也是同样的逻辑,两条线并行,各自进行循环。

    现在,5个基本项都已经清楚了,循环的流向逻辑应该也就没有什么问题了。

    现在我们已经理解For循环的基本运作逻辑了,但光知道原理还不太够,紧跟着来了一个新问题:怎样应用?

    我们在ComfyUI里搭建循环,可不是为了让它闲得没事在开始和结束中间反复横跳的,也不是简简单单让它进行数字运算的。我们的诉求是通过For循环实现批量生成/处理图片和视频才对。

    那到底如何让一段需要重复的流程循环起来呢?

    答案就是:想办法把它接进这个圈里,让起始节点的输出,「流经」你要循环的流程,最终汇入结束节点。

    乍一看,挺简单的,像画电路图连接元件一样,把要循环的部分插进去呗。

    但真正动手搭建的时候,问题就来了。

    如下图,下面蓝色分组的部分是一个再简单不过的SD1.5基础生图流程:

    Checkpoint加载器也好,CLIP文本编码器也好,Latent也好,好像没有地方能跟For循环的起始节点连接起来。接不进去啊,那咋办呢?

    所以我在上一篇文章说,Prompt line是一个很方便的快捷节点,它自己内置了循环,完全不需要我们考虑怎样触发循环。但自己写For循环,这个问题还是需要考虑一下的。

    不卖关子了,直接说答案,通常可以采取的方法是:数字连接。

    现在我们要重复的流程是一个生图流程,所以最终输出的展示值一定得有一个图像用来查看生成结果,我们在最右侧的「值1」输出接上一个「预览图像」。而这个图片它应该来自于SD1.5生图流程的输出,所以我们把下面「VAE解码」的「图像」连接到「初始值1」。

    现在循环的圈和需要被循环的部分还是没有建立真正的闭环连接。

    怎么连接呢?我们看到起始节点还有两个可以对外输出的端口「索引」、「值1」,但实际上「值1」并不可用,因为「值1」在循环中会被「初始值1」赋值,所以它的值类型必须是图片,而不能是数字了。不过「索引」本身就会自动输出0、1、2……的递增数字,而且我们要循环生图,肯定是需要输出不一样的图片的,这个数字应该随着循环次数发生变化,选择「索引」再合适不过了。

    再看下面SD1.5生图流程的部分,有哪个数字是允许发生变化但不影响生图稳定和效果的呢?不难找,「随机种」。我们把索引连到随机种上。

    现在成功闭环了,点击运行,循环生成了5张图片:

    成功实现循环。

    不过如果你细心的话,应该能察觉到两个小问题。

    第一个问题,如果直接把索引连接到随机种,那生图的种子就会从0、1、2、3……按顺序递增,完全不随机了。

    但我们可以通过一个简单的公式计算补救一下。

    新建一个随机Seed节点,生成随机种;然后新建一个简易运算节点,索引赋值为a,随机种的值赋值到b,对它们求和a+b,然后再把a+b的和赋值到生图流程K采样器的随机种,这样就又恢复随机性了。

    另外一个问题,预览图像现在只能输出最后一次循环的结果,没办法快速查看到这一批生成的所有图片,很不方便。

    解决这个问题,就需要用到一个新节点「任何批次组合」。

    作用是把any_1和any_2进行组合,后面我们具体解释。

    连接方法很简单,把它串在被循环的流程和结束节点的初始值1(或其他数字)输入之间,然后再把另一个输入节点和For循环的值1(或其他对应数字)相连:

    我们先来回忆一下前面说的For循环赋值的逻辑。

    假设第1次循环生成的图片是a,由于图像的输出连接(结束节点的)初始值1,则初始值1也被赋值图片a;

    来到第2次循环,虽然这个工作流中(起始节点的)值1不向后输出,但它还是在自顾自地发生变化。因为上一次循环中初始值1为图片a,所以这次循环的值1就被赋值图片a。进入到下面的生图流程,生成了一张图片b,同样的,初始值1也变成图片b。

    继续第3次循环,同样的,值1变成初始值1的图片b,生成图片c,初始值1变成图片c。

    最后,循环结束,值1输出图片c。

    发现问题没有?初始值1每次都被覆盖掉了,所以循环结束后输出的只是最后一次的结果。

    「任何批次组合」解决的就是这个被覆盖掉的问题。

    它把前面不向后输出的(起始节点的)值1利用了起来,暂存了上一轮的初始值1,再跟这一轮的生成结果合并到一起,赋值给这一轮的初始值1。

    循环结束,值1输出最后一次的初始值1,也就是[图片a,图片b,图片c]。

    到这儿,For循环的基本应用方法也就掌握了。

    上篇提到过,除了多prompt生图,使用For循环还能实现文件、图片的批处理。比如说,批量放大。

    刚好我这里有一些柯达EKTAR H35半格胶卷相机扫出来的照片,裁剪成单张还不到1300*900,按像素计只有可怜的100万像素出头,可以拿来做一下演示。

    单纯对于真实照片的放大,我一直非常推荐Philip Hofmann发布的一系列放大模型(https://github.com/Phhofm/models),之前就发文章介绍过其中之一:ComfyUI | 如何AI放大照片不模糊?推荐一个冷门的图片放大模型,这些模型基本不涉及大幅重绘,所以可以避免AI编辑造成的一些负面修改。例如,前面文章中我们用Qwen编辑模型处理过天坛的穹顶,当时瓦片的线条纹理发生了诡异形变,这种情况在使用这些放大模型时就不会发生。

    这次我选择使用Philip Hofmann的放大模型4xBHI_dat2_otf_nn,它在4倍高清放大的同时又不会进行过度的降噪处理,就算从1100万像素放大到1.8亿像素,100%缩放查看时仍然有一定细节,还很好地保留了胶片的颗粒质感。

    由于并没有进行重绘,工作流也非常简单:

    好,现在开始思考:怎么样设计循环,让这个流程能够批量自动处理一个文件夹的图片?

    首先我们需要一个能够从文件夹路径加载图像的节点,最好带有索引功能,能实现一张一张加载。这样的加载节点其实不少,像一般整合包都会带的Inspire-Pack和KJNodes都有。

    我一般习惯使用这个「加载图像(路径)」。

    使用它替换掉原来的单张图像加载节点,然后把循环起始节点的索引跟起始索引相连,这样就可以从0、1、2、3……按顺序自动加载了。因为每一张图片我们都要单独放大,所以图像加载上限填1,代表每次加载1张。

    循环次数设置与文件夹内图片数量一致,再添加「任何批次组合」节点,实现循环结束后一次性预览全部放大后的图片。

    就是这么简单。

    当然也可以做一些其它的花样,比如最近Comfy官方更新了一批图片编辑模板,有几个我觉得挺有意思的,像是这个:

    基于Qwen-Image-Edit模型,上传一张图片,可以获得8个角度的视角。

    我比较喜欢里面俯视视角,有点像低空无人机的角度,很有意思。

    那我就可以对这个工作稍作调整,加入For循环,对一整批图片批量生成俯视角度的新图;同时可以把原图的加载接入值2的循环,这样就能跟生成结果形成对比:

    这个工作流的视角变换基于Prompt,如果你愿意的话,也可以再嵌套一个提示词的循环对每张图输出任意你想要的视角(咳,注意文明)或者对图片内容进行固定元素的增减与修改,实现多组输出。

    For循环跟不同的节点结合可以有不同的玩法,比如上篇文章说的LLM生成多提示词,加入For循环再配合Excel写入写出节点,对反推和编辑都可以留出极大余地;比如说配合SAM等智能分割节点,就可以实现批量抠图编辑和去水印等功能;比如说配合一些图像处理和逻辑节点,可以实现图片的自动分割拼接,一键追色等等……

    掌握了For循环的基本逻辑和应用,这些都等着你去自由探索。

  • ComfyUI的批处理(上):如何实现多Prompt自动生图?

    我先问个问题:如果想在ComfyUI里一次生成多张图片,要怎么做?

    肯定有人说提高Latent的批次大小的值或者直接增加工作流运行的批次数量。

    没错,这样是可以批量出图。

    但是,这样做只是基于同一条Prompt,随机变换不同Seed生成多张图片,也就是俗称的「Roll图」。

    如果我要用不同的Prompt生成不同的图片呢?

    那就需要用到循环了。

    在ComfyUI中,我们常用的是Easy-Use节点包里的For循环节点。

    For循环能做很多事情,非常强大。

    不过,仅针对多Prompt自动生图这一项,有一个快捷方法。在For循环向各位发起脑筋急转弯硬控之前,容我先说说这个。

    同样是在Easy-Use包里,有一个很方便的内置循环的节点,叫做Prompt Line(提示词行)。

    每一行写一条Prompt,设置好最大行数,再把它接到Prompt输入即可。

    最大行数就是循环执行的次数。例如上图中,我一共写了5行提示词,但最大行数设置为4,则第5行的1goat不会被执行,输出结果如下图。

    简单吧,不能再方便了。

    补充下,如果填写行数大于实际行数,则它会执行到实际的最后一行为止。假设还是上面的Prompt,最大行数填写10,那它其实输出完第5行的1goat就会停止。所以如果咱们单纯就是要把所有Prompt都跑完,直接填个1000就好了。

    老规矩,举个实际应用案例吧。

    比如,我们可以在前面接一个用于生成文生图Prompt的LLM节点(LLM节点用法参考把工作流拆开揉碎了,带你入门ComfyUI),用DeepSeek输出的美少女写真图片提示词替换掉上面的"1girl"、"1boy"、"1cat"……

    像下面这样,我们就可以批量自动生成美女图片了:

    Prompt参考:

    # Role: AI摄影美学指导 (AI Photography Director)
    ## Task
    请以此身份创作 12 条专注于“日系糖水片(High-key Portraiture)”风格的生图提示词。旨在捕捉少女的清纯气质与光影的通透感,强调治愈、清新与明亮的视觉体验。
    ## Structure Rules
    每条提示词必须严格遵循以下构词逻辑,确保画面描述的完整性与专业度:
    `[核心风格定义] + [主体特写与情绪质感] + [服饰造型细节] + [环境氛围与光位布局] + [色彩科学/胶片模拟] + [镜头语言与景深控制]`
    ## Reference Example
    > **Style Reference:**
    > 一张极具空气感的日系人像摄影。画面主角是一位清纯甜美的少女,皮肤呈现白皙细腻的陶瓷质感,眼神清澈含水,嘴角上扬流露治愈系微笑,妆面采用伪素颜的清透感。身着纯白法式吊带裙,锁骨线条精致。置身于正午阳光下的绿茵草地,采用逆光(Backlight)拍摄手法,捕捉发丝边缘迷人的轮廓光与空气中的丁达尔效应。整体色调遵循高调摄影(High-key)原则,模拟佳能(Canon)直出的粉嫩色彩科学,白里透红,温暖柔和。配合85mm大光圈定焦镜头,背景呈奶油般化开的虚化效果,焦内发丝毕现,完美定格青春气息。
    ## Output Constraints (CRITICAL)
    1. **格式要求**:每条提示词汇编为一个连贯的自然段落。
    2. **分隔方式**:不同提示词之间仅通过换行进行区分。
    3. **纯净输出**:输出内容**严禁**包含序号(如1、2、3...)、引导语(如“当然,这是您的提示词...”)或任何形式的解释说明。直接输出结果即可。
    ## Execution
    开始生成:

    如果你觉得这样风格不够稳定,显得比较杂乱,我们还可以再稍微优化一下:

    # Role: AI系列摄影美学指导 (AI Series Photography Director)
    ## Task
    你正在执导一组具有高度连贯性的“日系糖水片”人像摄影套图。
    请创作一套固定的角色和场景方案,然后一次性生成10个不同的镜头描述。
    1. 设计一个具有明确物理特征包括发色、发型、五官特征、服装细节、场景环境、色彩科学)的全新人像方案。
    2. 基于固定的[主体物理特征](发色/发型/五官细节)、[服饰造型](颜色/款式/配饰)、[环境场景]、[色彩科学]、[光影基调](严禁修改这些特征,确保人物与场景的物理属性与历史记录字面上完全一致)创建差异元素设计10条不同的 [肢体动作]、[面部微表情] 或 [镜头构图]。
    3. 输出10段完整的提示词,每一段都必须包含完整的“固定设定”和独特的“动态变化”。
    ## Structure Rules
    生成时请严格遵循以下积木式结构,确保描述的具体性:
    `[核心风格定义] + [主体物理特征锚点] + [服饰细节与配饰] + [环境场景与光影布局] + [色彩科学/胶片模拟] + [镜头语言与景深]`
    * **主体物理特征锚点**:必须包含具体发色、发型细节、五官特征(如泪痣、酒窝)及具体的面部情绪。
    * **服饰细节与配饰**:必须明确服装的款式、材质、颜色以及关键配饰。
    ## Reference Examples
    一张极具通透感的日系糖水片风格人像摄影。画面主角是一位清纯甜美的年轻女孩,拥有白皙细腻的皮肤质感和清澈明亮的眼神,面带治愈系的自然微笑,妆容清淡精致。她身穿一件白色的法式吊带连衣裙,锁骨线条优美。场景设定在阳光明媚的户外草地或公园,光线采用柔和的逆光拍摄,捕捉发丝边缘的发光感和空气中的丁达尔效应。整体色调偏向明亮的高调风格(High-key),色彩还原自然且讨喜,强调佳能相机(Canon)的色彩风格,肤色呈现出白里透红的粉嫩感,温暖而柔和。使用大光圈镜头拍摄,背景柔和虚化,焦内清晰锐利,完美展现少女的青春气息。
    ## Output Constraints (CRITICAL)
    1. **数量要求**:必须一次性输出 **10条** 提示词。
    2. **格式要求**:每条提示词汇编为一个连贯的自然段落。
    3. **分隔方式**:不同提示词之间仅通过换行进行区分。
    4. **纯净输出**:输出内容**严禁**包含序号(如1、2、3...)、引导语(如“以下是为您生成的...”)或任何形式的解释说明。直接输出10段文本即可。
    5. **一致性强制**:10条提示词中的人物外貌、服装、场景描述必须完全一致,就像在同一地点连续拍摄的一组照片。
    ## Execution
    请开始生成:

    于是,现在我们可以生成套图。

    运行多个批次,那就是多组套图。

    如果要再完善,后续在这个工作流的基础上,我们还可以再附加上LoRA和ControlNet,获得更好的一致性和动作。

    当然,结合反推节点或者用在通义万相这样的视频生成模型上,也完全没有问题。

    Prompt Line是一个我非常推荐的多Prompt批量执行节点。

    它实际上是把循环内置了,并且自动进行多行Prompt文本的索引,这样你就不用自己手动把工作流接入循环体进行闭环,也不用手动去做索引值的对齐。甚至可以说,多提示词这块,如果就是不愿正经学For循环怎么用,硬要拿它凑合,它还真能凑合。这玩意儿主打就是一个方便、好懂。

    但有一说一,局限性还是不小。

    首先,前面套图生成工作流的LLM节点部分,我在Prompt写的一直都是生成10张图片,但实际每次运行生成的图片只有6-8张。为什么呢?仔细看截图就能发现,LLM的输出被截断了。上下文窗口就那么长,越复杂的需求,越容易被截断。

    所以很多时候只靠Prompt Line,没办法在ComfyUI里配置好了让它自己跑就完事儿。一般都是需要我结合一下飞书多维表格和Excel,先把Prompt写好,再接入到ComfyUI,混搭着用。虽然也还行吧,但是不够优雅。

    并且,单看LLM的输出质量,前面我们是只占用一个上下文窗口,一次性生成多张图的Prompt;而如果把LLM节点接在循环体中,则是每次使用完整的上下文窗口应对单独一张图的Prompt生成,效果也会存在差距。

    我把LLM节点嵌在For循环的循环体中,又做了两次套图生成。使用的模型跟刚才一样是DeepSeek-V3.2和Z-Image-Turbo,但新生成的这两组图片,角色状态明显比刚才更生动了:

    更重要的一点,Prompt Line本身的功能就是负责多提示词的批量执行,所以它只能循环字符串。但实际上,我们还有很多其他的批处理场景,比如批量抠图、批量放大、批量替换、视频首尾帧续杯等等,这些只靠Prompt Line就没法完成了。

    下一篇,我们就正经聊聊For循环。

  • 没有生成式填充,怎样在Photoshop里用上AI修图?

    有一种AI时代的新型大冤种,叫做正版中文Photoshop用户。

    新功能的推送是一个也不落下,一打开用就提示此功能不适用于您所在的地区。

    好你个Adobe,脸都不要了。

    但是,在Photoshop这样的创意生产环境里使用生成式AI填充,其实还挺重要。

    尽管过去一段时间,AI图片编辑模型越来越强力,越来越普及,去翻翻抖音、小红书、B站的评论区,到处都是网友用豆包P的梗图。

    不过起码就现在而言,仍有一个不大不小的尴尬。这些用AI编辑的图片,大多都被用来发在群里、朋友圈、社交媒体,网友们看了一块乐呵乐呵,基本不会出现在对画质需求比较高的场景。

    原因很简单,大多数网友,或者说大多数AI图片编辑产品,默认的操作逻辑就是直接整张图+Prompt输入,然后整张图输出。在这个过程中,受到算力等因素制约,最终输出图片质量存在明显的瓶颈——输入的原图质量越高,输出图片的质量就越是断崖式下滑。

    目前的图片编辑大模型,最大图片输出尺寸也就是1:1 4K(通常需要付费),大约1600万像素,换成更常见的3:4或2:3照片比例,差不多就是1200万像素左右。更常见的,不进行付费的情况下,比如直接使用豆包App,「P好」的图也就差不多只有140万到300万像素。而现在主流的数码相机CMOS,基本都是2400万像素起跳。

    整图输入再整图输出,从分辨率上就很难满足高画质影像处理需求了,更别说色域和色深的变化,以及全图输入可能带来的非目标编辑区域误伤风险了。

    全图编辑这个方案对高画质影像处理来说,既不优雅也不高效,而在PS里进行局部选区重绘,能解决的就是这个问题。

    但是,前面说了,Adobe的生成式填充功能不向国区开放,咋办呢?

    别慌,通过一个AI插件,就可以在Photoshop里使用AI图片编辑了。

    插件名字叫SD-PPP,开源免费。

    我们直接进入SD-PPP的官网https://sdppp.zombee.tech/,点击右上角的下载安装,然后下载ZIP压缩包解压到Photoshop安装目录的插件文件夹即可。如果你安装了Adobe Creative Cloud就更方便,直接下载CCX插件双击安装就行。

    安装完成后,在PS的增效工具菜单中可以找到插件。

    目前SD-PPP支持直接对接Google/OpenAI格式和Replicate的API,也可以接入到ComfyUI(包括通过本地和仙宫云这类云端算力租赁平台部署的)或RunningHUB,除了ComfyUI以外都需要进行一下API对接配置,非常简单,参考网站的基础教程即可。

    四者里面我建议使用ComfyUI或RunningHUB,可自定义程度更高。

    RunningHUB可以近似理解为一个经过第三方高度定制和运营的在线版ComfyUI,如果你没有自己部署ComfyUI的条件,可以考虑使用它,会有一些小限制,但更便捷。两者的使用方法和逻辑基本一致,我就直接以本地ComfyUI进行演示了。

    本地ComfyUI需要手动安装一下扩展,非常简单,打开节点管理器,搜索SD-PPP,点击安装即可。

    安装完成并重启ComfyUI后,左侧菜单多出一个SD-PPP选项卡,就是安装成功了。

    你可以使用手里常用的任意ComfyUI图片编辑工作流,本地的、API的、Flux.1 Kontext的、即梦的、Nano Banana Pro的,都可以。

    Z-Image-Edit发布之前,我想我最喜欢用的图片编辑模型还是Qwen-Image-Edit,我就以这个来从头演示一遍。

    先创建一个基础工作流,我们可以直接用ComfyUI自带的模板,找到下面这个「图像编辑 (新)」,打开,按照提示下载好模型和4步LoRA。

    非常基础的工作流,没啥好说的,各功能区如下:

    如果直接在ComfyUI里运行,就是上传图片,写好prompt,点运行按钮,搞定。

    不过我们要在PS里使用它,就要做一点小小的微调。

    点开左侧的SD-PPP扩展,你会发现面板里面有一个「加载图像」。

    其实ComfyUI里这个扩展的面板,就是Photoshop插件里的面板。你可以理解一下,上图我用红框圈出来这个面板里有什么,你的PS插件界面里就有什么。

    「加载图像」这个节点会默认被SD-PPP识别,出现在面板里面。

    比如我现在再启用一个参考区域的「加载图像」节点,这个节点就出现在里左侧的面板中。

    为了方便区分,可以点击修改一下面板里的节点名称,你能看到工作流里的节点名也会同步变化(有些版本的ComfyUI会有bug,刷新页面以后工作流里的节点名称会被重置,但没关系,面板里修改好的不会变化)。

    前面说过,ComfyUI里配置的这个左侧面板,其实就是我们使用PS插件的操作面板。那么可以思考一下,现在还缺什么呢?

    显而易见,我们需要有一个地方输入提示词才行。

    双击空白处添加一个新节点——「Primitive元节点」。这是一个ComfyUI的基础节点,不需要单独安装。

    添加好Primitive元节点后,将它与Qwen Image Edit节点的Prompt输入框相连(连接点在文本框左上角),你会发现Primitive元节点上出现了Prompt文本输入框。在这里可以编辑Prompt,并自动同步到Qwen Image Edit节点的Prompt输入框。

    同时,左侧的SD-PPP面板上,也出现了Prompt输入框。

    现在面板上的几个输入端口已经能够实现这个简单工作流的功能了。

    我们保存工作流,名字就叫「00-qwen演示工作流」。

    注意,这里一定要保存。

    只有保存到工作流文件夹的工作流才能在Photoshop里加载到。

    现在我们来到PS的增效工具,启用插件并打开插件面板。

    选择ComfyUI。

    连接地址插件内已经默认填好,网页版本的ComfyUI通常都是http://127.0.0.1:8188/。Comfy官方客户端默认监听端口应该是8000,但我自己测试不知道为啥总是失败,如果你的客户端也不行,我建议换个网页版本的,实测秋叶整合包没问题。

    连接成功后,保存过的工作流应该都会出现在下面,比如我们刚刚保存的「00-qwen演示工作流」。

    我们找一张图测试一下,单击工作流名称加载工作流。

    现在可以看到插件面板中我们先前配置好的三个输入端口:参考图、加载图像、Prompt。

    我们来给女孩换套衣服好了。

    这里要注意一下,「参考图」,也就是工作流里的连接到image2的「加载图像」节点,按字母顺序排序到了最上面,并且默认读取了我们打开的这张原图,这个是不对的,需要修改一下。

    下图左边红圈中的图标现在一直在旋转,代表画板侧操作发生变动后,这里加载的图片会随之自动更新。我们一般希望参考图保持固定,不要随意变动,所以点一下这个图标让它停止旋转,关掉这个功能。然后鼠标悬停在左下角三点处,在展开项中选择「从磁盘上传」,上传一张服装图片。

    接下来是「加载图像」的原图。

    我们只希望局部重绘替换女孩的服装,所以用矩形选框工具选取女孩。

    对于被修改的部分,我们是希望它随着选区的选取操作自动更新的,所以可以点开自动更新开关,让它旋转起来。

    然后配置一下输出区域,点击参考图上方的「输出至」,让它变更为「设为选区」,点击后,画板上会根据选区边界自动新建参考线,现在就代表AI处理过的图片会(以一个新的图层)返回到这个选区框选的区域。由于我们前面开启了「加载图像」的自动更新,「加载图像」节点所加载的输入图像也就自动变成刚才设置的输出选区在原图上所框选出的图像。

    Prompt处输入:让女孩换上图2衣服,身边的环境不变。

    然后点击运行按钮。

    运行完毕后,上方会出现结果预览。

    鼠标指向预览图片,点击图片下方出现的发送按钮,图片就被发送回选区区域了。

    并且AI编辑后的内容是以一个独立图层返回的,二次编辑起来非常方便。

    比如说,我可以对主体人物服装颜色进行调整的同时,尽量避免影响人物周围环境和陶俑的颜色。

    相比于参考另一张图片进行服装、物品的替换,直接对原图进行调整和瑕疵修复(俗称P图)的情况显然更常见,所以我们还应该有一个不使用参考图,直接编辑图片的工作流。

    不用按照前面的步骤从头来过,我们只要在ComfyUI中重新忽略掉参考图节点,然后使用另存为功能再存一个新工作流即可。

    名字就叫「01-qwen演示工作流」吧,当然你可以取一个更好区分功能的名字。

    这是我在天坛祈年殿拍的一张照片:

    当天是周六,可以看到有非常多的游客。这么多人,用传统PS工具难以消除,但借助AI会方便很多。

    并且由于这张照片是胶片拍摄,使用了比较实惠的冲扫,所以图片数码文件的分辨率不高,只有1818*1228。

    于是,当我直接在ComfyUI中输入整张图,试图让AI移除游客时,就开始出现问题。

    图被压得更小了不说,本来依稀可见的「祈年殿」三个金字,直接缩成了三个圆点,檐顶上的蓝琉璃瓦也变得怪怪的,好像瓦楞纸一样。

    这种时候,局部选区重绘就可以派上用场了。

    我们可以只对祈谷坛基座部分建立选区,进行AI移除游客处理,上面的祈年殿就不会受到影响。

    Qwen模型处理这种结构复杂的建筑,还是多多少少会有一些边缘的纹理线条不对齐的情况的。不过这是小问题,既然都已经在PS环境里面了,直接复制一个图层,手动对齐了擦擦抹抹修复一下就行。

    一张无游客版的祈年殿照片崭新登场:

    人像照片处理也用得到。

    这是一张遵从CC0协议,允许免费使用的人像照片,来自越南摄影师Felix Young,我们用这张图作为操作示例:

    先补充一个关于建立选区的小技巧。

    如果你用矩形选框工具随便拉选区,尤其是小范围选区,大概率会遇到返回的图片跟原图对不齐的情况。这是因为在latent diffusion工作流里,图片的宽高必须能被模型的latent下采样倍率整除,SD3及以后常见的开源生图模型如Flux.1、Qwen-Image等,这个倍率值大多是16。当宽高值不能被16整除时,ComfyUI节点会把尺寸自动对齐到最近的16的倍数。如果手拉选区的宽高恰好不是16的倍数,返回的图片自然就会出现偏移。

    为了不出现偏移,我们可以不直接使用选区工具去拉选区,而先用矩形工具创建一个宽高都是16倍数的矩形,降低一些透明度,把这个矩形放在想要选择的画面区域上。

    比如这张分辨率是20083008的人像照片,如果我想调整模特身上这件红色的上衣,那我就创建一个9602016的矩形,如果我想调整模特面部表情,那我就可以创建一个512*512的矩形。

    位置摆好后,就可以把矩形图层隐藏掉。按住键盘Ctrl键,点击矩形图层的缩略图,就能根据这个矩形创建选区了。

    举例比如说,现在这张照片的环境,我觉得它有一点欧洲风格。

    所以我想把它用在某一页表现欧洲元素的PPT上,但现在照片上的模特是个亚洲美女,不太搭,我又临时找不到其他图了。那我就可以使用矩形2这个脸部选区,重绘模特的五官,偷梁换柱,给她换成一个欧洲模特。

    新建一个图层,涂抹掉模特的五官。具体涂多少,可以视需要改变幅度的大小调整。

    然后让AI移除面部遮挡,告诉它这是一个欧洲美女。

    运行后,AI返回图片:

    这次结果还算不错,只是面部稍稍有一点发红。

    但有时候,特别是原图色彩比较丰富且对比强烈的时候,AI输出的结果容易有偏色,Qwen-Image-Edit模型尤其明显。这种偏色借助一些LoRA可以得到一定改善,但不能完全杜绝,还会对出图速度和质量有一些影响。

    不过没关系,既然我们都已经在PS里了,当然可以用PS的方法处理。

    我们使用跟重绘相同的选区,在原图上复制一份选区内容,也就是下面这个图层2(图中为了方便说明隐藏了其他图层,实际不需要)。

    然后先选中AI修改后返回的图层,再找到PS菜单的图像-调整-匹配颜色。

    源选择当前文件,图层选择图层2。

    这个操作的意思就是让AI生成的这个脸部选区的颜色,去匹配原图相同区域的颜色。

    放一个对比,应该能够看出来变化。原本偏红的面部颜色现在跟整个画面更和谐了。

    哎呀,原图问题有点少,给我整不会了。

    临时插入一个case吧。

    这是我在香港M+ Museum拍到的一个展品:

    我现在用AI让最前面这个人把嘴巴闭起来。

    在这张图,我们就能看到比较明显的偏色了。

    匹配颜色之后,后面焦外的两个人头部接缝处色差明显改善,但仔细看依旧可以看到有一丢丢的偏青色。

    怎么处理呢?不需要手动调色或涂抹擦拭,只需要借助一点点景深合成的操作即可。

    先把原图复制一个图层,然后按住Ctrl键点击AI修改后图层的缩略图,创建选区,然后找到PS菜单的选择-修改-收缩,收缩一定量的选区边界,以收缩选区后还能保持把画面中需要被AI修改的部分框在选区内为尺度。

    然后,删除这个收缩后选区中的内容。

    之所以要删掉这部分,是因为接下来我们做的是类似景深合成的操作。

    常规的景深合成,是在风光或者微距摄影中,因为要保证镜头通光量等原因,光圈没有办法收得更小,所以导致景深不够,没有办法让整个画面全部清晰。于是,就以不同对焦点拍摄多张照片,最后进行合成,消除掉被虚化的部分,获得一张大景深的清晰照片。

    用于景深合成的多张照片只是各有不同的位置处于焦外,被虚化掉了,所拍摄的物体还是不变的。但我们这个是直接用AI把物体修改了,再去合成到一起,原图和修改后的内容会打架,所以要提前把原图不需要的部分删除掉。

    接下来,同时选中这两个图层,找到菜单中的编辑-自动混合图层。

    选择「堆叠图像」,下面的「无缝色调和颜色」,其实就是我们真正用到的功能。

    点击确定,色差就自动融合好了。

    回到刚刚的美女照片。

    这张图AI修改的部分本身偏色就不明显,所以可以不做上面这一步。如果要再找问题的话,就是AI生成的面部缺少一些细节。

    别慌,同样能在PS里略微找补一下,我们可以参考高低频磨皮的操作。高低频磨皮是单纯的PS修图方法,网上教程很多,我就不截图贴一遍了。

    实际不是真的去磨皮,只要提取出原图的高频部分,挪到AI修改图层上面,混合模式选线性光,高频的细节就叠加回模特脸上了。

    因为五官整个都被AI变化了,所以高频有不重合的地方很正常。但能看出,跟原图变化不大的皮肤上的细节被找回了。

    然后就是在蒙版上涂涂抹抹,把变化大的地方擦掉,为皮肤部分叠加上高频的细节。

    现在我们就完成了照片的修改,并且还保持了原图的分辨率。

    上面的几个案例都只是利用单一功能节点工作流的演示,实际你完全可以像我在前面文章ComfyUI入门(二):如何打造自己的工作流,让AI生图/修图更高效?提到的,在现有的工作流节点中间,发挥想象力,串进更多有意思的节点,实现更加多样的功能。

    比如说,对图片进行预处理,或者同时结合进文生图的图片;比如说,在文本输入后面接上LLM,为你的提示词撰写提供一个只用电不吃饭的强有力的大脑;比如说,在单次图片编辑后加上分步的二次处理、三次处理,让整个流程更加细致和高效;比如说,创建不同的子工作流,实现不同的滤镜功能,然后在前面设计一个滤镜选择器;比如说,在ComfyUI之外,跟Photoshop的脚本形成组合……

    就看你自己的脑洞和设计了。

  • 我最常用的B端生产力工具推出了全新版本:聊聊「飞书多维表格」独立产品,以及你和你的团队应该选什么

    最近飞书有个新闻,飞书多维表格解除了跟飞书其他模块的强依赖,可以独立使用(访问链接https://v2ig.cn/srs2Zi5kVY8),不再需要下载和注册飞书套件了。

    Image

    这还挺棒的。

    我真的跟一些朋友交流过,他们的团队很需要飞书多维表格的一些自动化和AI的能力,但由于已经在其他平台生态有了不小的金钱和时间的投入,没有多余的预算去购买整个飞书套件的服务,所以只能放弃飞书多维表格。现在有了飞书多维表格单品,就很好地解决了这个问题。

    不过我也发现朋友圈子里对飞书多维表格单品有一些跑偏的解读,比如说它是一款新型的AI工具,是一个超级的Excel,或者即将取代WPS和Office等等。

    所以,作为飞书多维表格的官方大使,我觉得有必要写一篇文章跟大家好好聊聊:

    1.飞书多维表格能做什么。

    2.你和你的团队更适合飞书多维表格单品还是飞书套件。

    Image

    飞书多维表格能做什么

    飞书多维表格应该也是大家的老朋友了。

    我这个公众号的读者大概有相当一部分是因为我发布的飞书多维表格AI模板而来的。

    现在这些模板里有好几个都已经超过1000人使用了,还算挺受欢迎。

    为什么呢?

    本质并不在于模板搭建的技巧有多么高深,而是表格这种载体形式跟AI能力实在是太搭了。

    在Coze和ComfyUI中需要借助循环等技巧实现的批量化,在飞书多维表格中不过就是点击几下鼠标。

    批量化在很多时候就意味着生产力。

    不管是数据爬取、灵活推送、内容生成、图片生成、图像理解、发票识别、逻辑判断还是提炼总结,在飞书多维表格中都能以几乎零学习成本批量实现。

    这能给个人和小型工作室团队带来非常直观的效率提升。

    关于这方面我就不再多说了,如果大家使用过我之前发布的表格模板,应该有所体会。

    我们这次重点聊聊之前没提到过的B端方面。

    前面这些模板的场景我们姑且称之为生产类应用场景,对于个人或者小型工作室团队会有比较直观的效率提升;但这并非全部,因为在稍大一些的团队会存在另一类场景——管理类应用场景。

    我甚至可以发表一个有点暴论的观点:飞书多维表格在B端的业务管理和人员管理上价值要更大。

    只是针对B端的分享,其内容本身和出现的场合,决定了它不太容易形成广泛传播。

    不过恰好我们公司订阅了飞书商业旗舰版,整个团队对飞书多维表格的使用十分高频,恰好我在公司还负责搭表,所以这方面我恰好也可以唠一唠。

    飞书的成本说低真不低,举个直观的例子,我司一百多号人,商业旗舰版一年的费用就要十多万。

    Image

    有一个具体的数字在,我相信你也开始get到飞书多维表格单品的价值了。如果只订阅飞书多维表格,这笔支出至少砍掉一半还多。这一部分我们放到后面具体说,先说回我自己实际工作场景中的飞书多维表格应用。

    我们公司是一家广告代理+MCN业务的公司,有北上广深成都5个工区,还有个别同事散落在其他省市和海外远程办公,飞书多维表格对我们团队来说最大的价值是业务管理和团队协同,反而“作为快捷AI生成工具”或者“作为Coze、影刀等爬虫工具的数据写入库”这类用途,只占了很小一部分。。实际上,我们的达人媒介管理、销售CRM管理、业务执行管理,以及相对应的财务端口的绩效核算,都跟飞书多维表格深度结合在了一起。

    不过内部表格不太方便直接拿出来演示,我以一个简化的CRM环节作为示例:

    这是一个从客户引入到项目执行完成的简化流程。

    你会发现,每一个环节之间都存在相互关联的字段。例如,销售线索(客户)会存在1对多的商机,1对1对应的销售人员;商机状态变化时,会引入新的人员和子流程,例如提报方案阶段,商机会被分配对应的创意策划人员,最终成功赢单,则会接力到项目统筹人员主导的项目执行;项目则通过编号与财务系统数据相关联,同时财务数据又能够通过项目关联到商机、销售和不同执行环节对应的人员。

    如果你熟悉Excel的vlookup函数,就能发现,由于上面这一连串的环节之间存在相互关联,所以如果我已知这整个流程中的一项数据,就能够查询其他环节中的另一项数据。这个操作在飞书多维表格中同样可以使用公式完成,不过也有一个专门的功能,叫做查找引用。

    Image

    于是现在,我们就可以实现,客户行业仅在录入销售线索时进行一次录入,后面所有关于该客户的商机和项目都可以通过客户名称(或者客户编码)的关联,自动获取到行业归属。

    进而,对于每个项目,我们都会有项目金额的录入。那只要再结合简单的公式,就可以计算出每个行业的合作金额并进行排名;再进一步,引入商机状态字段,还可以分别计算出赢单/输单金额的行业排名,以及结合简单的数量统计,计算各行业的赢单输单率;再进一步,继续引入时间,我们又可以在时间维度上洞察这个月对比上个月行业排名有什么变化,赢单率有什么变化,进而设计下个月的改进方案,同时下个月又可以追踪相应调整的效果。

    同样的,人的管理也被串联了起来。我可以直接搭建仪表盘来查看每一位执行同事手里的总项目数量和金额,识别他们的工作饱和度以进行更合理的工作分配,也可以结合一定的计算,分析每一个人更擅长哪种类型的业务,更擅长维护和开拓哪种类型的客户。

    对于中小企业来说,这种形式的CRM在灵活度和使用门槛上都要比传统的SaaS类CRM友好得多。

    飞书多维表格也对企业团队提供了完善的高级权限功能。

    通过不同的角色配置,最小可以人为单位,控制其对记录和字段的可见与可编辑范围。

    Image

    例如,某些场景下,不同的销售人员之间存在竞争,那就可以将商机的记录可见范围设置为仅本人创建可见。如果在这个基础上,存在销售团队组别的管理,还可以通过人员字段带出组织架构中的直属上级,并勾选字段中包含成员的记录,这样组长就可以查看组员的记录。

    再比如,同一张表需要不同角色协同录入,但录入的信息又各自独立,那就可以通过设置字段的可见/可编辑范围避免误操作,同时集中显示对不同角色有效的数据,尤其能提高小屏同事的录入效率。

    但是,上面例子中的这种在同一张表由不同角色录入相互独立信息的情况,我个人以为不算是一个好的做表习惯。比较推荐的做法是把相关的必要信息拆分到不同表格。例如说,商机跟进和项目跟进,理论上应该分别由销售和执行两个部门的同事管理,那就把它们分为商机表和项目表两个表格。

    这时候就有一个问题,如果拆分成两个表格,那两表之间就有一些必要信息需要传递。如果项目表中没有商机编号或者商机名称,那它也不能通过这个字段去查找引用客户行业。这时候我们希望的就是,当商机变为赢单时,表格自动把商机表的商机编号、商机名称、销售人员抄到项目表新增一行。

    实现这个功能的就是工作流。

    飞书多维表格工作流的逻辑其实十分简单,你可以粗暴理解为「当……就……」。

    Image

    下面设置记录内容的意思,就是将商机表中赢单记录的商机名称、商机编号、销售人员三项内容,分别填写到项目表中对应的位置。

    Image

    现在搭建工作流我十分推荐你试一试刚推出的agent功能,只要说清楚要求,一句话就能自动完成工作流搭建,像这样:

    Image

    这也是今年飞书未来无限大会提到的飞书多维表格升级的能力之一:

    Image

    AI生成工作流的功能我不确定是否已经公测,如果没有的话可以查看一下多维表格助手的消息,加入内测群。

    不得不说字节的速度确实是快,新功能内测已经发到第三批了。

    Image

    这也算是我喜欢飞书多维表格的原因之一,迭代速度和生态在同类产品中十分强势。

    给大家两个推荐。

    ①一个是飞行社,上面有不少课程和模板都很棒:

    https://www.feishu.cn/community

    没准还能在首页刷到我司的分享和我做的模板(笑

    Image

    Image

    如果你想要找飞书多维表格在B端管理类应用场景的参考,我可以提供一个关键词——「一张表管公司」。

    ②另一个是我会平等发给每一个飞书多维表格新手同事的王大仙的飞书多维表格课程:

    https://larkcommunity.feishu.cn/wiki/RXjcwGsKsijxVskRb35caB4enng

    Image

    官方肯下功夫做这样一套全面细致的课程对企业来说尤其好,因为起码我们公司的条件是不允许在员工技能培训上投入这么大精力的。

    我是真的随时存着这个链接发给有学习飞书多维表格需求的同事的,不仅方便,细致度上也比把着手教要好。

    选单品还是选套件

    下面来回答这个很多人没搞明白的问题。

    ①首先要搞明白的,飞书多维表格单品和飞书套件分别包含什么。

    飞书多维表格单品,顾名思义,只包含飞书多维表格。

    不包含飞书用户常用的飞书会议、飞书妙记、知识库、OA审批等等功能,尤其要注意,也不包含飞书文档和飞书表格。

    飞书多维表格单品租户在「云文档」界面新建文档,只有下面这几种可选。

    Image

    可以在飞书多维表格内创建说明文档,但这个文档没办法像普通飞书文档一样独立分享出去。

    而飞书套件,哪怕是基础免费版,也是有会议、审批、知识库、飞书文档、飞书表格这些功能的,只是在额度和存储空间上有所限制。

    ②单品和套件每一个版本的功能额度与成本如何。

    飞书多维表格单品(https://base.feishu.cn)和飞书套件(https://www.feishu.cn/service)各自的官网上都有不同版本的对比。

    我用ChatGPT帮大家整理成了一个表格:

    Image

    条目挺多的,可以优先看这三项:价格、自动化运行次数、单表最大行数。

    一般情况下,我会给这样的建议:

    • 如果你没有预算,准备直接使用免费基础版,更建议选择飞书套件。 因为在飞书多维表格功能额度上,免费版基本相同,套件会带有飞书表格、飞书文档、知识库,并且目前飞书的免费基础版也支持使用「知识问答」功能。这些都很实用,不建议错过。
    • 50元/人/月的飞书商业标准版,如果你对飞书多维表格有需求,说实话这个档位有点鸡肋了。每个月只有200次自动化运行次数,不管是自动新增记录还是推送消息提醒,都会消耗这个次数,但凡你的团队有三五个人,基本上不可能够用的。并且它还不带有完整的飞书多维表格高级权限。 相比起来,35元/人/月的飞书多维表格专业版,支持高级权限,每月5w次自动化运行次数,2w条的表格最大行数,额度就要宽裕得多。我会更建议你考虑看看飞书多维表格单品专业版,或者飞书商业专业版,但要注意飞书商业专业版的自动化次数是5k/月,先评估下是否够用。
    • 80元/人/月的飞书商业专业版,自动化次数是跟飞书多维表格专业版相比在功能上的硬差距。此外的核心差异就在性价比,飞书多维表格专业版的价格不到飞书专业版的一半。假设你目前是一个10人的团队,飞书套件专业版会比飞书多维表格单品专业版多出5400元的成本。尤其是当你现有的办公软件生态是企微或者钉钉时,我更建议你选择飞书多维表格单品的专业版,通过webhook一样可以触发工作流和收发提醒。
    • 120元/人/月的飞书商业旗舰版,这也是我现在所在的团队目前正在使用的版本。5w的表格行数,50w次/月的自动化运行次数,在单纯的飞书多维表格层面,基本已经足够一个100人的团队使用了。是否选择这个版本实际上看的是飞书多维表格之外的办公生态,如果你的团队已经深度融入企微、钉钉的生态,那当然要先看飞书多维表格单品。不过以我个人的使用体验来说,飞书套件中的飞书文档、飞书表格(飞书多维表格本质上是个数据库,不是万能解药,并不能取代电子表格)、知识库、飞书会议(例如实时AI翻译就是旗舰版的功能,跟海外同事沟通或者看海外产品发布会都挺有用)这些产品也是我非常高频使用的产品,如果你的办公产品生态是可迁移的,也可以试试飞书。
    • 35元/人/月的飞书多维表格专业版,只要是深度使用飞书之外的办公生态,对飞书多维表格有需求,优先看这个版本就没错。相比套件来说,单品专业版的性价比十分优秀,自动化次数10倍于套件专业版,价格却不到一半,直接省掉原本打包购买套件浪费的钱。如果在这个基础上,你的记录数量非常多,自动化用量非常大,那就可以再去询价一下飞书多维表格单品的旗舰版。

    好了,以上就是本文的全部内容,希望对你在飞书多维表格的使用和版本选择上有所帮助。

  • TicNote:出门问问做了一款「有用」的AI硬件

    开门见山了,非常推荐。

    TicNote对于日常需要进行大量电话沟通(包括微信电话)的商务人士,以及经常需要开大小团队会议(包括线上和线下)的白领群体,是实实在在的效率工具。如果你是这两类群体,真的非常建议你认真看一下这款产品:

    我自己深度使用了它20多天,你可以先听听我的使用感受。

    AI录音卡这个产品形态,我给它的关键词是「有用」。

    它可以说是目前为数不多能够做到硬件与软件相辅相成,并在真实工作场景中提供效率加持的AI硬件。(多提一嘴,这个形态并非出门问问首创,但TicNote应该是当下唯一能直接在京东买到的国行产品。)

    软件层面不用多说,用过飞书/腾讯/钉钉会议等几款主流会议工具的应该都有体会,AI总结是大语言模型最成熟的几个落地场景之一。

    硬件层面,磁吸卡片的形态十分讨巧。足够轻薄,且直接物理绑定了「手机」这个超级入口,几乎就相当于无痛「侵入」了每个人的所有场景(*但提醒下蓝牙通话场景例外,如果你是24h挂着蓝牙耳机通话/开会的人,AI录音卡可能不太适合你)。并且录音的功耗足够低,它的续航可以做到足够长(实测我的TicNote用了20天左右,只充过一次电)。这些特性,让它几乎不增加用户任何的使用成本。

    别小看这一点。眼镜同样可以通过绑定近视人群高频「侵入」用户场景,但我戴了Rayban-Meta二代眼镜整整一年,并且还是雷鸟V3的首发用户,作为一个比较深度的AI眼镜用户,我可以告诉你,现在的智能眼镜不够轻,而且续航太短,硬件的成熟度相比录音卡还是要差一些。

    硬件有形态上的优势和独特性,软件的应用场景成熟度又非常高,这才能做到有机结合。不像有些单纯生硬嫁接AI能力作为噱头的「AI硬件」:在键鼠上提供一个物理按键,按下后在电脑呼出一个完全独立的AI工具箱软件;蓝牙耳机捕获到语音触发词后自动打开手机App,然后让用户在App里玩AI生图……

    以我自己认为TicNote最核心的两个使用场景来说:

    1.通话录音。磁吸形态让它能以类似骨传导耳机的原理,通过振动传导传感器清晰录制手机通话。第一,搞定了微信语音这类既非传统电话、也非线上会议的特殊通话形式的录音需求;第二,解决了iPhone用户电话录音提示音的尴尬。并且在通话录音这块,几乎没有更优的外挂方案了。

    比如下面这个就是我的一个通话录音:

    背景是我在某软件的购买的套餐记错了过期时间,导致额度几乎没怎么使用就过期了,于是咨询了线上客服看能不能帮我进行一下延期处理。结果我正在机场登机的时候对方电话打了过来,当时的情况是又慌乱又嘈杂,我甚至耳朵都听不太清楚对方说什么,等再飞几个小时晚上折腾到酒店,我这脑子真不好说还能不能记得自己该干啥。

    但我当时通过TicNote把通话录了下来,有空的时候一看AI总结我就知道自己要怎么操作了。

    TicNote有一个叫Shadow AI的配套Agent,它有一个功能对销售、客服、用户调研和回访人员都非常有用:

    你可以把多段录音编成一个项目,然后在这个项目中就能够针对所有录音进行AI对话。

    这样就可以让AI来做一整天的沟通复盘,找到共性的亮点和问题,也可以直接生成日报,把一天的电话沟通重点一键整理成一张表格。或者其他的任何能想得到的整合分析场景,比如练习讲PPT,比如多次面试的分析和反思……

    不过我自己的工作性质不太跟上面提到的几项相关,就简单贴图展示下单场会议的AI问答:

    2.团队会议。在会议室开会,不需要有多余的蓝牙连接、佩戴动作,我只需要把手机平扣在桌上,按下录音键就能清晰地录到声音,包括线上会议时从电脑扬声器传出的同事的声音。收音质量其实不如专业的全指向型会议麦克风,但在会议室中的收音表现完全够用,而且不需要占用正在使用的手机和电脑设备。

    下面放一段实录音频你可以感受一下。

    这是一段我跟ChatGPT的网页对话,录制环境是这样:

    我手持手机,通过电脑的麦克风跟ChatGPT对话,音箱在高处,直线距离我大概1米左右,因为时间是晚上,音量开在30%左右。

    这是TicNote的录音:

    (ChatGPT真太有节目了,说自己是豆包。)

    这是这段音频在TicNote App转录出来的文字:

    我知道你看到这儿,可能还没Get到它的点。

    甚至会有个疑问:我用飞书妙记不是能达到一样的效果吗?

    但是,使用硬件设备物理录音,恰恰解决的就是飞书搔不到的痒处。

    我管这个叫「会议资料私有化」。

    你要知道,不管是飞书,还是钉钉,还是腾讯会议,他们的性质都是面向企业的办公工具。作为企业工具,它一定要有层级分明的权限和角色系统,这是企业安全的要求。

    但是,在真实的落地环境,尤其是中小企业,哪有那么多涉密的信息呀。反过来,还会有很多老板和同事对权限操作真的不是很熟悉。

    如果你们公司也是飞书的客户,你应该跟我一样很熟悉这个提示:

    开完会后几分钟,飞书的会议助手才会弹出图中这样的提示。作为会议主持人,你要记得去点击「分享妙记给所有参会人」,这样其他人才能够查看这个会议的录屏和总结;而作为参会人,如果主持人忘记点击分享妙记,你需要点击「申请妙记权限」,然后等他同意,你才可以查看会议的回复。你应该能想象到,作为员工和中层管理,在这个环节上可能会产生什么样的问题和心理博弈……

    上面这个其实还不算大事儿,真有主动性且着急了,大不了打个电话。

    下面这个情况才是真正让人头大:

    首先还是得批评飞书一下。虽然飞书的妙记和会议总结相比从前干巴巴的开会,可以说是划时代的提升。但以今天的眼光来看,飞书默认的会议总结,表现有些一般。

    并且对于比较高强度的AI使用者来说,不同性质的会议,使用不同的总结角度来呈现,也算是一个基本操作。比如前段时间飞书的发布会,下面是我开了一个飞书会议转录文本然后自己用AI总结的重点:

    自带的会议总结真的很难达到这样的效果,所以导出文字记录算是我的刚需。

    但是,如上上张图片所示,仅限拥有管理员权限的用户(妙记所有者)才能导出或者批准参会人导出。

    于是问题就来了。上面的「申请妙记权限」只需要点下同意就行,基本不会在这儿有什么卡点。但这个导出权限配置,很多同事他是真不会……很多时候真就是得求着人家听你教他一遍,还很不好意思耽误了人家的时间。

    还有一点,飞书会议的音频转录实际上是只有ASR但不带说话人分离的,它的说话人区分来自于账号角色。这个设定对于数字游民组成的团队,我想是完全没有问题的。但我所在的公司是异地集体办公,而且还是乙方,这就使得我们的典型会议场景是:几波人在不同地点的会议室开线上会议,每个会议室留一台设备开麦和声音。结果就是说话人完全没有区分,回看时要通过上下文分析+看视频验证确定哪一句是谁说的话,最后AI整理的TODO,任务执行人也是错的。

    而TicNote的转录自带说话人分离,可以通过声音特点区分不同说话人:

    只要简单在App配置一下,就能够区分该次会议的每一个发言人。

    AI模型和Prompt模板上,TicNote也提供了多种选择。

    目前可选的模型有豆包1.5pro、通义千问Max、DeepSeek-V3和Kimi最新的K2;针对不同行业,也内置了不同的总结模板。

    并且,它还支持自定义Prompt:

    相信我,这是一个没用过的时候觉得没啥,用习惯之后又离不开的功能。不同的Prompt做会议总结会得到不同的侧重点和不同的视角。

    我自己的Prompt就不放上来献丑了,贴一个我也很喜欢用的另一个Prompt,抄自李继刚大神的公众号:

    = 会议纪要 =
    

    你可以找一个自己的会议转录试一下,应该会得到一个不太一样的会议评估视角

    最终的输出形式上,除了常规的文字总结外,TicNote也提供了一些其他的选择,比如思维导图:

    或者一键转换成播客,以二人双簧的形式再把内容回顾一遍:

    值得一提的是这个深度研究。

    它不太像我们常规意义上用的deep research,而是自动基于录音内容生成一个类似拓展阅读的研究大纲,在这个基础上进行延展性深入研究。

    不过最后输出的研究报告,有一说一,我认为离SOTA水平还有些差距。所以个人不太建议把积分花在这个上面,如果真想基于录音内容进行深入延展,直接拿TicNote给出的这个研究大纲,放进一个SOTA级别的AI或者AI Agent做深度研究,效果也许更好一些。

    提到积分,就得讲讲TicNote的收费模式了:

    有999/1499两个版本,硬件实际上都是一样的。

    只持有硬件不开通会员,每个月会赠送300免费积分,等同于300分钟的AI会议转录或翻译时间;如果订阅会员,每个月则是有1500免费积分,如果还不够,单独追加购买积分是22元/150积分(量大会便宜,细节就不展开了)。

    不同价位的版本区别就是赠送会员时长不同,999版本送三个月会员,1499版本首发是送18个月会员(首发过后是1年)。

    从会员时长看,现在购买1499版本更划算一些。但建议先评估一下自己每个月的使用时长,「每月赠送积分+按需购买补充包」的组合可能比直接购买长时长的会员更划算。

    作为一个NAS玩家,我知道现在肯定有人问:如果厂家倒闭或者停止服务了怎么办?

    我在上手之前也一直担心这个问题,所以第一时间做了测试。

    现在我可以公布答案:

    在账号没有任何积分额度的情况下,它可以作为一个单纯的卡片式录音笔来使用,支持以MP3或WAV格式,以蓝牙/WiFi的形式导出,硬件性能不会受到什么影响。

    基础的软件功能其实在AI时代也不需要太过担心。

    我刚才花了半小时,就用AI编程实现了一个纯前端的mp3文件ASR应用。支持调用硅基流动和火山引擎两个平台的API,硅基流动是免费API,火山引擎的API需要付费(20小时免费试用后)但支持说话人分离。

    只要填上对应的API Key和APP ID/Access Token就能使用。

    部署在了youware,感兴趣可以拿来玩:

    https://www.youware.com/project/r6hppllqt4?enter_from=share&invite_code=J8TX8R93WI

    如果你对源码感兴趣,我把它贴在本文左下角的查看原文链接内了。在这个基础上再花一些时间,接入LLM的API,内置几个会议总结的Prompt,延续会议总结的功能也并不难。不需要担心它有一天会完全不能用,以前软件上的很多门槛在被抹平,这是这个时代的红利。

    动手能力强的朋友,没准可以连带着硬件一起,自己DIY一个类似的产品出来,应该也挺有意思的。

    但也不是说我建议大家都自己开发配套啊,要达到原版这样,实现项目知识库、深度研究、导图和播客生成、自定义词库、日志汇总、智能推送等等细节功能并把体验优化到足够好,成本(包括开发、服务器、API)还是很高的。

    真的有直接使用需求,购买现成的产品还是最省心的选择。

    如果我提到的两个场景也是你的核心场景,那我可以向你推荐TicNote。

  • 录音转文字

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>录音转文字</title>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
    
            body {
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                min-height: 100vh;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 20px;
            }
    
            .container {
                background: rgba(255, 255, 255, 0.95);
                backdrop-filter: blur(10px);
                border-radius: 20px;
                padding: 40px;
                box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
                max-width: 700px;
                width: 100%;
            }
    
            h1 {
                text-align: center;
                color: #333;
                margin-bottom: 30px;
                font-size: 2.5em;
                background: linear-gradient(45deg, #667eea, #764ba2);
                -webkit-background-clip: text;
                -webkit-text-fill-color: transparent;
            }
    
            .config-section {
                margin-bottom: 30px;
                padding: 20px;
                background: rgba(102, 126, 234, 0.1);
                border-radius: 15px;
                border-left: 4px solid #667eea;
            }
    
            .config-section h3 {
                color: #333;
                margin-bottom: 15px;
            }
    
            .input-group {
                margin-bottom: 15px;
            }
    
            label {
                display: block;
                color: #555;
                margin-bottom: 5px;
                font-weight: 500;
            }
    
            input, select {
                width: 100%;
                padding: 12px;
                border: 2px solid #ddd;
                border-radius: 10px;
                font-size: 16px;
                transition: all 0.3s ease;
            }
    
            input:focus, select:focus {
                outline: none;
                border-color: #667eea;
                box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
            }
    
            small {
                display: block;
                margin-top: 5px;
                color: #666;
                font-size: 12px;
            }
    
            .record-section {
                text-align: center;
                margin: 30px 0;
            }
    
            .record-btn {
                background: linear-gradient(45deg, #667eea, #764ba2);
                color: white;
                border: none;
                padding: 20px 40px;
                border-radius: 50px;
                font-size: 18px;
                font-weight: bold;
                cursor: pointer;
                transition: all 0.3s ease;
                margin: 10px;
                box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
            }
    
            .record-btn:hover {
                transform: translateY(-3px);
                box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
            }
    
            .record-btn:active {
                transform: translateY(0);
            }
    
            .record-btn.recording {
                background: linear-gradient(45deg, #ff6b6b, #ee5a52);
                animation: pulse 2s infinite;
            }
    
            @keyframes pulse {
                0% { transform: scale(1); }
                50% { transform: scale(1.05); }
                100% { transform: scale(1); }
            }
    
            .status {
                margin: 20px 0;
                padding: 15px;
                border-radius: 10px;
                text-align: center;
                font-weight: 500;
            }
    
            .status.info {
                background: rgba(52, 152, 219, 0.1);
                color: #2980b9;
                border: 1px solid rgba(52, 152, 219, 0.3);
            }
    
            .status.success {
                background: rgba(46, 204, 113, 0.1);
                color: #27ae60;
                border: 1px solid rgba(46, 204, 113, 0.3);
            }
    
            .status.error {
                background: rgba(231, 76, 60, 0.1);
                color: #c0392b;
                border: 1px solid rgba(231, 76, 60, 0.3);
            }
    
            .result-section {
                margin-top: 30px;
            }
    
            .result-text {
                background: #f8f9fa;
                border: 2px solid #e9ecef;
                border-radius: 15px;
                padding: 20px;
                min-height: 120px;
                font-size: 16px;
                line-height: 1.6;
                white-space: pre-wrap;
                word-wrap: break-word;
            }
    
            .audio-player {
                margin: 20px 0;
                width: 100%;
            }
    
            .file-upload {
                margin: 20px 0;
                text-align: center;
            }
    
            .file-upload input[type="file"] {
                display: none;
            }
    
            .file-upload label {
                display: inline-block;
                padding: 15px 30px;
                background: linear-gradient(45deg, #28a745, #20c997);
                color: white;
                border-radius: 25px;
                cursor: pointer;
                transition: all 0.3s ease;
                font-weight: bold;
            }
    
            .file-upload label:hover {
                transform: translateY(-2px);
                box-shadow: 0 10px 25px rgba(40, 167, 69, 0.3);
            }
    
            .speaker-timeline {
                margin: 20px 0;
                padding: 15px;
                background: #f8f9fa;
                border-radius: 10px;
                border-left: 4px solid #667eea;
            }
    
            .speaker-segment {
                margin: 8px 0;
                padding: 10px;
                border-radius: 8px;
                position: relative;
            }
    
            .speaker-1 {
                background: rgba(102, 126, 234, 0.1);
                border-left: 3px solid #667eea;
            }
    
            .speaker-2 {
                background: rgba(255, 107, 107, 0.1);
                border-left: 3px solid #ff6b6b;
            }
    
            .speaker-3 {
                background: rgba(46, 204, 113, 0.1);
                border-left: 3px solid #2ecc71;
            }
    
            .speaker-4 {
                background: rgba(241, 196, 15, 0.1);
                border-left: 3px solid #f1c40f;
            }
    
            .speaker-label {
                font-weight: bold;
                color: #555;
                margin-bottom: 5px;
                font-size: 14px;
            }
    
            .speaker-time {
                font-size: 12px;
                color: #888;
                margin-left: 10px;
            }
    
            .speaker-text {
                margin-top: 5px;
                line-height: 1.4;
            }
    
            .analysis-section {
                margin: 20px 0;
                padding: 15px;
                background: rgba(52, 152, 219, 0.1);
                border-radius: 10px;
                border-left: 4px solid #3498db;
            }
    
            .toggle-section {
                margin: 10px 0;
            }
    
            .toggle-btn {
                background: #3498db;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 5px;
                cursor: pointer;
                font-size: 14px;
            }
    
            .toggle-btn:hover {
                background: #2980b9;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>🎙️ 录音转文字</h1>
            
            <!-- API配置区域 -->
            <div class="config-section">
                <h3>⚙️ API配置</h3>
                <div class="input-group">
                    <label for="apiProvider">API服务商:</label>
                    <select id="apiProvider">
                        <option value="siliconflow">SiliconFlow (纯转写)</option>
                        <option value="volcengine">火山引擎 (支持说话人分离)</option>
                    </select>
                </div>
                
                <!-- SiliconFlow配置 -->
                <div id="siliconflowConfig">
                    <div class="input-group">
                        <label for="apiUrl">API地址:</label>
                        <input type="text" id="apiUrl" value="https://api.siliconflow.cn/v1/audio/transcriptions">
                    </div>
                    <div class="input-group">
                        <label for="apiKey">API密钥:</label>
                        <input type="password" id="apiKey" placeholder="请输入您的SiliconFlow API密钥">
                    </div>
                    <div class="input-group">
                        <label for="model">模型:</label>
                        <input type="text" id="model" value="FunAudioLLM/SenseVoiceSmall">
                    </div>
                </div>
                
                <!-- 火山引擎配置 -->
                <div id="volcengineConfig" style="display: none;">
                    <div class="input-group">
                        <label for="volcSubmitUrl">提交任务API:</label>
                        <input type="text" id="volcSubmitUrl" value="https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit">
                    </div>
                    <div class="input-group">
                        <label for="volcQueryUrl">查询结果API:</label>
                        <input type="text" id="volcQueryUrl" value="https://openspeech.bytedance.com/api/v3/auc/bigmodel/query">
                    </div>
                    <div class="input-group">
                        <label for="volcAppKey">APP ID:</label>
                        <input type="text" id="volcAppKey" placeholder="火山引擎控制台获取的APP ID">
                    </div>
                    <div class="input-group">
                        <label for="volcAccessKey">Access Token:</label>
                        <input type="password" id="volcAccessKey" placeholder="火山引擎控制台获取的Access Token">
                    </div>
                    
                    <!-- 音频URL获取方式选择 -->
                    <div class="input-group">
                        <label for="audioUrlMethod">音频URL获取方式:</label>
                        <select id="audioUrlMethod">
                            <option value="direct">直接输入音频URL</option>
                            <option value="upload">通过上传服务获取URL</option>
                        </select>
                    </div>
                    
                    <!-- 直接输入URL选项 -->
                    <div id="directUrlConfig">
                        <div class="input-group" style="background: rgba(46, 204, 113, 0.1); padding: 15px; border-radius: 8px; border-left: 3px solid #2ecc71;">
                            <label for="directAudioUrl">音频文件URL:</label>
                            <input type="url" id="directAudioUrl" placeholder="https://example.com/your-audio-file.mp3">
                            <small>请输入音频文件的公网直链地址(可直接下载的URL)</small>
                            <button type="button" id="validateUrlBtn" style="margin-top: 8px; padding: 6px 12px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">🔍 验证链接</button>
                            <div id="urlValidationResult" style="margin-top: 8px; display: none;"></div>
                            <div style="margin-top: 10px; padding: 8px; background: rgba(255, 255, 255, 0.7); border-radius: 5px;">
                                <strong>✅ 有效的直链来源:</strong><br>
                                1. <strong>您自己的网站服务器</strong>:如 https://yoursite.com/audio/file.mp3<br>
                                2. <strong>GitHub Raw链接</strong>:上传到GitHub仓库,获取Raw文件链接<br>
                                3. <strong>专业CDN服务</strong>:如阿里云OSS、腾讯云COS等的公开链接<br>
                                4. <strong>免费直链托管</strong>:使用下方的"通过上传服务获取URL"<br>
                                <br>
                                <strong style="color: #e74c3c;">❌ 无效链接(不是直链):</strong><br>
                                • 百度网盘、OneDrive等云盘分享链接<br>
                                • 需要登录或验证的链接<br>
                                • 重定向链接<br>
                                <br>
                                <small style="color: #666;"><strong>测试方法:</strong>直接在浏览器中打开URL,应该直接下载/播放音频文件</small>
                            </div>
                        </div>
                    </div>
                    
                    <!-- 上传服务选项 -->
                    <div id="uploadServiceConfig" style="display: none;">
                        <div class="input-group" style="background: rgba(40, 167, 69, 0.1); padding: 15px; border-radius: 8px; border-left: 3px solid #28a745;">
                            <label>✅ 免费直链托管服务:</label>
                            <p style="margin: 5px 0; color: #155724; font-size: 14px;">
                                <strong>自动上传并获取直链</strong><br>
                                • 支持 catbox.moe、file.io 等服务<br>
                                • 自动生成可用于火山引擎API的直链<br>
                                • 如果遇到网络问题,请尝试几次或使用自定义服务<br>
                            </p>
                        </div>
                        <div class="input-group">
                            <label for="customUploadUrl">自定义上传服务API (可选):</label>
                            <input type="text" id="customUploadUrl" placeholder="http://your-server.com/upload">
                            <small>如果您有自己的文件上传API,请填入。API应返回 JSON: {"url": "直链地址"}</small>
                        </div>
                    </div>
                    
                    <div class="input-group">
                        <label>
                            <input type="checkbox" id="enableSpeakerDetection" style="width: auto; margin-right: 10px;" checked>
                            启用说话人分离
                        </label>
                    </div>
                </div>
            </div>
    
            <!-- 文件上传区域 -->
            <div class="file-upload">
                <div id="fileUploadSection">
                    <label for="audioFile">📁 选择音频文件</label>
                    <input type="file" id="audioFile" accept="audio/*">
                </div>
                
                <div id="directUrlSection" style="display: none;">
                    <div style="text-align: center; padding: 20px; background: rgba(46, 204, 113, 0.1); border-radius: 15px; border: 2px dashed #2ecc71;">
                        <p style="margin: 0; color: #27ae60; font-weight: bold;">🔗 使用直接URL模式</p>
                        <p style="margin: 5px 0 0 0; color: #666; font-size: 14px;">请在上方火山引擎配置中输入音频文件URL</p>
                    </div>
                </div>
            </div>
    
            <!-- 开始转换区域 -->
            <div class="record-section">
                <button id="convertBtn" class="record-btn" style="display: none; background: linear-gradient(45deg, #28a745, #20c997);">🚀 开始转换</button>
            </div>
    
            <!-- 状态显示 -->
            <div id="status" class="status" style="display: none;"></div>
    
            <!-- 音频播放器 -->
            <audio id="audioPlayer" class="audio-player" controls style="display: none;"></audio>
    
            <!-- 结果显示区域 -->
            <div class="result-section">
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
                    <h3>📝 转换结果:</h3>
                    <button id="exportBtn" class="toggle-btn" style="display: none; background: #28a745;">📄 导出TXT</button>
                </div>
                
                <!-- 转换结果文本 -->
                <div id="resultText" class="result-text">在这里将显示语音转文字的结果...</div>
                
                <!-- 音频分析信息 -->
                <div id="audioAnalysis" class="analysis-section" style="display: none;">
                    <h4>🔍 音频分析:</h4>
                    <div id="analysisInfo"></div>
                </div>
            </div>
        </div>
    
        <script>
            let currentAudioBlob;
            let currentTranscription = ''; // 保存当前转换结果
            let currentSpeakerSegments = []; // 保存当前说话人分离结果
    
            const convertBtn = document.getElementById('convertBtn');
            const exportBtn = document.getElementById('exportBtn');
            const status = document.getElementById('status');
            const resultText = document.getElementById('resultText');
            const audioPlayer = document.getElementById('audioPlayer');
            const audioFileInput = document.getElementById('audioFile');
            const audioAnalysis = document.getElementById('audioAnalysis');
            const analysisInfo = document.getElementById('analysisInfo');
            const apiProvider = document.getElementById('apiProvider');
            const siliconflowConfig = document.getElementById('siliconflowConfig');
            const volcengineConfig = document.getElementById('volcengineConfig');
            const audioUrlMethod = document.getElementById('audioUrlMethod');
            const directUrlConfig = document.getElementById('directUrlConfig');
            const uploadServiceConfig = document.getElementById('uploadServiceConfig');
            const directAudioUrlInput = document.getElementById('directAudioUrl');
            const customUploadUrlInput = document.getElementById('customUploadUrl');
            const fileUploadSection = document.getElementById('fileUploadSection');
            const directUrlSection = document.getElementById('directUrlSection');
            const validateUrlBtn = document.getElementById('validateUrlBtn');
            const urlValidationResult = document.getElementById('urlValidationResult');
    
            // API提供商切换
            function switchApiProvider() {
                const provider = apiProvider.value;
                if (provider === 'siliconflow') {
                    siliconflowConfig.style.display = 'block';
                    volcengineConfig.style.display = 'none';
                    // 硅基流动只支持文件上传
                    fileUploadSection.style.display = 'block';
                    directUrlSection.style.display = 'none';
                    // 重置火山引擎配置
                    directUrlConfig.style.display = 'none';
                    uploadServiceConfig.style.display = 'none';
                    customUploadUrlInput.value = '';
                    directAudioUrlInput.value = '';
                    audioUrlMethod.value = 'direct';
                } else if (provider === 'volcengine') {
                    siliconflowConfig.style.display = 'none';
                    volcengineConfig.style.display = 'block';
                    // 重置硅流配置
                    document.getElementById('apiUrl').value = 'https://api.siliconflow.cn/v1/audio/transcriptions';
                    document.getElementById('apiKey').value = '';
                    document.getElementById('model').value = 'FunAudioLLM/SenseVoiceSmall';
    
                    // 根据音频URL获取方式显示配置
                    switchAudioUrlMethod();
                }
                
                // 重置结果显示
                resetResults();
                // 重置转换按钮状态
                updateConvertButtonState();
            }
    
            // 音频URL获取方式切换
            function switchAudioUrlMethod() {
                if (apiProvider.value !== 'volcengine') return;
                
                if (audioUrlMethod.value === 'direct') {
                    directUrlConfig.style.display = 'block';
                    uploadServiceConfig.style.display = 'none';
                    fileUploadSection.style.display = 'none';
                    directUrlSection.style.display = 'block';
                    customUploadUrlInput.value = '';
                } else {
                    directUrlConfig.style.display = 'none';
                    uploadServiceConfig.style.display = 'block';
                    fileUploadSection.style.display = 'block';
                    directUrlSection.style.display = 'none';
                    directAudioUrlInput.value = '';
                }
                updateConvertButtonState();
            }
    
            // 更新转换按钮状态
            function updateConvertButtonState() {
                const provider = apiProvider.value;
                let canConvert = false;
                
                if (provider === 'siliconflow') {
                    // 硅基流动需要文件
                    canConvert = currentAudioBlob !== null;
                } else if (provider === 'volcengine') {
                    if (audioUrlMethod.value === 'direct') {
                        // 直接URL模式,检查URL是否已填写
                        const url = directAudioUrlInput.value.trim();
                        canConvert = url && (url.startsWith('http://') || url.startsWith('https://'));
                    } else {
                        // 上传服务模式,需要文件
                        canConvert = currentAudioBlob !== null;
                    }
                }
                
                if (canConvert) {
                    convertBtn.style.display = 'inline-block';
                    if (provider === 'volcengine' && audioUrlMethod.value === 'direct') {
                        showStatus(`🔗 已配置直接URL,点击"开始转换"进行语音识别`, 'success');
                    }
                } else {
                    convertBtn.style.display = 'none';
                }
            }
    
            // 重置结果显示
            function resetResults() {
                // speakerResults.style.display = 'none'; // 移除说话人结果显示
                // originalResults.style.display = 'block'; // 移除完整文本显示
                audioAnalysis.style.display = 'none';
                // toggleView.style.display = 'none'; // 移除视图切换按钮
                exportBtn.style.display = 'none';
                // currentViewMode = 'original'; // 移除视图模式变量
                currentTranscription = '';
                currentSpeakerSegments = [];
                resultText.textContent = '在这里将显示语音转文字的结果...';
            }
    
            // 显示状态消息
            function showStatus(message, type = 'info') {
                status.textContent = message;
                status.className = `status ${type}`;
                status.style.display = 'block';
                console.log(`[${type.toUpperCase()}] ${message}`); // 添加控制台日志
            }
    
            // 隐藏状态消息
            function hideStatus() {
                status.style.display = 'none';
            }
    
            // 导出为TXT文件
            function exportToTxt() {
                let content = '';
                const filename = `语音转文字_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
                
                if (currentSpeakerSegments.length > 0) {
                    // 有说话人分离结果,导出分离结果+完整文本
                    content = '=== 说话人分离结果 ===\n\n';
                    currentSpeakerSegments.forEach((segment) => {
                        content += `[说话人 ${segment.speaker}] (${formatTime(segment.startTime)} - ${formatTime(segment.endTime)})\n`;
                        content += `${segment.text}\n\n`;
                    });
                    content += `\n=== 完整文本 ===\n\n${currentTranscription}`;
                } else {
                    // 没有说话人分离结果,导出完整文本
                    content = currentTranscription;
                }
                
                if (!content.trim()) {
                    showStatus('⚠️ 没有可导出的内容', 'error');
                    return;
                }
                
                // 创建下载链接
                const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = filename;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                
                showStatus('📄 文件已导出!', 'success');
            }
    
            // 上传文件到托管服务(改进版)
            async function uploadToCatbox(file) {
                showStatus('📤 正在尝试免费托管服务...', 'info');
                
                // 可用的托管服务列表
                const uploadServices = [
                    {
                        name: 'catbox.moe',
                        upload: async (file) => {
                            const formData = new FormData();
                            formData.append('reqtype', 'fileupload');
                            formData.append('fileToUpload', file);
                            
                            const response = await fetch('https://catbox.moe/user/api.php', {
                                method: 'POST',
                                body: formData
                            });
                            
                            if (response.ok) {
                                const result = await response.text();
                                if (result.startsWith('https://files.catbox.moe/')) {
                                    return result.trim();
                                }
                            }
                            throw new Error('catbox.moe 上传失败');
                        }
                    },
                    {
                        name: 'file.io',
                        upload: async (file) => {
                            const formData = new FormData();
                            formData.append('file', file);
                            
                            const response = await fetch('https://file.io', {
                                method: 'POST',
                                body: formData
                            });
                            
                            if (response.ok) {
                                const result = await response.json();
                                if (result.success && result.link) {
                                    return result.link;
                                }
                            }
                            throw new Error('file.io 上传失败');
                        }
                    }
                ];
                
                let lastError = null;
                
                // 尝试各个服务
                for (let i = 0; i < uploadServices.length; i++) {
                    const service = uploadServices[i];
                    try {
                        showStatus(`📤 正在尝试 ${service.name}... (${i + 1}/${uploadServices.length})`, 'info');
                        console.log(`尝试上传到: ${service.name}`);
                        
                        const url = await service.upload(file);
                        console.log(`${service.name} 上传成功:`, url);
                        
                        showStatus(`✅ 文件上传成功!使用服务: ${service.name}`, 'success');
                        return url;
                        
                    } catch (error) {
                        console.warn(`${service.name} 上传失败:`, error.message);
                        lastError = error;
                        
                        if (i < uploadServices.length - 1) {
                            showStatus(`⚠️ ${service.name} 失败,尝试下一个服务...`, 'info');
                            await new Promise(resolve => setTimeout(resolve, 1000));
                        }
                    }
                }
                
                // 所有服务都失败了
                console.error('所有免费托管服务都失败了');
                showStatus(`❌ 免费托管服务暂时不可用`, 'error');
                
                // 提供详细的解决方案
                const errorMessage = `
    免费托管服务暂时不可用,请尝试以下方案:
    
    ✅ 推荐方案:
    1. 使用您自己的服务器上传音频文件
    2. 上传到GitHub仓库,获取Raw链接
    3. 使用专业CDN服务(阿里云OSS、腾讯云COS等)
    
    🔧 GitHub直链教程:
    1. 登录GitHub,创建新仓库
    2. 上传音频文件到仓库
    3. 点击文件 → Raw 按钮
    4. 复制Raw链接地址
    
    💡 自定义上传服务:
    如果您有自己的文件上传API,请在上方填写自定义上传服务地址
    `;
                
                throw new Error(errorMessage);
            }
    
            // 生成UUID
            function generateUUID() {
                return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                    const r = Math.random() * 16 | 0;
                    const v = c == 'x' ? r : (r & 0x3 | 0x8);
                    return v.toString(16);
                });
            }
    
            // 创建音频URL
            async function createAudioUrl(audioBlob) {
                const customUploadUrl = customUploadUrlInput.value.trim();
                
                // 如果用户提供了自定义上传服务,优先使用
                if (customUploadUrl) {
                    try {
                        showStatus('📤 正在使用自定义上传服务...', 'info');
                        console.log('尝试自定义上传服务:', customUploadUrl);
                        
                        const formData = new FormData();
                        formData.append('file', audioBlob);
                        formData.append('audio', audioBlob); // 兼容不同的字段名
                        
                        const response = await fetch(customUploadUrl, {
                            method: 'POST',
                            body: formData
                        });
                        
                        if (!response.ok) {
                            throw new Error(`自定义服务响应失败: ${response.status}`);
                        }
                        
                        const result = await response.json();
                        const url = result.url || result.file_url || result.link;
                        
                        if (url) {
                            console.log('自定义服务上传成功:', url);
                            showStatus('✅ 自定义服务上传成功!', 'success');
                            return url;
                        } else {
                            throw new Error('自定义服务返回数据格式不正确');
                        }
                        
                    } catch (error) {
                        console.warn('自定义上传服务失败,转用免费服务:', error.message);
                        showStatus('⚠️ 自定义服务失败,转用免费托管服务...', 'info');
                    }
                }
                
                // 使用免费托管服务
                return await uploadToCatbox(audioBlob);
            }
    
            // 将音频转换为文字
            async function convertToText() {
                const provider = apiProvider.value;
                
                // 检查是否需要文件
                if (provider === 'siliconflow' || (provider === 'volcengine' && audioUrlMethod.value === 'upload')) {
                    if (!currentAudioBlob) {
                        showStatus('⚠️ 请先选择音频文件', 'error');
                        return;
                    }
                }
    
                console.log('开始转换,提供商:', provider, '文件大小:', currentAudioBlob ? currentAudioBlob.size : '使用URL');
                
                try {
                    if (provider === 'siliconflow') {
                        await convertWithSiliconFlow(currentAudioBlob);
                    } else if (provider === 'volcengine') {
                        await convertWithVolcEngine(currentAudioBlob);
                    }
                } catch (error) {
                    console.error('转换失败:', error);
                    showStatus(`❌ 转换失败: ${error.message}`, 'error');
                    displayErrorInfo(provider, error);
                }
            }
    
            // SiliconFlow转换(纯转写)
            async function convertWithSiliconFlow(audioBlob) {
                const apiUrl = document.getElementById('apiUrl').value.trim();
                const apiKey = document.getElementById('apiKey').value.trim();
                const model = document.getElementById('model').value.trim();
    
                if (!apiKey) {
                    throw new Error('请填入SiliconFlow的API密钥');
                }
    
                showStatus('🔄 正在转换为文字...', 'info');
                
                const formData = new FormData();
                formData.append('file', audioBlob, 'recording.webm');
                formData.append('model', model);
    
                console.log('发送SiliconFlow请求:', { apiUrl, model, fileSize: audioBlob.size });
    
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Authorization': `Bearer ${apiKey}` },
                    body: formData
                });
    
                console.log('SiliconFlow响应状态:', response.status);
    
                if (!response.ok) {
                    let errorMessage = `API请求失败: ${response.status} ${response.statusText}`;
                    try {
                        const errorData = await response.json();
                        console.log('SiliconFlow错误响应:', errorData);
                        if (errorData.error && errorData.error.message) {
                            errorMessage += ` - ${errorData.error.message}`;
                        }
                    } catch (e) {
                        console.log('无法解析错误响应');
                    }
                    throw new Error(errorMessage);
                }
    
                const result = await response.json();
                console.log('SiliconFlow成功响应:', result);
                
                const transcription = result.text;
    
                if (!transcription) {
                    throw new Error('转换结果为空');
                }
    
                // 先重置界面
                resetResults();
                
                // 再保存和显示结果
                currentTranscription = transcription;
                currentSpeakerSegments = [];
                resultText.textContent = transcription;
                
                // 启用导出按钮
                exportBtn.style.display = 'inline-block';
                
                showStatus('✅ 转换完成!', 'success');
                console.log('SiliconFlow转换完成,结果长度:', transcription.length);
                console.log('保存的结果:', currentTranscription.substring(0, 100) + '...');
            }
    
            // 火山引擎提交任务
            async function submitVolcEngineTask(audioUrl) {
                const appKey = document.getElementById('volcAppKey').value.trim();
                const accessKey = document.getElementById('volcAccessKey').value.trim();
                const enableSpeakerDetection = document.getElementById('enableSpeakerDetection').checked;
                
                if (!appKey || !accessKey) {
                    throw new Error('请填入火山引擎的APP ID和Access Token');
                }
                
                const taskId = generateUUID();
                
                const requestBody = {
                    user: {
                        uid: "web-user-" + Date.now()
                    },
                    audio: {
                        format: "mp3",
                        url: audioUrl
                    },
                    request: {
                        model_name: "bigmodel",
                        enable_itn: true,
                        enable_punc: true,
                        enable_speaker_info: enableSpeakerDetection,
                        show_utterances: true
                    }
                };
                
                console.log('提交的请求参数:', JSON.stringify(requestBody, null, 2)); // 添加请求参数日志
                console.log('说话人分离是否启用:', enableSpeakerDetection); // 明确显示参数状态
                
                const response = await fetch('https://openspeech.bytedance.com/api/v3/auc/bigmodel/submit', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Api-App-Key': appKey,
                        'X-Api-Access-Key': accessKey,
                        'X-Api-Resource-Id': 'volc.bigasr.auc',
                        'X-Api-Request-Id': taskId,
                        'X-Api-Sequence': '-1'
                    },
                    body: JSON.stringify(requestBody)
                });
                
                console.log('提交任务响应状态:', response.status); // 添加响应状态日志
                console.log('提交任务响应头:', Object.fromEntries(response.headers.entries())); // 添加响应头日志
                
                if (!response.ok) {
                    const statusCode = response.headers.get('X-Api-Status-Code');
                    const message = response.headers.get('X-Api-Message');
                    throw new Error(`任务提交失败: ${statusCode} - ${message}`);
                }
                
                return taskId;
            }
    
            // 火山引擎查询结果
            async function queryVolcEngineResult(taskId) {
                const appKey = document.getElementById('volcAppKey').value.trim();
                const accessKey = document.getElementById('volcAccessKey').value.trim();
                
                console.log('查询任务ID:', taskId); // 添加任务ID日志
                
                const response = await fetch('https://openspeech.bytedance.com/api/v3/auc/bigmodel/query', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Api-App-Key': appKey,
                        'X-Api-Access-Key': accessKey,
                        'X-Api-Resource-Id': 'volc.bigasr.auc',
                        'X-Api-Request-Id': taskId,
                        'X-Api-Sequence': '-1'
                    },
                    body: '{}'
                });
                
                const statusCode = response.headers.get('X-Api-Status-Code');
                console.log('查询响应状态码:', statusCode); // 添加状态码日志
                console.log('查询响应头:', Object.fromEntries(response.headers.entries())); // 添加响应头日志
                
                if (statusCode === '20000000') {
                    // 成功
                    const result = await response.json();
                    console.log('查询成功,完整返回结果:', JSON.stringify(result, null, 2)); // 添加完整结果日志
                    return { status: 'completed', data: result };
                } else if (statusCode === '40000001') {
                    // 处理中
                    console.log('任务仍在处理中...'); // 添加处理中日志
                    return { status: 'processing' };
                } else {
                    // 失败
                    const message = response.headers.get('X-Api-Message');
                    console.error('查询失败:', statusCode, message); // 添加失败日志
                    throw new Error(`查询失败: ${statusCode} - ${message}`);
                }
            }
    
            // 火山引擎轮询结果
            async function pollVolcEngineResult(taskId, maxAttempts = 30) {
                for (let i = 0; i < maxAttempts; i++) {
                    try {
                        const result = await queryVolcEngineResult(taskId);
                        
                        if (result.status === 'completed') {
                            return result.data;
                        } else if (result.status === 'processing') {
                            showStatus(`🔄 处理中... (${i + 1}/${maxAttempts})`, 'info');
                            await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒
                            continue;
                        }
                    } catch (error) {
                        if (i === maxAttempts - 1) throw error;
                        await new Promise(resolve => setTimeout(resolve, 2000));
                    }
                }
                
                throw new Error('处理超时,请稍后重试');
            }
    
            // 火山引擎转换
            async function convertWithVolcEngine(audioBlob) {
                showStatus('🔄 准备进行语音识别...', 'info');
                
                // 获取音频URL
                let audioUrl;
                if (audioUrlMethod.value === 'direct') {
                    audioUrl = directAudioUrlInput.value.trim();
                    if (!audioUrl) {
                        throw new Error('请先输入音频文件URL');
                    }
                    if (!audioUrl.startsWith('http://') && !audioUrl.startsWith('https://')) {
                        throw new Error('音频URL必须以 http:// 或 https:// 开头');
                    }
                    showStatus(`📁 使用直接URL: ${audioUrl}`, 'info');
                    console.log('使用直接URL:', audioUrl);
                } else {
                    showStatus('🔄 正在上传音频文件...', 'info');
                    audioUrl = await createAudioUrl(audioBlob);
                    console.log('音频上传完成,URL:', audioUrl);
                }
                
                showStatus('🔄 正在提交识别任务...', 'info');
                
                // 提交任务
                const taskId = await submitVolcEngineTask(audioUrl);
                console.log('任务提交成功,ID:', taskId);
                
                showStatus('🔄 正在处理音频,请稍候...', 'info');
                
                // 轮询结果
                const volcResult = await pollVolcEngineResult(taskId);
                console.log('火山引擎处理完成:', volcResult);
                
                // 解析结果
                const transcription = volcResult.result.text;
                
                if (!transcription) {
                    throw new Error('转换结果为空');
                }
    
                // 先重置界面
                resetResults();
    
                // 保存基本结果
                currentTranscription = transcription;
                currentSpeakerSegments = [];
    
                // 显示基本结果
                resultText.textContent = transcription;
                
                // 启用导出按钮
                exportBtn.style.display = 'inline-block';
                
                // 检查是否启用了说话人分离且有结果
                const enableSpeakerDetection = document.getElementById('enableSpeakerDetection').checked;
                if (enableSpeakerDetection && volcResult.result.utterances && volcResult.result.utterances.length > 0) {
                    const speakerSegments = parseVolcEngineSpeakerResult(volcResult);
                    console.log('解析的说话人片段:', speakerSegments); // 添加调试日志
                    
                    if (speakerSegments.length > 0) {
                        // 保存说话人分离结果
                        currentSpeakerSegments = speakerSegments;
                        
                        // 生成带说话人标记的文本格式
                        let speakerText = '';
                        speakerSegments.forEach((segment) => {
                            speakerText += `[说话人 ${segment.speaker}] (${formatTime(segment.startTime)} - ${formatTime(segment.endTime)})\n`;
                            speakerText += `${segment.text}\n\n`;
                        });
                        
                        // 显示带说话人标记的文本
                        resultText.textContent = speakerText.trim();
                        
                        // 显示分析信息
                        if (volcResult.audio_info) {
                            audioAnalysis.style.display = 'block';
                            const urlMethod = audioUrlMethod.value === 'direct' ? '直接URL' : '免费托管服务';
                            const speakerCount = Math.max(...speakerSegments.map(s => s.speaker));
                            analysisInfo.innerHTML = `
                                <p><strong>音频时长:</strong> ${(volcResult.audio_info.duration / 1000).toFixed(2)} 秒</p>
                                <p><strong>检测到说话人数量:</strong> ${speakerCount} 人</p>
                                <p><strong>音频片段数量:</strong> ${speakerSegments.length} 个</p>
                                <p><strong>API提供商:</strong> 火山引擎 (专业级)</p>
                                <p><strong>音频获取方式:</strong> ${urlMethod}</p>
                            `;
                        }
                        
                        showStatus(`✅ 转换完成!已检测到 ${Math.max(...speakerSegments.map(s => s.speaker))} 个说话人`, 'success');
                    } else {
                        // 没有说话人分离结果,显示完整文本
                        resultText.textContent = transcription;
                        showStatus('✅ 转换完成!未检测到多个说话人', 'success');
                    }
                } else {
                    // 未启用说话人分离或没有utterances,显示完整文本
                    resultText.textContent = transcription;
                    showStatus('✅ 转换完成!', 'success');
                }
                
                console.log('火山引擎转换完成,结果长度:', transcription.length);
                console.log('保存的结果:', currentTranscription.substring(0, 100) + '...');
            }
    
            // 解析火山引擎说话人结果
            function parseVolcEngineSpeakerResult(volcResult) {
                console.log('完整的火山引擎返回结果:', volcResult); // 添加完整调试日志
                
                if (!volcResult.result || !volcResult.result.utterances) {
                    console.log('没有找到utterances数据');
                    return [];
                }
                
                const utterances = volcResult.result.utterances;
                console.log('utterances数据:', utterances); // 添加utterances调试日志
                console.log('utterances数组长度:', utterances.length);
                const segments = [];
                
                utterances.forEach((utterance, index) => {
                    console.log(`utterance ${index} 完整结构:`, JSON.stringify(utterance, null, 2)); // 添加详细的utterance结构日志
                    
                    // 尝试多种可能的说话人ID字段
                    let speakerId = 1; // 默认值
                    
                    // 按优先级尝试不同的字段名
                    if (utterance.additions && utterance.additions.speaker !== undefined) {
                        speakerId = parseInt(utterance.additions.speaker); // 火山引擎的说话人ID在additions.speaker中
                        console.log(`找到additions.speaker: ${speakerId}`);
                    } else if (utterance.speaker_id !== undefined) {
                        speakerId = utterance.speaker_id;
                        console.log(`找到speaker_id: ${speakerId}`);
                    } else if (utterance.spk_id !== undefined) {
                        speakerId = utterance.spk_id;
                        console.log(`找到spk_id: ${speakerId}`);
                    } else if (utterance.speaker !== undefined) {
                        speakerId = utterance.speaker;
                        console.log(`找到speaker: ${speakerId}`);
                    } else if (utterance.channel_id !== undefined) {
                        speakerId = utterance.channel_id;
                        console.log(`找到channel_id: ${speakerId}`);
                    } else if (utterance.spk !== undefined) {
                        speakerId = utterance.spk;
                        console.log(`找到spk: ${speakerId}`);
                    } else {
                        console.log(`utterance ${index} 没有找到说话人ID字段,使用默认值1`);
                    }
                    
                    segments.push({
                        speaker: speakerId,
                        startTime: utterance.start_time / 1000, // 转换为秒
                        endTime: utterance.end_time / 1000,
                        text: utterance.text
                    });
                    
                    console.log(`解析结果 ${index}: speaker=${speakerId}, text="${utterance.text.substring(0, 20)}..."`);
                });
                
                console.log('解析后的segments:', segments); // 添加解析结果调试日志
                
                // 统计说话人数量
                const speakers = [...new Set(segments.map(s => s.speaker))];
                console.log('检测到的说话人ID列表:', speakers);
                console.log('说话人数量:', speakers.length);
                
                return segments;
            }
    
            // 显示火山引擎说话人结果
            function displayVolcEngineSpeakerResults(segments) {
                // 这个函数已经不再需要,因为我们直接在resultText中显示带说话人标记的文本
                console.log('displayVolcEngineSpeakerResults函数已废弃');
            }
    
            // 格式化时间
            function formatTime(seconds) {
                const mins = Math.floor(seconds / 60);
                const secs = Math.floor(seconds % 60);
                return `${mins}:${secs.toString().padStart(2, '0')}`;
            }
    
            // 切换视图(已废弃)
            function toggleViewMode() {
                // 这个函数已经不再需要,因为我们移除了视图切换功能
                console.log('toggleViewMode函数已废弃');
            }
    
            // 显示错误信息
            function displayErrorInfo(provider, error) {
                if (provider === 'siliconflow') {
                    resultText.innerHTML = `转换失败。请检查:
    1. API密钥是否正确
    2. 网络连接是否正常
    3. 音频文件格式是否支持
    
    错误详情:${error.message}`;
                } else if (provider === 'volcengine') {
                    // 检查是否是音频下载失败的错误
                    if (error.message.includes('audio download failed') || error.message.includes('Invalid audio URI')) {
                        resultText.innerHTML = `
    <div style="color: #e74c3c; margin-bottom: 15px;">
    <strong>❌ 音频文件下载失败</strong>
    </div>
    
    <div style="background: #fff3cd; padding: 15px; border-radius: 8px; border-left: 4px solid #ffc107; margin-bottom: 15px;">
    <strong>🔍 问题原因:</strong><br>
    您提供的URL不是有效的音频文件直链。火山引擎API无法直接下载该文件。
    </div>
    
    <div style="background: #d1ecf1; padding: 15px; border-radius: 8px; border-left: 4px solid #bee5eb; margin-bottom: 15px;">
    <strong>✅ 解决方案:</strong><br>
    <br>
    <strong>1. 检查URL是否为直链:</strong><br>
    • 在浏览器中直接打开您的URL<br>
    • 如果直接开始下载/播放音频文件,则为有效直链<br>
    • 如果跳转到登录页面或分享页面,则为无效链接<br>
    <br>
    <strong>2. 获取有效直链的方法:</strong><br>
    • <strong>GitHub方式:</strong>上传到GitHub仓库,点击文件→Raw,复制链接<br>
    • <strong>自有服务器:</strong>上传到您的网站服务器<br>
    • <strong>专业CDN:</strong>使用阿里云OSS、腾讯云COS等服务<br>
    • <strong>免费托管:</strong>切换到"通过上传服务获取URL"模式<br>
    </div>
    
    <div style="background: #f8d7da; padding: 10px; border-radius: 8px; border-left: 4px solid #dc3545;">
    <strong>⚠️ 以下链接类型无效:</strong><br>
    • 百度网盘、OneDrive、GoogleDrive分享链接<br>
    • 需要登录验证的链接<br>
    • 重定向链接<br>
    </div>
    
    <strong>错误详情:</strong> ${error.message}`;
                    } else {
                        resultText.innerHTML = `转换失败。请检查:
    1. 火山引擎 APP ID 和 Access Token 是否正确
    2. 网络连接是否正常
    3. 音频文件URL是否为有效的直链地址
    
    错误详情:${error.message}`;
                    }
                }
            }
    
            // 验证URL是否为有效直链
            function validateUrl() {
                const url = directAudioUrlInput.value.trim();
                const resultDiv = urlValidationResult;
                resultDiv.style.display = 'none'; // 隐藏之前的结果
    
                if (!url) {
                    resultDiv.textContent = '请先输入URL';
                    resultDiv.style.display = 'block';
                    return;
                }
    
                if (url.startsWith('http://') || url.startsWith('https://')) {
                    resultDiv.textContent = '✅ 这是一个有效的直链URL!';
                    resultDiv.style.color = 'green';
                    resultDiv.style.fontWeight = 'bold';
                    resultDiv.style.display = 'block';
                } else {
                    resultDiv.textContent = '❌ 这不是一个有效的直链URL。请确保它以 http:// 或 https:// 开头。';
                    resultDiv.style.color = 'red';
                    resultDiv.style.fontWeight = 'bold';
                    resultDiv.style.display = 'block';
                }
            }
    
            // 处理文件上传
            audioFileInput.addEventListener('change', async (event) => {
                const file = event.target.files[0];
                if (file) {
                    console.log('选择的文件:', file.name, file.type, file.size);
                    
                    // 检查文件类型
                    if (!file.type.startsWith('audio/')) {
                        showStatus('❌ 请选择音频文件', 'error');
                        return;
                    }
    
                    // 检查文件大小(限制为50MB)
                    if (file.size > 50 * 1024 * 1024) {
                        showStatus('❌ 文件过大,请选择小于50MB的音频文件', 'error');
                        return;
                    }
    
                    currentAudioBlob = file;
                    
                    // 显示音频播放器
                    const audioUrl = URL.createObjectURL(file);
                    audioPlayer.src = audioUrl;
                    audioPlayer.style.display = 'block';
                    
                    // 显示开始转换按钮
                    updateConvertButtonState(); // 更新按钮状态
                }
            });
    
            // 事件监听器
            convertBtn.addEventListener('click', () => {
                console.log('点击转换按钮');
                convertToText();
            });
            
            exportBtn.addEventListener('click', () => {
                console.log('点击导出按钮');
                exportToTxt();
            });
            
            // toggleView.addEventListener('click', toggleViewMode); // 移除视图切换按钮监听器
            apiProvider.addEventListener('change', switchApiProvider);
            audioUrlMethod.addEventListener('change', switchAudioUrlMethod);
            directAudioUrlInput.addEventListener('input', updateConvertButtonState); // 添加URL输入监听器
            validateUrlBtn.addEventListener('click', validateUrl); // 添加URL验证按钮监听器
    
            // 页面加载完成后的初始化
            document.addEventListener('DOMContentLoaded', () => {
                console.log('页面加载完成');
                showStatus('🔧 请选择API服务商并配置相关信息。', 'info');
                
                // 初始化视图
                // toggleView.style.display = 'none'; // 移除视图切换按钮
                convertBtn.style.display = 'none';
                exportBtn.style.display = 'none';
                switchApiProvider(); // 初始化API配置显示
                
                // 为选择的输入框添加监听器以实时更新按钮状态
                document.getElementById('apiKey').addEventListener('input', updateConvertButtonState);
                document.getElementById('volcAppKey').addEventListener('input', updateConvertButtonState);
                document.getElementById('volcAccessKey').addEventListener('input', updateConvertButtonState);
            });
        </script>
    </body>
    </html>
  • 用这套多维表格模板,零门槛搭建属于自己的知识库

    实不相瞒,ChatGLM3时代我就尝试着用Langchain-Chatchat搞一个自己的知识库。

    但幻想很美好,现实很骨感。真的是……不太好用。

    纯文本内容相对还好,勉强还在能用的范畴内,但PPT之类的文档识别效果很差,都不知道识别成什么样子了。

    后来这个问题好像一直都没有太好的解法。

    前段时间飞书正式发布了「飞书知识问答」。

    甚至都不需要自己做embedding,0技术门槛,只要你有权限阅读的飞书文档,都可以进行AI知识问答。

    这又燃起了我搞个知识库的想法。

    并且还真让我找到一个不需要技术门槛的解决PPT识别问题的方案:

    利用多维表格+多模态视觉模型批量识图解析PPT再转换成文字版文档。

    仔细想想还挺合理的,PPT这种呈现形式本身就跟视觉效果强相关。

    它不像文章一样,从头到尾由连续的文字串成一个连贯的整体,反而是要求文字上尽可能做到高度概括,甚至单纯的文字内容之间需要有跳跃,再由排版等视觉设计加上读者的联想共同完成信息的表达。

    例如这样:

    就算完整提取出其中的文字,不存在任何的顺序错乱和乱码,仅凭文字还是不能理解它表达的意思。反而同时具备视觉和理解两项能力的多模态大模型来做这个,刚刚好。

    于是,就有了这个多维表格模板。

    先展示一下效果,再讲讲怎么用这个表格。

    这是原版PPT(既有表格又有文字):

    Image

    这是使用多维表格转换成的文字版文档(3×3表格行列关系精准识别):

    Image

    这是我在飞书知识问答里的提问:

    Image

    这份PPT文件经过表格的转换,无缝成为了飞书知识问答的知识库信息源。

    如果你不是飞书用户,没有飞书知识问答,也没有关系,我在表格中也做了基于知识库内容(塞入上下文)的问答功能:

    Image

    如果你单纯要做文档的整理和速览,它也是不错的选择,可以自动对文档进行总结并生成封面图片:

    Image

    下面我们来详细介绍一下这个模板的功能和用法:

    打开模板链接:

    https://gcnax5pj3z0y.feishu.cn/base/AM26bPSpnamXYesHukscryQJnhg?table=ldxLEVCkyDeBPNQe

    然后点击「使用该模板」按钮,就可以使用这个模板创建自己的知识库了。

    通过模板创建表格后:

    1.文件上传&留存

    在同名表格中,通过附件字段,将需要归档的所有文件(PDF、图片、PPT等)统一上传。 表格会自动提取文件名并进行选项化操作。

    Image

    你可以对上传资源进行分类管理。

    模板对「文件分类」、「一级标签」、「二级标签」三个选单的选项进行了统一管理,如需调整,可前往「配置和工具」下「配置表-选项索引」表进行调整。

    Image

    「三级标签」做了宽松处理,支持填写时手动创建选项。

    2.文件分类处理

    PPT(包括PDF格式的PPT)请这样操作:

    ①先将PPT的每一页都转换成图片提取出来。

    可以使用WPS等工具进行转换:

    Image

    如果没有现成工具也没有关系,可以前往「配置和工具」下「工具箱-PPT转图片工具」表格中,点击下图链接使用Web工具进行转换。

    Image

    这个工具是直接生成的,这就是我一直说的,AI时代很多做事儿的范式都可以尝试大胆改变了:

    Image

    用了纯前端HTML+JS加外部库的方式实现,绿色轻量,源代码和网页文件也在工具箱表格中。

    简单到只有几个按钮,用法应该也不用我多说了:

    Image

    Image

    ②前往「PPT内容识别」表格中,使用固定在右上角侧边栏的「批量上传附件」插件批量上传每一页截图。

    Image

    ③双击「内容识别」,为AI图片理解字段捷径配置豆包大模型账号。

    建议使用Doubao-1.5-vision-pro模型,如果你希望AI更加忠实于原文并减少延展,也可以使用Doubao-Seed-1.6。

    Image

    Image

    (可以参加火山方舟的协作奖励计划,每天领取50w tokens。)

    配置好后,豆包大模型开始理解每一张PPT页面并进行总结。

    经过PPT排版和美化后的分区块不连贯内容,也可以被豆包大模型理解并总结。

    ④手动录入图片字段左侧的「页码编号」字段和「资料名称」字段,支持像Excel一样拖动填充柄进行填充。

    页码录入会关联右侧公式字段,公式内带有Markdown标题标记,以便于后续创建飞书文档时自动识别页码标题。

    Image

    Image

    文本类PDF(非扫描)请这样操作:

    ①前往「PDF文档提取」表格中,在下拉列表中选择相应的资料名称。

    Image

    ②手动将自动匹配出的附件,复制到右侧PDF文档附件字段中。

    Image

    PDF文档的文字内容会被自动提取到「内容提取」字段。

    截图类请这样操作:

    动作基本跟文本类PDF相同。

    ①前往「图片OCR」表格中,在下拉列表中选择资料相应的资料名称。

    ②手动将自动匹配出的附件,复制到右侧PDF文档附件字段中。

    Image

    3.生成文档总结和飞书文档

    表格默认按照自增编号进行资料匹配,只要在「1.文件上传&留存」中上传过的文件,下左图红框内的信息都会自动出现,不需要手动操作。如果上传文件数量超过本表行数导致新传文档未显示出,只需要向下插入新行即可。

    Image

    Image

    如果按钮为灰色,需要进入自动化中心手动启用自动化流程。

    Image

    按钮生效后,对需要总结的文档点击按钮,则按钮右侧自动生成「资料内容」、「重点总结」、「飞书文档」三个字段的内容。

    Image

    生成飞书文档需要按提示进行扣子授权,授权通过后重新运行字段捷径即可。

    飞书文档生成后,就可以在飞书知识问答中作为参考信息被AI引用了。

    Image

    使用「查询页」,可以查询对应资料的重点信息。

    Image

    表格最右侧几个字段根据资料重点内容总结完成了封面图生成。生图需要消耗一定API费用,默认关闭自动生成,你可以自行选择是否开启。

    启用后,「知识图册」仪表盘的画册组件会带有AI生成的标题封面:

    Image

    画册展示效果是这样:

    Image

    4.豆包智能决策建议

    本功能可实现小范围参考本表格知识库内资料进行解决方案问答。

    可通过「参考1」、「参考2」、「参考3」选择最多3份资料进行参考,然后在「问题字段」输入问题。

    则豆包大模型会在「智能建议」字段返回问题的答案/建议。

    Image

    问答示例:

    问题1:我想招聘一个谷歌广告创意策划,帮我出5道题考考他。

    参考:《Google Ads Video Certification学习笔记》

    豆包回答:

    Image

    问题2:我们是一家抖音达人电商公司,想在2025年拓展一个AI驱动的像LABUBU一样的潮玩IP业务,有什么AI技术趋势是值得我们关注的吗?

    参考:《State of AI Report – 2024 ONLINE》、《2024达人电商全年报》、《LABUBU顶流现象洞察报告》

    豆包回答:

    Image

    此处豆包大模型选用了Doubao-Seed-1.6模型,直接通过自定义API捷径调用成本相对较低的Doubao-Seed-1.6-flash,表现也不错。

    5.其他知识剪藏

    这两个表格用于碎片化知识快速剪藏,内容不进入上述文档知识库中。

    建议配合飞书手机APP使用。

    Web链接剪藏

    适合快捷收藏公众号、小红书等社媒以及博客和网站的内容。

    粘贴链接后,可自动总结内容并根据内容生成一个更具概括性的资料名称(过滤自媒体标题党)。

    Image

    碎片截图剪藏

    适合快速存储和整理手机截图、群聊图片、朋友圈图片中的有价值信息。

    以上就是这张表格的全部功能,希望你能喜欢~

  • ComfyUI入门(二):如何打造自己的工作流,让AI生图/修图更高效?

    这是ComfyUI入门系列第二篇,如果你对ComfyUI还是完全零基础,建议先看上一篇:把工作流拆开揉碎了,带你入门ComfyUI

    本来上篇结尾是说,我们这次要来聊Flux Kontext图片编辑。

    但是,豆包最近新出了个「超能创意2.0」。我真的觉得它在图片编辑能力上,大部分场景都不输Kontext,甚至创意上尤有过之。如果是要拿来就用,快速处理70%的问题,说实话,我更建议大家去试试豆包。

    放张图演示一下豆包的效果:

    跟Kontext一样都是局部重绘,不会改变在prompt中要求固定不变部分,而且门槛更低,还免费。有了它之后,我个人觉得Flux Kontext这个模型本身,反而没那么重要了。

    所以,本文的内容,虽然我依旧准备写Kontext,但会侧重在通过Kontext工作流的搭建来演示怎么样把一个特定能力的模型用舒服,最好能在ComfyUI工作流设计思路上给大家一些启发。希望你对本文也能带有这样的预期,如果你需要一份关于Flux Kontext本身能力与使用方法的全面介绍和评测,可能就还需要再补充阅读一些其他文章了。

    其实不止是豆包,黑森林工作室也提供了可以聊天对话式使用Flux Kontext的Playground(https://playground.bfl.ai/)。

    写到这儿,我觉得我不得不先回答一个问题才能继续写下去了:为什么要用ComfyUI?直接在网页使用看起来也挺好,为什么还得学ComfyUI呢?

    这背后其实跟另一件近来被广泛提及的问题是一件事儿:

    ChatBot(聊天机器人)还是Agent(智能体)?

    放在今天的语境,也可以被写作:

    ChatBot(聊天机器人)还是WorkFlow(工作流)?

    ChatBot的使用门槛更低,但它能够完成的总是单次的操作。

    可我们在大多数真实的应用场景中要完成的,往往都是一套操作。而受限于过程中的交互需求、大模型的上下文窗口等等,AI在对话这种沟通形式中很难一次性完成一整套操作。

    这就造成了,如果我们要使用ChatBot完成复杂任务,必须经过繁琐的多轮对话,可控性和效率都不高。

    WorkFlow则解决了这个问题。

    它本身就是一套操作,并且其中可以结合大模型的能力进行智能化判断。

    举例来说,

    我经常会使用飞书多维表格的AI字段捷径功能,例如这样一个表格:

    不要认为只有用连线把节点连起来的才算工作流。

    实际上很多用多维表格搭建出的模版,本质上也是工作流。甚至你每天早晨到公司冲杯咖啡,打开电脑,整理昨天的事项和安排今天的工作,它也是工作流。

    上面的这个表格实现的功能是向不同背景的外国朋友针对性推荐在中国城市游玩的方案,任意的朋友背景和目标城市都可以随时切换。

    你可以想象一下,如果这些可变选项频繁变化且量足够大,在ChatBot中实现是有多复杂。

    我现在展开一下最后一个字段捷径的配置,prompt是这样写的:

    你觉得它像什么呢?

    把它放进ComfyUI中想象,朋友的家乡、年龄、性别、爱好等等信息节点,它们有的是初始节点(例如性别),有的是二级节点(有什么好玩的),都各自拉出一条连接线,连接到了最终生成推荐结果的这个节点的输入端点。

    多维表格和ComfyUI,只是两种不同的工作流展现形式。

    再看下面这个表格,这也是之前发过的,海报仿图模板:

    是不是很像上一篇ComfyUI文章中我们创建的图片反推再生成的工作流?

    图片

    其实本质逻辑是一样,但表格模板的工作流中,更多环节存在LLM介入,所以更加智能。

    这也是我想跟大家分享的让AI生图/改图工作流更高效的小技巧之一:

    适当引入LLM(大语言模型)。

    我们结合着Flux Kontext来说吧,毕竟本来是要讲Kontext。

    最新版的ComfyUI真的比从前方便太多。

    直接点击右上角「工作流」选择「浏览模板」,就可以一键导入ComfyUI官方内置的各种工作流模板。

    在这里,本地运行的dev版本工作流和调用线上API的版本工作流都有提供。分别在「Flux」和「图像API」选项卡下。

    API毕竟是付费服务,模型规模也更大一些。而且本地fp16的满血模型对我只有24G显存的4090来说还是有些吃力,我使用的是Comfy提供的fp8量化版本。所以,在一些细节效果上,难免API要比本地运行表现好。

    但是, 本地模型它好在不吃网络也不花钱。

    在鱼与熊掌各有各好的情况下,我一般的处理方案就是全都要。

    所以,我搭建自己的工作流的第一步,就是把他们拼装起来,我想用哪个用哪个。

    在ComfyUI中拼装工作流的操作也非常简单。

    只需要按住ctrl,选中要复制的节点,ctrl+c复制,再粘贴进目标工作流窗口中即可。

    就这样,我就把API工作流粘贴进了本地工作流的窗口中。

    我们来依次拆解一下这两个工作流。

    首先是相对复杂的本地工作流。

    不用看默认Step123的编组,我们先按我画的三个颜色的选框来看。

    先看左下角蓝色框内的部分,一共四个节点:

    先是两个加载图像(来自输出)。当前其中一个是忽略状态,代表只有一张图片被加载。

    然后Image Stitch这个节点的功能是把两张图片拼在一起,因为Flux Kontext虽然可以做多图的参考,但其实只能接收单张图片的输入,所以要提前把多张图片拼起来,这个我稍后演示一下;如果像现在图示状态一样,只有一张图片上传,则这个拼图节点输出的还是一张图。

    最后FluxKontextImageScale这个节点,是对图片进行了缩放处理,可以看到原图尺寸是22082944,经过它处理以后的预览图像尺寸变成了8801184,这个尺寸比较适合本地模型处理。

    这个加载图像(来自输出)不太方便,只能选择之前生成的图片作为输入,需要稍微改造一下。我们把它们换成两个普通的「加载图像」,这样就可以任意选择图片上传了。

    右键点击第二个图片上传节点,在弹出菜单中点击「(取消)忽略节点」,就能够启用它。

    我们上传一个红色的帽子,这时候在预览界面就能看到Image Stitch节点的工作效果了,它把这两张图拼接成了一张。direction参数是right,所以image2(帽子)被拼接在了人物右边。

    看完了上传图片的蓝框部分,再来看红框和黄框。

    这两个框选加起来的工作流你应该很熟悉了,就是上一篇写到的本地Flux生图工作流。只是加载的生图模型换成了Kontext模型,并且把正向提示词单独拿了出来。

    切到API生图的工作流,这个工作流更加简单。

    左边部分是上传原图,右边部分是生成新图。

    这个工作流的Kontext API是由Comfy中转提供,速度比直连黑森林工作室的API要快,充值和消费都是通过Comfy来进行,直接点开右上角头像就能找到充值入口。

    但是Comfy的Kontext节点不提供可控内容审查尺度功能,所以有时候一些抽象或者NSFW的图片修改请求会被直接拒绝。

    所以一般常用的Kontext API还有另一个版本,也就是黑森林官方的版本,是支持使用手动设定审查尺度的节点的。我们把它也一块加进这个all in one工作流中。

    需要安装一个「Flux Kontext Creator for ComfyUI」节点,具体操作方法可看上一篇。

    然后按照GitHub上README文件的配置说明配置好config.ini文件和密钥。具体操作我就不演示了。

    https://github.com/ShmuelRonen/FluxKontextCreator

    至于获取API密钥和进行充值都需要进入到我们前面提到过的黑森林官方网站(https://dashboard.bfl.ai/),默认注册会送200个积分免费体验。

    Flux Kontext Creator for ComfyUI节点的使用方法跟Comfy的KontextAPI节点基本相同,接法也一样,只是多出了手动选择pro和max节点和手动设置审查强度的参数。

    右键「添加组」,可以像上图一样用不同颜色的背景框把实现同功能的节点整理到一起。

    现在我们是要把本地、Comfy API、BFL API这三条生图路径放进一个工作流中,原图加载和prompt模块可以让三者共用。

    于是我们可以上传图像的输出端点跟全部三者的输入图像端点相连;然后添加一个输入字符串节点,分别接入到三者的提示词文本框,这样只需要在输入字符串节点填写prompt,三条子工作流都能使用。

    可以在空白处添加一个「忽略多框」节点。

    通过这个节点可以快捷控制功能框的启用状态。

    于是,这样一个简单的all in one工作流就完成了。

    接下来让我们进一步细化它,让它更「好用」一些。

    首先是提示词。

    ComfyUI内置的工作流模板中其实有贴心地提供一份提示词技巧注释在旁边供使用者学习参考。

    但学习成本嘛,还是稍高。并且Kontext只支持英文,让英文不好的同学压力很大。

    那要怎么做呢?

    对这种问题,通常我的解法就是:接入LLM。

    多模态生成模型的结果所见即所得,更容易被理解,也更好落地,但要说真正的智能,还得是大语言模型。LLM的训练和推理成本都更高,不是没有道理的。使用大模型越多,越能感受这一点。

    我们可以直接搭建一个「提示词生成器」。

    把前面的「提示词技巧」作为系统prompt输入给LLM:

    你是一个「Flux Kontext提示词生成大师」,擅长根据用户描述的需求,生成符合Flux Kontext Prompt Techniques的提示词。
    

    然后,只需要上传图片,使用最没有技巧的描述把要表达的需求说清楚就好。

    例如,我输入需求「给她换一条蓝色的裙子」。

    不用考虑什么变化什么不变,措辞应该是she还是the women,LLM会按规范帮你处理好一切。

    整个工作流执行完毕,她就被换上了一条蓝裙子。

    再进一步,如果原图比较复杂,单纯的需求描述无法让LLM理解应该变化什么保留什么以及如何措辞。

    我们也可以把上一篇提到的反推模块加进来,使用「合并字符串」节点跟原prompt合并到一起,把图片中有什么也告诉LLM,然后让它更精准地生成提示词。

    再举例来说,API节点有一个不太好的地方,只能选择固定几个图片比例进行生成。

    但是我们上传的原图,有可能是横图,也有可能是竖图,每次调整就很不方便。

    怎样做成自动调节图片比例呢?也可以使用LLM来完成。

    首先我们可以使用图像尺寸节点读取图像的尺寸,然后输出宽比高,例如上图的输出即是2208:2994。

    然后可以直接使用LLM判断:

    请判断我给你的图片比例更接近下面哪一比例,然后直接输出(且只输出)这个比例
    

    输出最接近的尺寸比例。

    然后把这个比例接入到API节点的比例参数即可。

    注意,参数类型一致才能相连,这里有一个小技巧确认参数类型:

    从对应参数的节点拉出一条连线新建节点,这时候就可以看到参数类型的提示。例如下图,这个比例的参数类型是:Combo(组合框)。

    所以,我们要在刚才的字符串和比例参数中间添加一个格式转换节点:字符串到Combo。

    这样,就能实现自动比例变换了。

    我们找一个新case来跑一遍。

    比如说我想让下面这个房间的家具和装修都不变,改得更有少女感一些。

    现在我只需要输入图片处理需求:

    保持原有装修和家具,把这个房间布置成可爱女孩子的房间。
    

    LLM自动帮我丰富了女孩子房间的物品内容,并要求保留现有的装修和家具:

    然后,又自动识别出原图的比例更接近3:4的竖图,让API生图自动按照3:4的比例输出。

    这是最终本地模型+API生成的两张图片:

    举一反三,图像尺寸获取,我们也可以用在刚才的多图参考上。

    本地Kontext模型的输入输出图片是同比例。而多图的处理方式是先合并成一张图再输出,所以输出的图片比例就是拼接后的图片比例。

    如果两张输入图都带背景,其实一般会进行融合,但像上面案例这样其中一个舞台是白底图,那大概率结果就是这样,依旧带着一张白底图输出:

    为了便于直出,我们就可以在这里做一个多图裁剪的功能块。

    同样是利用图像尺寸节点,甚至都不需要介入LLM,直接根据原图和生成图片的分辨率,使用四则运算表达式计算出裁剪位置,进行裁剪。

    效果即如下图:

    已经5000字了,篇幅有限,能在文章中展开的也只是冰山一角。

    不过如果你是从上一篇看到这里的ComfyUI新手,相信你对Flux Kontext和ComfyUI工作流的搭建应该也有了一些思路。

    后续根据实际的使用需求,还可以在工作流加入图片放大、人物面部优化等等不同功能的节点和模块。甚至还可以通过SD PPP插件,直接把ComfyUI工作流接入到Photoshop,合并入你日常工作中的图片处理工作流。

    这些就留给你自己去探索了。

  • 把工作流拆开揉碎了,带你入门ComfyUI

    出差了一周,回来再写Flux.1 Kontext[dev]总感觉有点晚,那不如直接聊聊ComfyUI好了。

    这篇写ComfyUI,下篇还能再写Kontext。

    嘿嘿。

    当我跟大多数朋友提到ComfyUI,得到的反馈最多的就是「难」,然后就是「我显卡不行」。因为这些理由,他们就选择把了解都没了解过的ComfyUI直接拒之门外。

    但是,有没有一种可能,ComfyUI并没有他们想象的那么难?有没有一种可能,不需要高端显卡也能使用ComfyUI?

    世界上最高的门槛,往往是「你觉得它有门槛」,世界上最难的事,往往叫「你觉得它难」。

    不管你怎么觉得,反正我觉得,等你读完本文,应该就能够入门ComfyUI,并且能够在ComfyUI环境下自己搭建一个图片生成工作流。

    别着急,一步步跟着我来。

    首先,到下面这个视频的评论区下载秋叶ComfyUI整合包:

    https://www.bilibili.com/video/BV1Ew411776J/

    解压并运行绘世启动器,在版本管理中把ComfyUI升级到最新版本。

    没有任何命令行和代码,全程只需要进行可视化的点击操作。更新完成后一键启动,系统将自动在浏览器中打开ComfyUI的可视化节点工作流界面。

    (如果因为网络环境问题无法完成升级,这里就不便教导了,建议自行补习一下冲浪技能XD)

    默认加载的工作流如上图,是一个经典的加载SD模型画瓶子的工作流,运行的结果就像这样:

    但我们今天不按照正常的学习AI绘画的路径来学习,上面这个工作流是什么意思,你现在完全不需要懂。什么Checkpoint、正负向提示词、K采样器……我们现在都先不管它。

    不过既然到这儿了,这里有一个对ComfyUI的典型认知误区,我还是稍微展开一下。

    很多人,尤其是一些营销号,喜欢把SD(Stable Diffusion)和ComfyUI并列,典型的常用句式如「应该学SD还是学ComfyUI」。如果你觉得这句话没问题,那我问你,上面那个工作流,我刚刚在ComfyUI里调用SD1.5的模型(麦橘写实v7)画了个瓶子,这算什么呢?

    ComfyUI和Stable Diffusion并非一个性质的东西。

    你可以这样粗暴地理解,在当初还没有黑森林工作室,没有FLUX.1的时候,ComfyUI就是一套基于节点工作流的Stable Diffusion的GUI(图形用户界面)。而当时与之相对的,是Stable Diffusion的另一套GUI,叫做AUTOMATIC1111(也被简称为A1111)WebUI,这也就是现在营销号口中所说的SD(现在除了A1111,也还有其他的WebUI分支,比如ControlNet作者敏神的Forge)。

    后来有了FLUX.1,不论是ComfyUI还是WebUI,也都支持了FLUX.1模型的调用。

    所以,你现在应该理解,ComfyUI并非像SD或者FLUX.1一样的生图模型,它是一套图形用户界面。你可以通过ComfyUI使用Stable Diffusion,也可以通过ComfyUI使用FLUX.1[dev],也可以通过ComfyUI使用FLUX.1Kontext,也可以通过ComfyUI调用生图API……甚至,只要有合适的节点,你也可以在ComfyUI中使用ChatGPT。

    现在,我们就先来安装一个在ComfyUI中使用LLM的节点。

    打开节点管理器:

    搜索LLM_party,安装这个扩展。

    安装好后如图所示:

    按要求重启,又经过一系列的安装:

    再次回到浏览器的节点工作流界面,我们把那个画瓶子的工作流完全删掉,在左侧节点库添加进一个「llm_party」的「API LLM通用链路」节点和一个「API LLM加载器」节点:

    看到「API LLM加载器」需要填写的这些项目,是不是回到舒适区了?之前做过LLM API调用的朋友应该都很熟悉,就是分别填写模型名称、接口地址和API KEY:

    (如果不熟悉,参考这篇文章:DeepSeek服务器总繁忙怎么办?不愿稍后,不如试试通过API续命你的聊天儿

    例如,我们调用硅基流动的DeepSeek-V3,就是填成这样:

    如果你用过Coze,应该能理解节点工作流的执行逻辑,就是顺着这个工作流的节点,先执行A,再执行B,然后执行C。

    ComfyUI的执行逻辑跟Coze几乎完全一样,只是把Coze中每个节点的输入输出参数,以更形象直观的可连接端点的形式体现了出来。

    只要上一节点右侧和下一节点左侧有端点相同(颜色相同),那么它们就可以相连。例如下面两个节点:

    「API LLM加载器」右侧的model和「API LLM通用链路」左侧的model对应相连,意思就是通过「API LLM加载器」加载上硅基流动的DeepSeek-V3的模型,把它加载进了「API LLM通用链路」节点当中。

    然后,对于「API LLM通用链路」节点,我们还缺prompt的输入,还有大模型执行结果的输出。

    对于prompt,我们可以在「API LLM通用链路」节点之前,加入两个字符串输入节点;而对于结果输出,我们则可以直接连接一个「显示文本」节点,用于展示assistant response的输出结果。

    像这样连接:

    我们在连接system prompt input(系统提示词输入)的「输入字符串」中填写:

    你是一个诗人,不管别人给你什么关键词,你总能根据它作一首诗。
    

    在连接user prompt input(用户提示词输入)的「输入字符串」中填写:

    小猫
    

    点击运行。

    一首关于「小猫」的诗,就出现在了右侧的「显示文本」节点中。

    我们再来复盘一下上面执行的这个简短的工作流:

    1.通过「API LLM加载器」节点,加载了来自硅基流动的DeepSeek-V3模型。

    2.通过「输入字符串」节点,设定了系统提示词:你是一个诗人,不管别人给你什么关键词,你总能根据它作一首诗。

    3.还是通过「输入字符串」节点,输入了用户提示词:小猫。

    4.「API LLM通用链路」节点接收到了上面1、2、3的输入,并执行。

    5.通过一个「显示文本」节点,将「API LLM通用链路」的执行结果——一首AI生成的小诗《猫的哲学》——显示了出来。

    这就是ComfyUI的基本运作逻辑。

    理解了基本运作逻辑,我们继续对这个生成小诗的工作流进行微调,让它更加贴近我们的实用场景。

    观察「API LLM通用链路」节点的端点,可以发现,除了文字prompt输入外,我们还可以进行图片输入。

    正好,我们就可以做一个图片反推的功能。

    硅基流动刚刚上线了智谱新发布的视觉模型,我们就用这个:

    直接在API LLM加载器中修改掉模型名称。

    为了上传图片,我们还需要添加一个「加载图像」节点:

    用户提示词可以去掉,系统提示词改成:

    请分析图片:
    

    然后,执行。

    我们就得到了GLM-4.1V-9B-Thinking模型对这张图片的反推:

    不难吧。

    短短几分钟之内,我们已经基于「 API LLM通用链路 」这个节点开发了两个小型工作流了。

    这时候就要提到新手容易出现的另一个误区了:「像它那样复杂的工作流我搭不来。」

    学会了搭建这种几个节点的小型工作流以后,一定会有人说:「虽然你说的这个我学会了,但我只会这样简单的工作流又有什么用呢?」

    但是,朋友们。

    你需要知道的真相是:当然复杂工作流的搭建跟技术能力有关系,但那可能只占20%,另外80%则是跟需求相关。

    例如说,我现在已经有了一张图片,但我还希望通过AI生成一批跟它类似的图片。

    我们拆解一下这个任务要怎么完成:

    1.首先我要能描述现在有的图片。

    2.然后才能编写生成类似图片的提示词。

    3.最后要把提示词输入给可以生成图片的大模型来生成新图片。

    这其中可能还会涉及到一系列语言的翻译和转换问题。

    不难发现,第1条,这个任务的起点,就是刚才的反推图片工作流。

    执行刚才的反推工作流之后,得到的就是图片的描述。

    然后我们要执行第2步,编写提示词。

    这一步可以通过AI来完成,我们直接在前面的生成结果后面继续接一个「API LLM通用链路」节点,来生成生图的提示词。

    加载一个大语言模型,比如Qwen/Qwen3-235B-A22B。

    系统提示词设置如下:

    请帮我根据图片描述生成一段用于文生图的纯英文prompt。你的输出结果将直接用于图片生成,请直接输出生成的prompt内容,不要带有任何解释性文字
    

    图片分析结果直接作为用户提示词。

    直接生成一段英文prompt,使用「显示文本」将它展示出来。

    为了便于查看,在「显示文本」的后面又可以接一个谷歌翻译的节点,将英文翻译成中文,专门用于查看。

    现在的工作流就变成了这个样子:

    接下来就到了第3步,生成图片。

    最新版的ComfyUI内置了FLUX.1的API生图节点(付费)。我们直接在空白的地方双击,在搜索框中输入”FLUX”,找到「Flux 1.1 [pro] Ultra Image」,单击即可添加。

    生图节点左侧的prompt端点,向前连接上刚才Qwen生成的生图提示词,右侧的image端点,向后接上一个「保存图像」节点用于图片保存。

    整个仿图的工作流就这样完成了。

    虽然其实算不上复杂,但这个工作流要比前面三级节点组成的小型工作流完备多了。

    你说它搭建起来更难了吗?并没有。

    需求足够明确,为了实现这个需求,需要用什么节点,就添加什么节点,工作流自然就会丰富起来。

    而这,也就是复杂工作流的基本搭建逻辑。

    你看,到现在为止,我们没有调用过本地显卡。

    并且搭建了一套完整的图片反推再生图的工作流。

    能够做到这一步,你已经可以说是入门ComfyUI了。

    现在,我们再回来看刚才的SD本地模型画瓶子工作流,应该也很容易理解了:

    无非就是把颜色相同的端点相连,加载模型,设定正负面提示词,设置图片尺寸,采样、解码,生成图片。

    你可能又想说,SD过时了,我要用FLUX。

    当然可以了:

    注意跟上面的SD工作流对比,你会发现Flux.1[dev]的工作流只有两点差别:

    1.FLUX不需要反向提示词,留空即可

    2.正向提示词后面增加引导节点

    当你这样拆解开来看,你会发现所有的变化其实都只是微微调整。

    有的FLUX.1模型并没有被打包成一键加载的checkpoint,这时候需要把UNet / CLIP / VAE 分别加载,注意对比下面的工作流和上面的工作流:

    只有加载本地模型的部分发生了变化,其他地方都不需要改变。

    也就是把简易版的checkpoint加载器,分别拆分成了UNet / CLIP / VAE 加载器(甚至我在模型后面又加了一个LoRA节点)。

    现在,我们再来搞一下排列组合。

    用上面的Flux.1[dev]本地生图工作流,替换掉再之前的仿图工作流中的API节点,也就是这个节点:

    于是我们就有了一个基于本地Flux.1[dev]模型的仿图工作流。

    用新的方案替换掉原工作流中实现相同功能的节点(组),是我们最常见的工作流优化动作。

    那,怎么获得新的方案呢?

    除了自己冥思苦想以外,更有效的途径是多学习(玩)别人的工作流。

    很多AI绘图爱好者社区,比如国外的civitai,国内的liblib、tusiart等等,都有无数大神上传了各种用途的优秀工作流。

    你获得到的别人的工作流常见有两种形式,一种直接就是一个json文件,另一种是通过该工作流生成的图片,其中带有json文件一样的信息。这两种形式无论是哪种,使用方式都是直接把文件拖到自己的ComfyUI界面中即可,如下图:

    松开鼠标,工作流就被加载进你的ComfyUI了。

    当然,因为这个工作流是我自己做的,所以我加载起来十分丝滑。

    但如果你是刚刚才安装ComfyUI的话,大概率会满屏红框框,这就代表红框中的节点并没有在本机安装。

    这时候不要慌,点击右上角的「管理器」(更新到最新版它就是在右上角,旧版会在右侧),然后在管理器中点击「安装缺失节点」,对缺失的节点进行安装就可以了。

    工作流我放在这里,供你练习:

    https://pan.baidu.com/s/1WGNnJKhh5tKYxk9ld2b7CQ?pwd=ebj2

    如果你下载的其他工作流作者都像我一样有良好的习惯,把每一部分的功能标注得清清楚楚,那就更方便了。

    在工作流中删除不需要的部分,只保留你想要的模块,然后再安装缺失节点即可。

    例如,我们只保留这个本地反推:

    它就刚刚好可以替换掉我们在上面做的那个从硅基流动调用智谱视觉模型的反推模块,原先的工作流就变成了下面这样。对我的显卡来说,本地反推工作流的运行速度要比等待硅基流动的API反馈快上不少:

    你看,我们从刚开始的调用DeepSeek写诗,到现在都已经可以搭建出来带有三个功能版块的仿图工作流了。

    ComfyUI并没有多难,只要理解它的运作逻辑,入门还是很轻松的。

    好了,今天就到这里,希望你已经成功入门了ComfyUI。

    下一篇我们继续说在ComfyUI中使用Flux Kontext进行图片编辑。

  • 时隔一个月,聊聊昆仑万维的天工超级智能体

    直接说结论吧:

    在今天(2025.6.26)这个时间节点,只让我留一个工作用的Agent,我会选天工。

    最近一个月间也出来了几个新的Agent产品,但在办公领域,我认为天工成功捍卫住了自己的定位。

    我们按办公三件套(PPT、WORD、EXCEL)三个版块分别来聊。

    PPT

    老读者应该记得,我对之前市面上的AI一键生成PPT产品颇有微词,甚至为此手搓过一个基于多维表格的Workflow:

    自己肝了一个AI写PPT的小工具,它比AI自动套模板更适合白领职场人

    PPT这个东西,首重说服逻辑,其次才是美化。

    但大多AIPPT产品有一些本末倒置,专心搞美化,说服逻辑反而直接丢掉。好一点的,让用户自己提供WORD文档,恶劣些的,甚至让大语言模型在丧失约束的情况下随机生成。

    最后只有大主题是吻合的,具体内容全部失控,套个模板直接就出活儿了。

    我前面做这套表格,核心解决的就一件事儿,通过反复输入约束,增强对PPT内容的控制力。

    然而,它的问题在于,缺失了最后一公里。

    它无法直接生成PPT。

    天工智能体,恰恰帮我解决了这个问题。

    天工优秀的指令跟随,支持这样的Prompt:

    “` 请严格基于下面每一页(Page)的大纲和内容,生成一份 [页面数量] 页的 PPT。注意 PPT 分页标题要与文件中的保持一致。 “`

    直接把调好的大纲粘贴在上面这句Prompt下面,天工真的会按照你给到的信息生成PPT。

    Image

    一眼就能看出来,这样生成的PPT,逻辑远比优先关注美化的AIPPT工具好(因为大纲层面就有人类介入调整),内容细节也远比优先关注美化的AIPPT工具丰富。

    我截取一些生成的PPT页面放在下面,你们可以感受一下:

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    Image

    顺带一提,就算你要偷懒,不自己参与大纲的打磨,只抛出一个主题,完全交给天工Agent来执行,它调研和梳理的内容丰富度也十分不错。

    PPT生成后,它甚至支持直接导出成PDF和PPTX文件,便于发布和二次编辑。

    Image

    当然它还是不够完美,HTML网页代码转PPT方案的常见问题虽然得到一定控制,但它们依旧会存在:例如很难把所有页面的比例严格控制在16:9,例如导出的PPTX文件并不像原生PPT文件一样那么好编辑。

    但是,这种形态更接近我理想的AIPPT的样子。

    WORD

    身边的朋友新开通ChatGPT或者Gemini会员的时候,我有个必安利的功能,叫DeepResearch。

    尤其是ChatGPT的DeepResearch,在执行搜索任务的同时还保有强大的模型推理能力,使得它可以一边调查一边研究,最终输出的不只是搜索结果,更可以是一份落地方案。

    例如,使用这样的Prompt进行DeepResearch:

    “` # 商业模式分析SOP – 利益相关者交易结构分析框架## 简介//作者:LQ//版本:0.1//适用:OpenAI DeepResearch或其他类似的Research工具及Agent## 角色设定你是一位顶级的商业分析专家,深度精通以"利益相关者交易结构"为核心的商业模式理论。## 核心任务你将对以下指定的企业进行一次全面、深入的商业模式调研与分析,并严格遵循SOP的四个阶段执行。你需要生成一份结构清晰、逻辑严谨的深度分析报告。## 分析目标企业[请在此处插入公司名称]## 执行要求1. 严格遵循顺序:必须按照从【第一阶段】到【第四阶段】的顺序,依次执行所有步骤,不得跳过或颠倒任何步骤。2. 完整性:报告必须包含所有阶段和步骤的分析内容。3. 深度与证据:你的分析不应停留在表面描述,需要深入探究其背后的商业逻辑,并尽可能引用公开数据、财报信息、权威媒体报道或案例作为论证依据。4. 输出格式:最终交付物为一份完整的Markdown格式报告,使用清晰的标题和子标题来组织结构,对应SOP的各个阶段和步骤。## 第一阶段:宏观准备与范畴界定### 任务1.1:识别核心实体与商业生态系统#### 指令:- 将分析目标定义为"焦点企业"。- 识别并列出其所处的商业生态系统中的关键利益相关者类别,至少应包括:上游供应商(原料、技术等)、下游渠道商/合作伙伴、直接客户/用户、竞争对手、以及其他重要参与者(如资本方、监管机构等)。### 任务1.2:搜集并总结基础信息#### 指令:- 搜集并整理该企业的核心背景资料:成立时间、创始人背景、主营业务、核心产品/服务矩阵、公开的营收/估值数据、关键发展里程碑。- 将这些信息总结为一段简明扼要的"企业概览"。### 任务1.3:定义核心问题与初步价值主张#### 指令:- 基于背景资料,分析该企业所在的行业存在什么普遍的"痛点"或"真问题"。- 明确该公司旨在为目标客户解决的核心问题是什么。- 用一句话初步概括其核心的价值主张。## 第二阶段:魏朱六要素深度剖析### 核心指令绘制或用文字详细描述该公司的业务系统图,并在分析过程中,系统性地拆解其商业模式的六大要素。### 任务2.1:定位 (Positioning)#### 指令:详细分析企业的商业模式定位。- 满足方式:明确企业提供的是产品、服务、解决方案,还是一个"赚钱工具"?- 产权切割:分析交易的核心标的物(如产品、服务)的所有权、使用权、收益权等是如何在利益相关者之间进行分配的。- 交易过程:描述客户从"搜寻信息"到"完成交易"再到"售后服务"的全过程中,企业是如何组织这些环节的(线上/线下/O2O等)。### 任务2.2:业务系统 (Business System)#### 指令:详细拆解企业的业务系统。- 角色 (Roles):精确列出所有内外部利益相关者,并详细描述每个角色在价值链中承担的具体业务活动是什么。- 构型 (Configuration):使用文本或流程图形式,清晰展示这些利益相关者是如何相互连接和作用的。- 关系 (Relationships):明确利益相关者之间的交易关系(例如:是采销、委托代工(OEM)、品牌授权、股权投资,还是战略合作?)。### 任务2.3:盈利模式 (Profit Model)#### 指令:深入剖析企业的盈利模式。- 收支来源:收入主要来自哪些利益相关者?成本主要支付给谁?列出其主要的收入流和成本项。- 收支方式:收入的性质是固定、分成,还是剩余?定价方式是进场费(会员费)、过路费(按次/点击)、停车费(按时)、油费(按价值/用量),还是分享费(价值增值分成)?是否存在交叉补贴、两部计价(如"剃须刀-刀片"模式)等复杂定价策略?### 任务2.4:关键资源能力 (Key Resources & Capabilities)#### 指令:识别并分析支撑其商业模式运转的核心竞争力。- 列出3-5项最重要的关键资源(如品牌、数据、专利、渠道网络)和关键能力(如供应链整合能力、算法推荐能力、生态运营能力)。- 分析这些资源和能力是如何获取的(内生积累或外部获取)。- 评估这些能力是否构成了与商业模式相匹配的"有效优势"。### 任务2.5:现金流结构 (Cash Flow Structure)#### 指令:分析企业的现金流特征。- 评估其模式是重资产还是轻资产运营。- 分析其现金周转周期特点(例如,是预收款模式还是有较长的账期?)。- 描述其主要的融资方式和现金流健康状况。### 任务2.6:企业价值 (Enterprise Value)#### 指令:评估该商业模式的最终价值。- 分析其核心的价值增量在何处(即相比传统模式,它为整个生态系统额外创造了什么价值)。- 列出衡量其成功的关键绩效指标(KPIs)。- 阐述其护城河(竞争壁垒)的来源和可持续性。## 第三阶段:动态与竞争分析### 任务3.1:追溯模式演化路径#### 指令:如果可能,请梳理该公司商业模式的演化历史(例如,从1.0到2.0、3.0的迭代)。分析每次演化的驱动因素和带来的变化。### 任务3.2:进行商业模式竞争分析#### 指令:- 选取1-2个主要的竞争对手。- 对比分析该公司与竞争对手在商业模式上的异同点、各自的优劣势。### 任务3.3:应用"DARE视角模型"进行高级分析#### 指令:基于以下定义的"DARE视角模型",分析并归纳该企业的核心创新路径属于哪一类或哪几类的组合。- 设界 (Define): 调整企业自身的业务活动边界,决定在生态系统中扮演哪些角色,不扮演哪些角色。- 补缺 (Add): 在现有生态系统中,针对"痛点"或机会点,增加一个全新的业务活动角色,以提升整个生态系统的效率。- 重构 (Restructure): 基于新的认知或新技术,颠覆性地改变生态系统原有的价值创造逻辑、成本结构或盈利来源。- 觅新 (Explore): 跨出企业当前所处的生态系统边界,通过跨界、异业合作等方式,探索全新的增长机会和价值空间。## 第四阶段:综合评估与洞察提炼### 任务4.1:系统性整合与内核总结#### 指令:总结六大要素是如何形成一个逻辑自洽、相互增强的有机系统的。用一句话提炼出该商业模式的"内核"或"游戏规则"。### 任务4.2:评估模式的稳健性与风险#### 指令:综合分析该商业模式最大的优势和最核心的风险/脆弱点。### 任务4.3:提炼核心洞察与未来预判#### 指令:基于以上所有分析,提出你对该企业商业模式的最终洞察,并对其未来的发展趋势和潜在的演化方向做出预判。 “`

    只需要在[]中填写目标公司名称,其他一切交给AI执行,然后就可以得到一份十几页的超详细报告,像这样:

    Image

    Image

    篇幅原因,就不放出全文了,如果你想看完整版,可以直接查看这个链接:

    https://fh5k77901t.feishu.cn/docx/Pag1dBBhtoF5SqxwZtncndRgnFc

    我特别喜欢使用这样分步执行的长Prompt压力DeepResearch,以至于发现,哪怕是OpenAI的DeepResearch,在面对执行步骤分解过多过细的要求时,它也会撂挑子不干。

    比如说用这个Prompt的时候:

    “` # 基于超级符号理论的产品营销策划Prompt## 简介//作者:LQ//版本:0.1//适用:AI Agent,建议使用Skywork## 初始输入 (Initial Input)### 前提条件 (Prerequisites)在开始执行前,请一次性提供以下信息。[品牌名称]:[产品全名]:## 第零阶段:AI核心指令### AI初始化系统指令 (System Prompt)指令: 你现在是一名遵循"华与华方法"的顶尖营销战略AI。你已接收到初始输入的[品牌名称]和[产品全名]。在接下来的所有任务中,你必须严格遵守以下五大核心原则,并将它们作为你所有分析、判断和内容生成的根本逻辑:- 【成本原则】:你的一切输出,都必须以"如何用创意降低产品的营销传播成本"为首要目标。- 【一体化原则】:你必须将产品营销的所有环节(调研、购买理由、符号、包装、广告等)视为一个不可分割的整体进行思考和设计,确保能量聚焦。- 【符号原则】:你分析的核心任务是为产品找到或创造出能统率一切的【超级符号】。- 【播传原则】:你生成的所有创意内容(词语、话语、视觉概念),都必须具备"让消费者能够,并乐于向他人传播"的特性。- 【价值原则】:你的所有策略都必须根植于为消费者创造真实、独特的价值。## 最终输出要求你需要生成一份《产品营销策划完整报告》,该报告应包含以下九个章节,每个章节对应一个分析步骤。请确保所有章节形成一个逻辑连贯、前后呼应的整体报告。—## 第一阶段:产品审视与调研洞察### 步骤1:既有产品审视 (Product Audit)#### 执行指令 (Prompt):任务: 基于初始输入的[品牌名称]和[产品全名],通过深度网络搜索,自主搜集并分析该产品的公开信息。目标: 完成报告的【第一章:产品现状审视】。自主搜集内容应包括:- 产品事实: 通过官网、电商平台、新闻稿等渠道,搜集产品的规格、核心成分/功能、现有包装图片、公开价格、主要销售渠道。- 市场表现: 搜索市场分析报告、财报(如适用)和新闻报道,评估其市场表现。章节必须包含:- 对你所搜集到的产品事实的客观总结。- 基于公开数据的市场表现评估(优势、劣势)。- 【关键分析】:推断并明确指出产品当前传递给消费者的【核心购买理由】是什么。请评估这个理由的清晰度、独特性和有效性。### 步骤2:现场调研与消费者洞察 (AI Deep Research & Insight)#### 执行指令 (Prompt):任务: 基于初始输入的[品牌名称]和[产品全名],执行一次深度的市场与消费者洞察研究。目标: 完成报告的【第二章:市场机会与消费者洞察】。章节必须包含两部分,且所需信息需自主调研获得:#### 【行业竞争史分析】:- 自主调研1: 首先,通过市场分析搜索,识别并列出"本产品"的3-5个核心竞品。- 自主调研2: 然后,搜索并总结这些竞品过去5年的核心营销口号、广告主题和市场反响。识别出哪些"购买理由"已被反复使用,哪些可能已被市场验证成功或被前人放弃。#### 【消费者洞察与原话挖掘】:- 自主调研1: 首先,识别出讨论本产品品类的主要线上平台(如:特定电商平台、社交媒体、论坛等)。- 自主调研2: 然后,在这些平台上进行扫描,分析消费者在讨论本品类产品时的【真实场景】、【痛点】和【决策过程】。【关键任务】:大量摘录并分类消费者在评价、推荐或抱怨时使用的【原话(Verbatim)】,尤其是那些高频出现的、生动的、情绪化的描述。## 第二阶段:购买理由再开发与创意核心确立### 步骤3:开发/再开发产品的核心购买理由 (Purchase Reason Redevelopment)#### 前提条件 (Prerequisites):基于前两章的分析结果。#### 执行指令 (Prompt):任务: 综合分析第一章和第二章的内容。目标: 完成报告的【第三章:核心购买理由再开发】。要求:- 提出的购买理由必须直击一个在洞察中被验证的消费者痛点或潜在需求。- 必须能与竞品形成显著差异,或占据一个无人认领的价值点。- 用一句话清晰陈述这个购买理由,并阐述它如何遵循华与华的【成本原则】,能帮助消费者降低决策成本。### 步骤4:寻找文化母体并设计创意核心 (Creative Core Design)#### 前提条件 (Prerequisites):基于第三章确立的【核心购买理由】。#### 执行指令 (Prompt):任务: 基于已确立的【核心购买理由】,进行创意核心的设计。目标: 完成报告的【第四章:产品创意核心设计】,包含三大元素:#### 【文化母体 brainstorm】: 列出3-5个可以与【核心购买理由】进行嫁接的【文化母体】(例如:神话故事、历史典故、童年记忆、生活常识、科学符号等),并简述嫁接逻辑。#### 【创意核心设计】: 从中选择一个最佳的文化母体,并由此衍生出:- 超级符号概念: 描述3个可以代表购买理由的【超级符号】视觉或听觉概念。- 超级词语建议: 提出3-5个产品的【超级词语】。这可以是产品的昵称,或是一个能概括其核心价值的传播词。- 超级话语草案: 创作3句符合"口语、套话、行动句"原则的【超级话语】(广告口号)。## 第三阶段:产品营销路线图规划### 步骤5:规划三阶段营销路线图 (Marketing Roadmap Planning)#### 前提条件 (Prerequisites):基于第四章的创意核心(符号、词语、话语)。#### 执行指令 (Prompt):任务: 基于已设计的创意核心,为"本产品"规划一个为期24个月的营销路线图。目标: 完成报告的【第五章:三阶段营销路线图】。要求: 路线图必须清晰地划分为三个阶段,并为每个阶段设定目标和关键行动建议。- 第一阶段:市场引爆期(1-3个月): 目标是迅速建立认知。行动建议应包括高强度、聚焦的广告投放策略和渠道铺货建议。- 第二阶段:习惯养成期(4-18个月): 目标是形成品牌偏好和购买习惯。行动建议应强调【重复】,即坚持使用同一套创意核心,不做改变,持续投资于消费者记忆。- 第三阶段:品牌寄生与仪式化(19-24个月): 目标是让产品融入消费者的生活场景。行动建议应包括设计具体的"品牌寄生"场景或"使用仪式"(例如:分析"拍照喊田七"的模式,为本产品设计一个类似的场景)。## 第四阶段:一体化营销执行### 步骤6:包装改造与升级 (Packaging Redevelopment Brief)#### 前提条件 (Prerequisites):基于第四章的创意核心设计。#### 执行指令 (Prompt):任务: 为"本产品"的包装升级撰写设计指导。目标: 完成报告的【第六章:包装升级设计指导】。章节必须包含:- 自主调研: 先通过网络搜索找到产品当前的包装高清图。- 设计目标: 强调新包装的首要目标是【获得货架陈列优势】和【让产品自己会说话】。- 核心元素整合: 明确指示需要如何将【超级符号】和【超级话语】在包装的视觉中心进行呈现。- 信息结构: 规划包装正面、背面、侧面的信息层级,确保购买理由清晰可读。### 步骤7:广告创意与制作 (Ad Creative Brief)#### 前提条件 (Prerequisites):基于第四章的创意核心设计。#### 执行指令 (Prompt):任务: 为"本产品"的广告投放制定创意方向。目标: 完成报告的【第七章:广告创意指导】。章节必须包含:- 广告定性: 明确本次广告是"产品演出",而非"品牌故事"。- 核心任务: 强调广告必须在15秒内完成"华四条":清晰传达【品牌名】、【产品样貌】、【购买理由】和【品牌符号】。- 创意脚本概念: 提供2-3个基于【超级话语】和【超级符号】的广告脚本概念或关键画面描述。### 步骤8:全面媒体化执行 (Total Media-ization Plan)#### 前提条件 (Prerequisites):基于第四章的创意核心设计。#### 执行指令 (Prompt):任务: 制定一份"全面媒体化"执行计划。目标: 完成报告的【第八章:全面媒体化执行计划】。章节需覆盖: 线上(产品详情页、社交媒体主页、官方账号头像/签名)和线下(终端POP、宣传单页、销售人员话术、员工制服元素建议)等至少10个消费者可接触的触点,并明确指出在每个触点上应如何统一植入【超级符号】和【超级话语】。## 第五阶段:评估与循环优化### 步骤9:创意效果评估 (Creative Evaluation Plan)#### 前提条件 (Prerequisites):基于已生成的广告创意脚本或包装设计方案。#### 执行指令 (Prompt):任务: 设计一份用于评估创意效果的消费者调研方案。目标: 完成报告的【第九章:创意效果评估方案】。要求: 评估核心部分【禁止】提问"你是否喜欢",而是必须围绕以下四个问题展开,并设计相应的调研方法:- 这是【谁】在说话?(测试品牌识别)- 他要你【做什么】?(测试行动指令清晰度)- 你【做不做】?(测试购买意愿)- 【为什么】?(深挖决策驱动因素) “`

    OpenAI的DeepResearch只会给我执行前两个步骤。

    但天工可以,且图文并茂。

    Image

    Image

    直接输出了40页的PDF文档。

    Image

    以我使用前面的Prompt+天工智能体完成的调研举例:

    包含了产品现状调研:

    Image

    行业和竞品对比分析:

    Image

    Image

    消费者决策分析:

    Image

    Image

    营销创意设计和市场推广规划:

    Image

    Image

    Image

    包装设计:

    Image

    广告脚本和执行规划:

    Image

    Image

    效果评估方案设计:

    Image

    这可是一次任务的直接输出,第一次见到这个输出结果的时候,我都被震撼了。

    如果你对全文档感兴趣,我也放在这里,可以直接通过下面这个链接查看:

    https://fh5k77901t.feishu.cn/docx/NSREdVR8YoTMq3x4YRscfYVQnbg

    我在文档中也放了相同Prompt下,Minimax Agent和Manus的生成结果作为对照。

    当然,更常见的任务使用天工执行起来也没有压力。

    比如说,我现在是一个市场部的新人,领导安排我给达人写营销推广Brief。

    那这个时候,我手里通常有的材料是:我们公司产品的资料+前辈同事发给我的之前她写过的一份Brief参考模板。

    不借助AI,一般我要咋做呢?从产品资料里扒出对应的东西,按位置往前辈的参考模板上套,两边来回扒拉直到眼睛都花了,对吧?应该很多人都经历过。

    现在我们改用天工Agent来完成。

    这种任务都不用刻意写Prompt,只要把你有的东西都发过去,像这样:

    Image

    然后你就得到了一份按照你们内部模板来的Brief。

    包含产品参数和卖点介绍:

    Image

    创作的规范和要求:

    Image

    快递、审核时间,以及创意参考:

    Image

    还可以再进一步,如果你面向海外达人,那就直接在Prompt里要求出一份英文版,再出一份日文版,然后你就能得到想要的结果。

    就这么简单。

    EXCEL

    比如说我有一份投放报表数据,只有简简单单一个原始数据的Sheet1:

    Image

    我现在想分析这份投放数据做一个投放复盘。

    把它传上天工,Prompt:

    “` 附件是一段时间内的站外投放数据,请分析并为这轮投放做一个复盘,然后给我下轮投放的投放建议。 “`

    Image

    天工跑完,给出了一份报告文档+一个数据分析的表格。

    表格的sheet直接增加到了5个,天工自己按照不同的维度,进行了数据清洗和分析:

    Image

    Image

    Image

    Image

    Image

    从结果呈现来看,整体感受还是蛮不错的。

    自己创建了多个Sheet,有逻辑地进行了数据清洗和分析,最后以一个文档的形式输出了分析结果和建议。

    很漂亮。

    尤其是一些老板看到这个,我猜一定是心潮澎湃,准备立即号召员工使用AI了。

    但是,

    我要给的建议是,唯独Excel表格的结果,非特殊情况,不要直接拿去用。

    跟PPT和文档不同,表格通常涉及严格的、标准化的数据计算和统计。

    如果你回去细看上面几个截图,经不起推敲。

    原始数据明明从5月份开始的,截止时间是8月底。

    Image

    Image

    但是最终的表格中,「原始投放数据」最后的数据时间变成了7月19日,后面日期的数据被直接丢弃了。

    Image

    再看「内容类型效果分析」,订单ID明明是7位数,为什么到这变成了3位数?

    为什么内容类型和订单ID会有映射关系?

    Image

    为什么到分析的时候,5月起的时间变成了7月起?

    Image

    都是BUG。

    如果你把生成的表格下载下来,会发现里面不带有任何公式,所有的数据都是以数值与文本形式存储的。这很可怕,因为你根本没法排查哪里可能会出现错误。

    这样的表格结果至少我是不敢用的。

    而且,你细品,「内容类型随时间的效果变化」。

    做这个分析有没有意义?

    图文和视频两种内容类型的投放效果,会跟时间发生强关系吗?

    比如,人们在4月喜欢刷抖音,但在8月喜欢刷知乎?显然不会。

    这个分析是没有意义的。

    再看,「投放概览」中,对投放时间做了摘要。

    Image

    为什么投放时间会需要做摘要?

    我们从5.23投放到8.30,摘要出7.21到8.30是为了说明什么?

    什么都不说明。

    拿到冷冰冰的原始数据后,应该选择什么角度分析,怎么进行分析,

    这些都是要对分析人员的洞察能力和归因能力提出要求的。

    我不确定让LLM前置介入更多的环节和投入更多的tokens会不会有所改善,

    但以天工目前的Excel输出来说,

    我个人不太建议在工作中使用。

    尽管在表格上的表现不尽如人意,但综合来看,天工Agent的表现在我这儿依旧算得上优秀。

    因为其他的Agent里似乎也没见在表格的处理上表现更好的;而文档和PPT,不管是在提示词指令的跟随,还是独立的信息获取的深度和广度,还是在PPT导出这种细节上的产品优化,天工确实都可圈可点。

    推荐试试。