AI围炉茶话会
AI围炉茶话会 — 工作流文档
1. 概述
- 工作流名称:AI围炉茶话会
- 工作流定位:用户输入一个聊天主题,自动生成两位说话人的口语化对话台词,并逐句合成为双人对话音频
- 触发方式:n8n 表单(表单标题「话题闲聊」,字段「主题」)
- 核心链路:主题输入 → 台词生成(LongCat-2.5)→ 双路语音合成(MiMo TTS)→ 音频合并(FFmpeg)→ 输出 merged_audio.wav
- 输出:合并后的双人对话音频文件(merged_audio.wav)
- 关键模型与服务:
- 台词生成:LongCat-2.5(OpenAI 兼容节点,按 JSON Schema 约束返回结构化台词)
- 语音合成:mimo-v2.5-tts,音色 Chloe(说话人 $1)、白桦(说话人 $2)
- 音频合并:n8n FFMPEG 节点(concat 拼接,pcm_s16le 编码)
2. 整体流程架构
- 主链路:表单填写 → 解析输入字段 → 生成编剧台词 → 转换数据类型 → 双分支拆分 → 双路语音合成 → 合并 → 重排 → 存储 → 音频拼接
- 分支结构:台词按说话人拆为两路,分别走「发起生成语音请求」→「base64转文件」→「写盘」,最终汇入 Merge 节点
- 数据流:主题文本 → 结构化台词数组 → 逐句音频(base64)→ 磁盘文件(1.xxx.wav / 2.xxx.wav)→ 文件列表 → 合并后的对话音频(merged_audio.wav)
- 关键中间产物:
- dialogue:台词数组,含 $1、$2 两位说话人各自的 content 列表
- 双路文件命名:1.
.wav(说话人 $1)、2. .wav(说话人 $2) - mylist.txt:按对话顺序排列的音频文件清单,供 FFmpeg concat 使用
3. 节点详解
3.1 表单填写(Form Trigger)
- 类型:n8n-nodes-base.formTrigger,版本 2.6
- 位置:[0, 0],整条工作流的触发器
- 作用:生成表单页,用户填写「主题」后提交触发工作流
- 参数:
- formTitle:话题闲聊
- formDescription:输入主题,给你返回闲聊
- formFields.values[0].fieldLabel:主题
- formFields.values[0].placeholder:请输入您要聊的主题
- formFields.values[0].requiredField:true
- options.appendAttribution:true
- options.buttonLabel:Submit
- 输出(每条 item 的 json):
1 | { |
- 说明:下游统一通过 $json[‘主题’] 读取,中文字段名必须用方括号写法访问
3.2 解析输入字段(Set)
- 类型:n8n-nodes-base.set,版本 3.5
- 位置:[224, 0]
- 作用:把表单输出的中文字段「主题」映射为英文字段 topic,供下游节点引用
- 参数(assignments.assignments):
- id:680295b0-2c51-4820-8f79-c281a6f5aca8
- name:topic
- value:=
- type:string
- 输出:{ “topic”: “用户输入的主题文本” }
3.3 生成编剧台词(OpenAI)
- 类型:@n8n/n8n-nodes-langchain.openAi,版本 2.3
- 位置:[448, 0]
- 作用:调用大模型,根据主题生成两位说话人的口语化对话台词,强制返回 JSON 结构
- 参数:
- modelId:{ “__rl”: true, “value”: “LongCat-2.5”, “mode”: “list”, “cachedResultName”: “LONGCAT-2.5” }
- builtInTools:{}
- responses.values[0].content:
1 | =请根据如下主题:{{ $json.topic }} |
- options.textFormat.textOptions.type:json_schema
- options.textFormat.textOptions.schema(完整):
1 | { |
- 输出:$json.output[0].content[0].text 为 JSON 字符串,解析后:
1 | [ |
3.4 转换数据类型(Set)
- 类型:n8n-nodes-base.set,版本 3.5
- 位置:[816, 0]
- 作用:把模型输出中的 JSON 字符串提取为字段 dialogue,并标记为数组类型,供后续 Split Out 使用
- 参数:
- assignments.assignments[0].id:d4f54230-93b9-4890-98e6-7e43f097f048
- assignments.assignments[0].name:dialogue
- assignments.assignments[0].value:=
- assignments.assignments[0].type:array
- options:{}
- 输出:
1 | { |
- 说明:type 设为 array 后,下游 Split Out 才能正确按数组下标取 dialogue[0].content / dialogue[1].content
3.5 Split Out / Split Out1(拆分双方台词)
3.5.1 Split Out(说话人 $1)
- 类型:n8n-nodes-base.splitOut,版本 1.1
- 位置:[1056, -176](上分支)
- 作用:把 dialogue[0].content 数组逐项拆成独立 item,每个 item 是一句 $1 的台词
- 参数:
- fieldToSplitOut:dialogue[0].content
- options:{}
- 输出(每个 item):{ “content”: “一句台词” }
- 说明:拆分后每个 item 的 $json.content 即为该句台词文本,直接作为 TTS 输入
3.5.2 Split Out1(说话人 $2)
- 类型:n8n-nodes-base.splitOut,版本 1.1
- 位置:[1024, 208](下分支)
- 作用:把 dialogue[1].content 数组逐项拆成独立 item,每个 item 是一句 $2 的台词
- 参数:
- fieldToSplitOut:dialogue[1].content
- options:{}
- 输出(每个 item):{ “content”: “一句台词” }
- 说明:与 Split Out 并行,分别对应两位说话人
3.6 发起生成语音请求 / 1(HTTP Request → MiMo TTS)
3.6.1 发起生成语音请求(说话人 $1)
- 类型:n8n-nodes-base.httpRequest,版本 4.5
- 位置:[1232, -176](上分支)
- 作用:把单句台词发给 MiMo TTS 接口,返回 base64 音频
- 参数:
- method:POST
- url:https://api.xiaomimimo.com/v1/chat/completions
- sendHeaders:true
- headerParameters.parameters[0].name:api-key(值为接口密钥)
- headerParameters.parameters[1].name:Content-Type,value:application/json
- sendBody:true
- specifyBody:json
- jsonBody:
1 | { |
- options:{}
- 说明:assistant 角色承载真正要合成的台词,user 角色为空串;voice 固定为 Chloe,代表说话人 $1
- 输出:
1 | { |
3.6.2 发起生成语音请求1(说话人 $2)
- 类型:n8n-nodes-base.httpRequest,版本 4.5
- 位置:[1200, 208](下分支)
- 作用:与 3.6.1 相同,但使用另一个音色,生成说话人 $2 的语音
- 参数:与 3.6.1 完全一致,唯一差异:
- jsonBody.audio.voice:白桦
- 说明:两个 HTTP 节点形成并行双路,分别产出两位说话人的音频
3.7 base64转文件 / 1(Convert to File)
3.7.1 base64转文件(说话人 $1)
- 类型:n8n-nodes-base.convertToFile,版本 1.2
- 位置:[1392, -176]
- 作用:把接口返回的 base64 音频字符串转成二进制文件
- 参数:
- operation:toBinary
- sourceProperty:choices[0].message.audio.data
- options.fileName:=.wav
- options.mimeType:audio/wav
- 输出:
1 | { |
- 说明:文件名使用接口返回的 audio.id,保证唯一性
3.7.2 base64转文件1(说话人 $2)
- 类型:n8n-nodes-base.convertToFile,版本 1.2
- 位置:[1392, 208]
- 作用:与 3.7.1 完全相同,处理说话人 $2 的音频
- 参数:与 3.7.1 一致
- 说明:两路产物文件名同为
.wav,靠后续写盘时的前缀区分说话人
3.8 Read/Write Files from Disk1 / 2(写盘)
3.8.1 Read/Write Files from Disk1(说话人 $1)
- 类型:n8n-nodes-base.readWriteFile,版本 1.1
- 位置:[1584, -176]
- 作用:把音频二进制写到本地磁盘
- 参数:
- operation:write
- fileName:=/home/node/.n8n/1.
- 输出:{ “fileName”: “/home/node/.n8n/1.
.wav”, “binary”: { … } } - 说明:统一加前缀 “1.”,标记为说话人 $1;后续 Code 节点据此区分两组文件
3.8.2 Read/Write Files from Disk2(说话人 $2)
- 类型:n8n-nodes-base.readWriteFile,版本 1.1
- 位置:[1568, 208]
- 作用:与 3.8.1 相同,写入说话人 $2 的音频
- 参数:
- operation:write
- fileName:=/home/node/.n8n/2.
- 输出:{ “fileName”: “/home/node/.n8n/2.
.wav”, “binary”: { … } } - 说明:统一加前缀 “2.”,与说话人 $1 的文件区分
3.9 Merge(合并双分支)
- 类型:n8n-nodes-base.merge,版本 3.2
- 位置:[1808, 16]
- 作用:把上分支(说话人 $1)与下分支(说话人 $2)的所有 item 汇总到一条链路
- 参数:{}(无显式参数,使用默认合并模式)
- 连接方式:
- input(0) ← Read/Write Files from Disk1
- input(1) ← Read/Write Files from Disk2
- 输出:包含两路全部 item 的数组,每个 item 形如
1 | { |
- 说明:默认合并模式为 append,输出顺序为 $1 全部在前、$2 全部在后,因此后续 Code 节点必须按说话人重新交替排序
3.10 拉链式合并:两个数组改成对话方式(Code)
- 类型:n8n-nodes-base.code,版本 2
- 位置:[1968, 16]
- 作用:把「先 $1 全部、后 $2 全部」的列表,重排成两人交替对话的顺序,并生成 FFmpeg concat 可用的文件清单文本
- 参数:
- jsCode:
1 | const items = $input.all(); |
- 输出:
1 | { |
- 说明:
- 用文件名前缀 “2.” 而非 speaker 字段判断说话人,因为此处 item 中只有 fileName
- 交替顺序为 $1 → $2 → $1 → $2,形成你一言我一语的对话节奏
- 输出内容直接对应 FFmpeg concat demuxer 的 list 文件格式
3.11 文本转二进制文件(Convert to File)
- 类型:n8n-nodes-base.convertToFile,版本 1.2
- 位置:[2144, 16]
- 作用:把 Code 生成的清单文本转为二进制文件对象
- 参数:
- operation:toText
- sourceProperty:=file_content
- binaryPropertyName:=data
- 输出:
1 | { |
- 说明:生成的是纯文本清单,不是音频
3.12 存储文件(Read/Write File)
- 类型:n8n-nodes-base.readWriteFile,版本 1.1
- 位置:[2336, 16]
- 作用:把清单文本写到固定路径,供 FFmpeg 读取
- 参数:
- operation:write
- fileName:/home/node/mylist.txt
- 输出:{ “fileName”: “/home/node/mylist.txt” }
3.13 合并多个音频为一个音频(FFMPEG)
- 类型:n8n-nodes-ffmpeg.ffmpeg,版本 1
- 位置:[2512, 16]
- 作用:按清单顺序把所有单句音频拼接为一个完整对话音频
- 参数:
- operation:custom
- outputFileName:merged_audio.wav
- outputBinary:mergedAudio
- ffmpegCustomArgs:=-f concat -safe 0 -i ““ -c:a pcm_s16le -f wav “{output}”
- 说明:使用 concat demuxer 无损拼接,输出为 wav 格式
- 输出:二进制属性 mergedAudio,即最终成品音频
4. 关键设计说明
- 双说话人拆分与并行 TTS 逻辑:
- 台词生成后由两个 Split Out 节点分别拆分 $1 / $2,形成两条独立并行链路
- 每条链路独立完成「TTS 请求 → base64 转文件 → 写盘」,互不阻塞
- 优势:两次合成可并行执行,整体耗时约等于单路耗时,而非两路之和
- 语音角色分配(Chloe / 白桦):
- 说话人 $1 → voice: Chloe
- 说话人 $2 → voice: 白桦
- 两个 HTTP 节点除 voice 字段外参数完全一致,保证音色切换只影响该字段
- 文件命名前缀区分说话人:
- 说话人 $1 文件写为 /home/node/.n8n/1.
.wav - 说话人 $2 文件写为 /home/node/.n8n/2.
.wav - 前缀由写盘节点 fileName 表达式添加,下游仅凭文件名即可判断归属
- 说话人 $1 文件写为 /home/node/.n8n/1.
- 对话顺序重排算法(交替合并):
- Merge 默认 append 导致顺序为「$1 全部在前、$2 全部在后」
- Code 节点按文件名前缀 “2.” 分组,得到 groupA($1)与 groupB($2)
- 按 maxLen 循环,交替 push
file '<$1 文件>'与file '<$2 文件>' - 结果:$1 → $2 → $1 → $2 的对话节奏,而非两段各自独白
5. 输入输出规范
5.1 表单字段定义
- 表单标题:话题闲聊
- 表单描述:输入主题,给你返回闲聊
- 字段:
- 字段名(json key):主题
- fieldLabel:主题
- placeholder:请输入您要聊的主题
- requiredField:true
- 提交按钮文案:Submit
- 附加归属信息:appendAttribution = true
- 触发输出(每条 item):
1 | { |
5.2 台词 JSON Schema 结构
- 顶层:array,minItems = 2,maxItems = 2,items = false
- prefixItems[0]:
- type:object
- required:[“speaker”, “content”]
- additionalProperties:false
- properties.speaker:const “$1”
- properties.content:array,minItems = 1,items 为 object
- required:[“content”]
- additionalProperties:false
- properties.content:string
- prefixItems[1]:
- 结构同上,唯一差异:properties.speaker.const = “$2”
- 解析后结构示例:
1 | [ |
5.3 最终音频文件规格
- 文件名:merged_audio.wav
- 格式:wav
- 编码:pcm_s16le
- 生成方式:FFmpeg concat demuxer 无损拼接(-f concat -safe 0 -i list.txt -c:a pcm_s16le -f wav)
- 内容:两位说话人交替的对话音频
- 输出二进制属性名:mergedAudio
6. 依赖与配置
6.1 模型依赖
- 台词生成模型:LongCat-2.5
- 节点:生成编剧台词(OpenAI)
- modelId:{ “__rl”: true, “value”: “LongCat-2.5”, “mode”: “list”, “cachedResultName”: “LONGCAT-2.5” }
- 通过 OpenAI 兼容节点调用,使用对应凭据
- 语音合成模型:mimo-v2.5-tts
- 节点:发起生成语音请求 / 1(HTTP Request)
- 接口地址:https://api.xiaomimimo.com/v1/chat/completions
- 请求体:model 字段固定为 mimo-v2.5-tts
- 音色:Chloe($1)、白桦($2)
- 音频格式:wav
6.2 文件路径约定
- 单句音频目录:/home/node/.n8n/
- 说话人 $1:/home/node/.n8n/1.
.wav - 说话人 $2:/home/node/.n8n/2.
.wav
- 说话人 $1:/home/node/.n8n/1.
- 音频清单文件:/home/node/mylist.txt
- 内容格式:每行
file '<音频绝对路径>',按对话顺序排列
- 内容格式:每行
- 最终成品:merged_audio.wav
- 由 FFMPEG 节点输出,二进制属性名 mergedAudio
6.3 凭据与密钥说明
- OpenAI 节点:使用 LongCat-2.5 对应的 OpenAI 兼容凭据(模型列表来自该凭据)
- HTTP Request 节点(两个):请求头 api-key 字段携带 MiMo 接口密钥,用于调用 TTS 接口
- 说明:两个 HTTP 节点共用同一接口地址与同一 api-key 请求头,仅 voice 字段不同
7. 运行与测试
7.1 触发方式
- 通过表单触发:访问表单页填写「主题」并点击 Submit
- 表单地址:http://localhost:5678/form/8654ef69-4290-4013-924e-0600c5d90d6e
- 手动执行:在画布中点击 Execute workflow,或直接执行「表单填写」节点
7.2 预期结果
- 台词生成:返回含 $1 / $2 两位说话人的结构化 JSON
- 语音合成:每位说话人每句台词生成一个 wav 文件
- 文件落盘:/home/node/.n8n/ 下生成 1.
.wav 与 2. .wav 两组文件 - 清单生成:/home/node/mylist.txt 按 $1 → $2 交替顺序记录文件路径
- 最终产物:FFMPEG 节点输出 merged_audio.wav,内容为两人交替对话的完整音频
7.3 常见问题排查
- 表单提交后无反应:
- 检查工作流是否已激活(Active)
- 确认表单地址中的 Webhook ID 与节点一致
- 台词生成失败或格式错误:
- 检查 LongCat-2.5 凭据是否有效、模型是否在凭据可用列表中
- 确认 options.textFormat.textOptions 已启用 json_schema,且 schema 与代码一致
- TTS 请求失败(401/403):
- 检查 HTTP 请求头 api-key 是否正确
- 确认接口地址 https://api.xiaomimimo.com/v1/chat/completions 是否可访问
- 语音文件未生成或为空:
- 检查 /home/node/.n8n/ 目录是否存在且可写
- 确认 Convert to File 节点的 sourceProperty 路径 choices[0].message.audio.data 正确
- 合并音频顺序错误:
- 确认 Code 节点按文件名前缀 “2.” 分组逻辑正常
- 检查 Read/Write Files from Disk1 / 2 是否分别加了 1. / 2. 前缀
- 合并音频失败:
- 检查 /home/node/mylist.txt 内容格式是否为
file '<路径>' - 确认 FFMPEG 节点 ffmpegCustomArgs 中 能正确引用清单文件路径
- 检查 /home/node/mylist.txt 内容格式是否为
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 BARAN的小站🔥🔥🔥!

