做实时视觉这些年,TouchDesigner 一直是我工作流里的主力工具。最近几套互动影像项目里,AI 生成内容的比重越来越高,我也顺理成章地把 ComfyUI 接进了 TD 的管线里。这个组合的好处很直接:ComfyUI 是节点式的本地图像生成环境,跟 TD 的节点逻辑天然对味;而且它自带完整的 HTTP API,相当于给 TD 开了一个可以远程调用本地显卡的接口。这篇文章不聊怎么装 ComfyUI,也不讲 Stable Diffusion 模型怎么调参,专门把 TD 对接 ComfyUI 过程中我踩过的坑、试出来的解法整理出来。适合已经在用 TD、准备把 ComfyUI 变成后台生成器的人参考。
1. 对接前的链路设计与环境准备
1.1 为什么选 ComfyUI 当 TD 的 AI 后端
TD 里接入 AI 生图的方式不止一条路。有人用 Stable Diffusion WebUI 的 API,有人直接调云端服务,也有人在本机跑 ComfyUI 再通过共享文件夹对接。我最终固定在 ComfyUI 上,是因为它和 TD 在思路上太像了——都是节点图,都是数据流驱动。TD 里一个 CHOP 接一个 TOP,ComfyUI 里一个采样器接一个解码器,这种结构上的同构性让你在 TD 里思考生成流程时,几乎不用切换心智模型。
另外 ComfyUI 的启动速度、显存占用、出图效率都明显优于 WebUI,尤其在做视频帧序列处理的时候,ComfyUI 的 batch 处理能力更稳。并且它的 API 设计非常干净——一个 POST 请求提交工作流,一个 WebSocket 推送状态和结果。对 TD 这种需要实时反馈的工具来说,这套机制足够轻量,也足够可控。
1.2 软硬件环境与端口监听配置
先确认你的 ComfyUI 版本和启动参数。官方默认启动只监听本机 127.0.0.1,端口 8188。如果 TD 和 ComfyUI 在同一台机器上,这个配置没问题;但如果你像我一样,用一台渲染机跑 ComfyUI、另一台工作机跑 TD,就必须让 ComfyUI 监听局域网。启动命令里加 --listen 0.0.0.0 即可。
bash复制python main.py --listen 0.0.0.0 --port 8188
注意 Windows 防火墙会弹窗询问是否允许 Python 访问网络,一定要点“允许”,否则局域网里另一台机器怎么都连不上。这个坑我遇到好几次,排查到最后发现根本不是代码问题,是防火墙拦了端口。
1.3 秋叶整合包用户特别要注意的几个点
很多国内用户用的是秋叶的一键整合包,这个包把 Python 环境、模型、常用插件都打好了,对新手非常友好。但它有几个和 TD 对接相关的细节必须了解:
- 整合包默认也是 8188 端口,如果你同时在跑 WebUI 或其他服务,可能端口冲突,需要改
--port。 - 整合包自带的启动器里有一些额外参数,比如
--force-fp16、--lowvram,这些参数在某些显卡上会影响 API 的响应速度,如果发现出图特别慢,可以检查启动器里的高级选项。 - 整合包的模型目录通常在
ComfyUI\models\checkpoints下,但有的整合包会把模型放在单独的models文件夹里。TD 通过文件路径访问输出图时,务必先确认 ComfyUI 的output目录到底在哪。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心对接方案:HTTP 提交 + WebSocket 监听
2.1 提交任务前先搞定 API 格式的工作流
这是整个对接过程中最容易翻车的一步。ComfyUI 界面里保存的工作流是 UI 格式,包含了节点坐标、连线信息、注释等一堆 TD 用不到的数据。而 API 接口只认 API 格式的 JSON,也就是每个节点以节点类型为键、class_type 和 inputs 为值的对象结构。
在 ComfyUI 界面里,点右上角菜单,选择 Save (API Format) 就能导出 API 格式的工作流。如果你从网上下载别人分享的工作流,也要注意区分格式——很多分享链接给的是 UI 格式,直接拿来提交会报 nodes 相关错误。
我刚开始对接的时候,误把 UI 格式 JSON 直接 POST 到 /prompt 接口,返回的报错信息非常含糊,排查了很久才发现是格式问题。所以第一步一定要确认:你手里那份 JSON 是 API 格式,不是 UI 格式。
2.2 TD 里发起 HTTP 请求的两种写法
TD 里发 HTTP 请求有两条路:一是用 Textport DAT,直接写 POST 请求;二是用 Script DAT 写 Python,import requests 调用。我用下来更推荐 Python 方式,因为参数构造、返回值解析都更灵活。
TouchDesigner 内置的 Python 环境自带 requests 库,这点很省心。下面是一个提交任务的脚本示例:
python复制import requests
import json
import time
# 读取 API 格式的工作流
with open("workflow_api.json", "r", encoding="utf-8") as f:
workflow = json.load(f)
# 替换工作流里的文本参数,比如 prompt 和 seed
workflow["6"]["inputs"]["text"] = "a beautiful sunset, digital art"
workflow["3"]["inputs"]["seed"] = 12345
# 提交给 ComfyUI
payload = {
"prompt": workflow,
"client_id": "td_visual_client"
}
resp = requests.post("http://127.0.0.1:8188/prompt", json=payload)
data = resp.json()
print("prompt_id:", data.get("prompt_id"))
注意:workflow["6"] 里的数字 6 是节点 ID,你在自己的工作流里导出 API JSON 后,需要先检查节点 ID 对应哪个节点。如果节点 ID 变了,脚本里的索引也要跟着改。实际项目里我习惯在 workflow JSON 里加一层映射,用节点类型去找节点:
python复制def find_node_by_class(workflow, class_type):
for node_id, node in workflow.items():
if node.get("class_type") == class_type:
return node_id
return None
这样即使节点 ID 变了,也能动态匹配,不用每次手动改脚本。
2.3 用 WebSocket 拿进度和结果
提交任务之后,ComfyUI 会在后台排队执行。如果只知道一个 prompt_id,你只能傻等,不知道生成到哪一步了。这时候 WebSocket 就派上用场了。ComfyUI 的 WebSocket 地址是 ws://127.0.0.1:8188/ws?clientId=td_visual_client,这里的 clientId 要和提交请求时带的 client_id 一致,这样才能收到对应任务的进度推送。
TD 里可以用 WebSocket DAT 连接这个地址,然后在它的 onReceiveMessage 回调里处理消息。ComfyUI 推送的消息主要有几种类型:
| 消息类型 | 内容 | 用途 |
|---|---|---|
status |
队列剩余数量 | 判断是否拥堵 |
executing |
当前执行的节点 ID | 判断执行到哪一步 |
progress |
value / max,表示当前节点内进度 | 实时进度条 |
executed |
节点执行完毕,包含输出图像信息 | 拿到结果数据 |
下面是一个在 Script DAT 里解析 WebSocket 消息的片段:
python复制def onReceiveMessage(dat, rowIndex, message, byteData):
if byteData:
# 二进制消息,通常是图片数据
return
msg = json.loads(message)
msg_type = msg.get("type")
if msg_type == "executed":
images = msg.get("data", {}).get("images", [])
if images:
img_info = images[0]
print("生成完成:", img_info["filename"])
这里有一个重要的经验:executed 消息会在每一个节点执行完时都触发,不只是最后的输出节点。所以判断“整张图生成完毕”,要么看最后一个节点的 executed 消息,要么等 status 消息里 queue_remaining 变成 0。
2.4 一个可跑通的 TD 端完整脚本示例
为了让你能直接抄作业,我把 TD 端的完整流程串起来。核心思路:用 Script DAT 发请求,用 WebSocket DAT 收进度,用 File COMP 或 Moviefilein TOP 显示结果图。
在一个 Script DAT 的 onStart 里初始化:
python复制import requests
import json
import time
requests_session = requests.Session()
base_url = "http://127.0.0.1:8188"
def submit_workflow(workflow_api_path, prompt_text, seed):
with open(workflow_api_path, "r", encoding="utf-8") as f:
workflow = json.load(f)
# 动态找到文本节点和采样器节点
text_node = find_node_by_class(workflow, "CLIPTextEncode")
sampler_node = find_node_by_class(workflow, "KSampler")
workflow[text_node]["inputs"]["text"] = prompt_text
workflow[sampler_node]["inputs"]["seed"] = seed
payload = {"prompt": workflow, "client_id": "td_visual_client"}
resp = requests_session.post(f"{base_url}/prompt", json=payload)
data = resp.json()
return data.get("prompt_id")
然后在 WebSocket DAT 的 onReceiveMessage 里根据 executed 消息里的图片信息,拼接出图片的完整 URL:
python复制def get_output_url(img_info):
filename = img_info["filename"]
subfolder = img_info.get("subfolder", "")
img_type = img_info.get("type", "output")
return f"http://127.0.0.1:8188/view?filename={filename}&subfolder={subfolder}&type={img_type}"
拿到 URL 之后,TD 可以用 Web DAT 或者 HTTP Request 把图片拉下来缓存成本地文件,再交给 Moviefilein TOP 显示。但要注意:TD 里同一个 TOP 不能频繁换文件路径,否则会闪烁。更好的做法是先生成到固定临时文件,再让 MOVIEFILEIN TOP 加载这个固定路径。
3. 图像回传的几种实现方式与效率对比
3.1 文件路径直读:简单粗暴但能跑
ComfyUI 默认会把生成结果写到 output 目录,文件名带时间戳和随机码。TD 这边最快的方式是用 Folder DAT 监听输出目录,一旦出现新文件就自动加载到 Moviefilein TOP。
这个方案的优点是稳定,不涉及网络传输,纯本地文件读写,几乎不会有数据丢失。缺点是文件会越积越多,需要定时清理。另外 ComfyUI 写文件需要一点时间,如果 TD 监听太快,可能在文件还没写完时就触发加载,导致读到一个损坏的图片。
我的做法是:在 ComfyUI 工作流里加一个 SaveImage 节点,并且把文件名参数改成固定名称,比如 latest.png。这样 TD 不需要监听目录,只需要每帧检查 latest.png 的修改时间,有新变化就重新加载。同时配合一个 Delete 逻辑,在上一次加载完成后把旧文件重命名备份,避免重复触发。
3.2 WebSocket 把图片塞回 TD 内存
如果不想落盘,希望在 TD 里直接拿到图片数据,可以用 WebSocket 的二进制消息。ComfyUI 在 executed 消息里除了返回图片信息,还可以通过配置把图片数据以 base64 或二进制帧的形式推送过来。TD 的 WebSocket DAT 支持接收二进制数据,在 onReceiveMessage 回调里可以通过 byteData 拿到原始字节。
拿到字节之后,最实用的处理方式是用 Python 的 PIL 转成 numpy 数组,再喂给 Script TOP 里的 numpy_to_texture 函数。这里有个性能问题:图片分辨率越大,转纹理越耗时。实测在 512x512 下没什么压力,但到了 1024x1024,每帧做一次解码 + 转纹理,帧率会掉到十几帧。所以这个方案更适合对实时性要求极高、且分辨率不高的场景。
python复制from PIL import Image
import io
import numpy as np
byte_data = your_byte_data_from_websocket
img = Image.open(io.BytesIO(byte_data)).convert("RGBA")
arr = np.array(img)
# 交给 Script TOP 生成纹理
3.3 HTTP 轮询 history 接口
还有一种很朴素的方案:提交任务后,每隔一段时间请求一次 GET /history/{prompt_id},检查 outputs 里有没有图像数据。这个方法的好处是逻辑简单,不需要 WebSocket,适合快速原型验证。缺点是存在延迟,轮询间隔设得太短会加重 ComfyUI 的负担,设得太长又不够实时。
我一般在调试阶段用轮询,跑通之后换成 WebSocket。轮询的代码也很短:
python复制def wait_for_result(prompt_id, timeout=120):
start = time.time()
while time.time() - start < timeout:
resp = requests.get(f"{base_url}/history/{prompt_id}")
history = resp.json()
if prompt_id in history:
outputs = history[prompt_id].get("outputs", {})
for node_id, output in outputs.items():
if "images" in output:
return output["images"][0]
time.sleep(2)
return None
3.4 三种方案的实测对比与选型建议
| 方案 | 延迟 | 稳定性 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 文件路径直读 | 中等(200-500ms) | 高 | 低 | 大多数演出、交互项目 |
| WebSocket 二进制 | 低(50-100ms) | 中 | 高 | 快速预览、实时联动 |
| HTTP 轮询 | 高(2-5s) | 高 | 低 | 调试、非实时任务 |
我的建议是:正式项目优先用文件路径直读,配合固定文件名和修改时间判断,既稳定又省心。WebSocket 方案留到确实需要亚秒级反馈的时候再用,因为它在 TD 里的调试成本确实不低。
4. 高频故障与排查技巧实录
4.1 常见报错速查表
| 报错现象 | 可能原因 | 解决方法 |
|---|---|---|
| TD 连不上 127.0.0.1:8188 | ComfyUI 没启动或端口不对 | 检查 ComfyUI 启动日志、确认端口 |
| POST /prompt 返回 400 | 提交的是 UI 格式工作流 | 用 Save (API Format) 导出 |
| WebSocket 连不上 | clientId 不匹配 | 提交和连接用同一个 client_id |
| 出图一直卡在排队 | GPU 显存不足,任务堆积 | 降低 batch size、用 --lowvram |
| 返回图片 URL 但 TD 打不开 | 子目录或 type 参数不对 | 确认 subfolder 和 type 字段 |
| JSON 解析报错 | 工作流中有中文引号或非法字符 | 用文本编辑器检查 JSON 格式 |
4.2 模型加载失败与自定义节点缺失
从网上下的工作流,拿回来一提交就报 ValueError: ... 或者 node type not found,大概率是缺少自定义节点。ComfyUI 社区的工作流重度依赖各种自定义节点,比如 ControlNet Preprocessors、Impact Pack、WAS Node Suite 等。
遇到这种情况,优先用 ComfyUI Manager 安装缺失节点。装完之后重启 ComfyUI,再重新导出 API 格式。这一步里有个容易忽略的坑:有些自定义节点在 API 格式下会暴露额外的必需参数,如果你用的工作流是旧版本,输入参数对不上,也会报错。解决办法是重新在界面上加载一次工作流,确认所有节点都能正常显示,再导出 API 格式。
4.3 队列堆积、超时与请求打架
TD 如果每一帧都发一次生成请求,ComfyUI 的队列会瞬间堆爆。我在早期项目里就犯过这个错——视觉参数一变,就触发重新生成,结果 ComfyUI 的队列排了几百个任务,等轮到当前帧的任务时,画面早就该切到下一个状态了。
解决办法是在 TD 侧做好请求节流:同一个 prompt_id 在执行期间,不发送新的提交请求;如果参数变了,先把当前任务标记为“取消”,再提交新任务。ComfyUI 的 POST /queue 接口支持清空队列,参数可以传 {"clear": true}。但清空队列会放弃所有正在排队的任务,如果正在执行的任务也受影响,可能需要权衡一下。
另外一个容易忽略的点是 client_id 的冲突。多个客户端共用一个 client_id 会导致 WebSocket 消息串台。每次启动 TD 工程时动态生成一个 UUID 作为 client_id,可以避免这个问题。
4.4 TD 端性能坑:别在每帧里干重活
TD 的帧循环默认 60fps,如果你在 onFrame 里同步等待 ComfyUI 返回结果,整个工程都会卡住。一定要把耗时操作放到异步线程里,或者至少把请求和等待拆开,不要在帧事件里做 requests.post 然后立即等待响应。
实际项目中我的做法是:用一个 Script DAT 维护一个状态机,状态包括 idle、submitting、waiting、processing。只有在 idle 状态下才提交新任务,提交之后立即把状态切到 waiting,不阻塞帧循环。WebSocket 回调收到 executed 后才把状态切回 idle。这样 TD 的帧率完全不受生成任务影响。
写到这里,我再补充一个经验:ComfyUI 的 prompt 文本里尽量不要混用中英文,Stable Diffusion 的 CLIP 模型对中文支持很一般。很多新手喜欢在 prompt 里写中文标签,出来效果往往飘忽不定。建议装一个免费的中英翻译节点,在 ComfyUI 工作流里先把中文 prompt 翻译成英文再进入采样器,效果会稳定很多。这不算 TD 对接的问题,但直接影响你从 TD 传进来的参数能否生成预期效果。
5. 实战案例:做一个可调参数的实时生成控制台
5.1 用 TD 控件映射 ComfyUI 参数
下面分享一个我实际跑过的场景:用 TD 做一个交互控制界面,通过滑块、按钮实时调整生成参数,控制 ComfyUI 出图。参数映射逻辑很简单——界面组件绑定到 Script DAT 的变量,触发事件时把变化写入 workflow JSON。
python复制# 滑块回调示例
def onValueChange(channel, sampleIndex, val, prev):
# 更新全局参数
parent().par.Seed = int(val)
# 标记需要重新生成
parent().par.Regen = True
参数响应要做防抖。滑块连续拖动时,触发的频率很高,每次都提交任务会把 ComfyUI 打崩。我在脚本里加了一个 300ms 的防抖:只有在停止拖动 300ms 后才真正提交。这个数字不是拍脑袋,是我实测下来的结果,既能保持交互流畅,又不会让队列堆积。
5.2 循环生成与自动筛选机制
另一类常见需求是“无限生成,挑选满意的图”。TD 里可以做一个循环触发器,每 3 秒自动提交一个新种子,同时把生成结果按时间戳存到指定文件夹。关键是如何判断结果可用——我通常在 ComfyUI 工作流里加一个 NSFW 检测节点或清晰度检测节点,在出图之后自动过滤掉不合格的图,只把过关的结果写到 TD 的自动筛选目录。
这种自动化流程跑起来之后,你会发现真正影响体验的不是生成速度,而是种子选择。完全随机种子很容易出废片,我会在 TD 里维护一个种子池,根据交互状态从池子里选种子。如果你做的是视频类项目,想让连续几帧的人物保持一致,固定 seed 至关重要——即使只改 prompt 里的一两个词,只要 seed 不变,主体结构就能保持相对稳定。
5.3 实测数据与效果表现
我实测的配置是:RTX 4090 24GB,512x512 分辨率,默认采样器,单张图生成时间约 1.2 秒。TD 端通过文件路径直读,从参数变化到画面更新大约 1.5-2 秒,其中一半时间花在排队和采样上。这个延迟对演出场景来说是可以接受的,毕竟观众看到的是一个渐变的视觉流,而不是追求毫秒级的反馈。
如果换到 1024x1024 分辨率,单张生成时间会拉到 5-6 秒,这时候就不能直接同步显示,需要配合缓存机制:先出低分辨率预览,等高清图生成后再替换。TD 里用两个 Moviefilein TOP 做交叉淡化就能实现这个效果,成本很低,但体验提升非常明显。
在跑通这套流程之后,我最大的体会是:TD 和 ComfyUI 的对接,技术难点从来不在某个单独的环节,而在整体的链路设计——从环境准备、工作流格式、请求方式、图像回传到异常处理,每个环节都埋着坑。建议你第一次做对接时,先别急着写完整项目,按我上面说的顺序,分阶段把每一步跑通,再把它们串起来。我自己最初就是先手动点 ComfyUI 界面确认节点能出图,再在 TD 里发一个简单的 POST 请求,最后才加 WebSocket 和自动保存逻辑。这样一步步来,即便出了问题,也清楚知道该排查哪一段。
