把 ComfyUI 改造成无人值守视频产线:状态机、顺序锁与 ComfyUI 插件工程实践

硬件约束:RTX 5070 Laptop 8GB VRAM / 32GB RAM / Windows。 生成端:ComfyUI 0.40 + MiniMax H3(33B 全模态,int8_convrot)。 编排端:video_pipeline.py(单文件服务,约 160KB),监听 127.0.0.1:8898。 本文只讨论工程实现,不讨论提示词和模型选型。


1. 问题定义

单条生成链路本身已经能跑通:ComfyUI 出 mp4,Whisper 对齐字幕,ffmpeg 烧录,JEV 抽帧质检,ACE-Step 出 BGM。

真正的问题是这条链路不可运营:

  • 任务入口靠人拖文件,状态靠人记;
  • 任一阶段失败,整条链断,无法从失败点恢复;
  • 长片由 16 个分段组成,单段失败会拖垮整片;
  • ComfyUI 重启后,编排层丢失"正在生成"的任务;
  • 没有统一的 status/event/history,无法做 ETA 和告警。

所以目标不是"再写一个更长的脚本",而是把生成端和编排端解耦,做成一个可恢复、可观测、可回滚的常驻服务。


2. 总体架构

vbnet 复制代码
┌─────────────────────┐        file system        ┌──────────────────────┐
│  ComfyUI :8188      │ ────────────────────────> │  H:\h3cache\raw      │
│  H3 / MOSS / ...    │   SaveVideo 落盘           │  (任务入口)          │
└─────────┬───────────┘                           └──────────┬───────────┘
          │ /prompt (仅长片分段主动提交)                       │ detector 每 4s 扫描
          │                                                  v
┌─────────┴────────────────────────────────────────────────────────────────┐
│  H3 Director's Desk :8898                                                │
│  FIFO queue → single worker → 6-stage state machine → history/manifest   │
│  /status  /download  /task/bgm  /task/delete  /?embed=1                  │
└─────────┬────────────────────────────────────────────────────────────────┘
          │ 子进程 / HTTP
          v
  JEV 4B(决策/分镜/质检) · faster-whisper(字幕) · ACE-Step(BGM) · ffmpeg

三个边界:

  1. ComfyUI 不感知编排层 :导演台不修改采样图、不接管队列,只在长片分段时通过 /prompt 主动提交。
  2. 编排层不感知采样细节:它只消费 mp4、字幕、评分,不关心 H3 用了多少步。
  3. 所有状态落盘 :pipeline_history.json、pipeline_eta.json、长片 manifest.json,重启可恢复。

3. 任务发现:为什么用"大小稳定"而不是文件事件

ComfyUI 的 SaveVideo 是"先创建文件,再写入"。文件出现瞬间大小为 0,直接入队会把半截视频送进流水线。

检测器每 4 秒扫描 raw,判定条件:

ini 复制代码
# 伪代码
size = stat(path).st_size
if size > 1 * 1024 * 1024 and size == last_size.get(path):
    enqueue(path)
last_size[path] = size

要求"连续两次采样大小一致",用一个扫描周期换掉对文件系统事件的依赖,跨平台、无额外依赖,也避免了 inotify 在 SMB/exFAT 上的各种边界问题。

副作用:最坏情况下任务延迟 8 秒入队。对分钟级的生成任务可以接受。


4. 状态机:阶段可跳过、可失败、可恢复

单条视频六阶段:

阶段 依赖 可跳过 失败策略
语义决策 JEV 4B 否 重试 1 次,仍失败则用默认配置
帧率重构 ffmpeg minterpolate 是(24fps 直出) 跳过并记录
字幕合成 H3 自带字幕 / Whisper + ASS 是(已有 baked subtitle) 重试 1 次,仍失败则交付无字幕版并标记
质量评估 JEV 4B 是(配置关闭) 标记 unknown,不阻塞交付
配乐混音 ACE-Step + ffmpeg 是(默认关闭) 跳过配乐,保留原声
交付归档 文件系统 否 失败进 失败,写 history

实现上不是 stage1() && stage2() && ...,而是:

css 复制代码
for stage in stages:
    if stage.should_skip(ctx):
        emit(stage, "end", detail="已跳过")
        continue
    emit(stage, "start")
    try:
        stage.run(ctx)
    except Exception as e:
        emit(stage, "error", detail=str(e))
        if not stage.retryable:
            raise
        stage.retry(ctx)
    emit(stage, "end", detail=stage.summary)

每个阶段写 start / detail / end 三类事件,事件带全局自增 seq 和任务 id。前端按 seq 增量拉取,不需要轮询整份日志。

恢复策略 :重启后扫描 pipeline_history.json,把处于 running 状态的任务标记为 interrupted;如果输出文件已存在且帧数达标,则从"交付归档"恢复,否则从失败阶段重跑。


5. 长片:段数由剧本决定,重跑按 idx 原位替换

长片入口是 raw<片名>.film.json:

json 复制代码
{
  "style": "3D 卡通都市恋爱",
  "lang": "zh",
  "lines": ["...", "..."],
  "reuse": ["seg_00.mp4", "seg_01.mp4"]
}

5.1 段数计算

lua 复制代码
每段最多 3 行台词
段数 = ceil(len(lines) / 3)

reuse 只决定"这一段复用已有文件还是重新生成",不参与段数计算。这样同一份剧本无论复用多少段,切分边界都稳定。

5.2 顺序锁

  • 分段重跑:结果写临时文件,os.replace() 原位替换原 idx;
  • 拼接:只按 idx 升序读分段,写新文件,绝不修改分段;
  • 全 cut 且编码参数一致:concat -c copy(无损);
  • 含 crossfade:xfade 链;
  • 参数不一致:先生成归一化副本,再拼接。

这条规则的价值在后期:任何一次"重拼"都不会破坏已有分段,也不会产生"拼接把源文件改了"的不可逆事故。

5.3 逐段质检与重跑

每段生成后:

  1. 画幅归一化、音频统一 48kHz;
  2. JEV 抽 8 帧评分(broken / flawed / good);
  3. 检测 H3 是否自带字幕(有则跳过后期烧录);
  4. 不合格换 seed 原位重跑,最多 2 次。

长片 16 段,单段失败只影响该段,整片仍可继续。


6. 可观测性:状态快照 + 事件流 + 时钟插值

前端每 1.5 秒 GET /status,响应包含:

json 复制代码
{
  "tasks": [...],
  "films": [...],
  "events": [{"seq": 1024, "task": "...", "stage": "subtitle", "kind": "detail", "text": "..."}],
  "stages": [{"key": "decide", "label": "语义决策", "eta": 50.8}],
  "overall": {"phase": "idle", "text": "待片中 · 盯住 raw 目录"}
}

两个实现细节:

  1. 事件增量 :前端记录最后一条 seq,后续只追加新事件,单条线路最多保留 80 行,避免 DOM 无限增长。
  2. 进度插值:阶段进度每 400ms 用本地时钟插值,服务端只提供阶段起止和 ETA。这样进度条平滑,但不伪造"采样百分比"。

日志采用"粘性跟随":默认跟随底部;用户上滚后固定位置,显示"N 条新输出 · 回到最新";回底恢复跟随。


7. ComfyUI 插件机制:不需要 fork 主程序

ComfyUI 的扩展点只有三个:

ini 复制代码
# custom_nodes/MyNode/__init__.py
NODE_CLASS_MAPPINGS = {"MyNode": MyNode}
NODE_DISPLAY_NAME_MAPPINGS = {"MyNode": "我的节点"}
WEB_DIRECTORY = "./web"   # 可选,纯前端扩展

节点类实现 INPUT_TYPES / RETURN_TYPES / FUNCTION 即可。ComfyUI 负责把输入渲染成控件、把输出连成线、管理显存和采样循环。

这意味着:

  • 不要自己写采样循环 。先把模型加载成 MODEL / CLIP / VAE,用原生 KSampler、ModelSamplingSD3、VAEDecode 跑通。
  • 不要 fork 主程序 。所有定制都应该收敛在 custom_nodes/ 和 extra_model_paths.yaml 里。
  • 失败必须隔离。插件初始化异常不能让 ComfyUI 起不来。

8. 案例 A:MOSS-Image 5B(Apache 2.0)与 INT8 单文件兼容

MOSS-Image 是 5B 文生图模型,官方提供 ComfyUI 插件。导入路径:

makefile 复制代码
D:\ComfyUI\custom_nodes\moss-image\
D:\ComfyUI\models\diffusion_models\moss_image_sft_int8_convrot.safetensors
D:\ComfyUI\models\text_encoders\qwen3.5_9b_w4a8_gptq.safetensors
D:\ComfyUI\models\vae\flux2-vae_small.safetensors

跨盘模型通过 extra_model_paths.yaml 挂载,不复制第二份。

8.1 为什么需要补丁

Comfy-Org 的 INT8 单文件把 q/k/v 融合成一个 qkv 投影;插件原始实现按三个独立 q/k/v 读权重。直接加载会缺 key,或者更隐蔽:加载成功但注意力计算错误。

补丁让模型类同时支持两种布局:

ini 复制代码
self.proj = nn.ModuleDict(...)
# Comfy-Org INT8 single-file checkpoints fuse q/k/v into one qkv projection.
self.proj["qkv"] = operations.Linear(dim, 3 * dim, bias=False, ...)

qkv_w = self.proj["qkv"].weight
if qkv_w is not None:
    qkv = self.proj["qkv"](y).unflatten(-1, (3 * self.heads, -1))
    q, k, v = qkv.chunk(3, dim=2)
else:
    q = self.proj["q"](y)
    k = self.proj["k"](y)
    v = self.proj["v"](y)

8.2 工作流

scss 复制代码
MOSSImageUNETLoader
  → ModelSamplingSD3 (shift 5.0)
  → SkipLayerGuidanceDiTSimple (single_layers=22)
  → KSampler (euler / sgm_uniform, 24 steps, cfg 3.0)
  → VAEDecode (FLUX.2 small VAE)
  → SaveImage

插件只提供 loader,采样/引导/解码全部复用原生节点。8GB 实测:1024×1024、24 步约 274 秒(含加载)。

8.3 升级风险

插件升级会覆盖 model.py,补丁丢失后模型会静默输出错误结果(不一定是报错)。工程上要把它当成"需要回归验证的变更",而不是普通升级。


9. 案例 B:H3 一致性守卫与 PromptServer 队列钩子

9.1 节点

yaml 复制代码
H3 一致性提示词守卫 (H3ConsistencyPrompt)
  inputs:  prompt, profile_path, segment_index, repair, previous_segment
  outputs: guarded_prompt, warnings_json

节点只做一层壳:

ini 复制代码
res = h3_guard.build_prompt(prompt, prof, int(segment_index), repair=bool(repair))
return (res["prompt"], json.dumps(meta, ensure_ascii=False))

规则在服务端 h3_guard.py,ComfyUI 节点和批量脚本共用同一套实现,避免"界面一套、脚本一套"的双份逻辑。

9.2 为什么需要队列钩子

已有工作流不会自动带这个节点。要让守卫对"所有 H3 任务"生效,只能在 PromptServer 的队列入口做一次拦截:

ini 复制代码
orig_put = q.put

def guarded_put(item=None, *args, **kwargs):
    if isinstance(item, tuple) and len(item) >= 3:
        prompt = _auto_guard_prompt(item[2], item[3] if len(item) > 3 else None)
        if prompt is not item[2]:
            item = (item[0], item[1], prompt) + item[3:]
    return orig_put(item, *args, **kwargs)

q.put = guarded_put

9.3 隔离条件

钩子必须"只改它认识的工作流":

  • 只处理 CLIPTextEncode / CLIPTextEncodeFlux;
  • 只对匹配到 profile(片名或至少两个角色名)的工作流注入;
  • 检测到 MOSSImage* 节点直接跳过,生图工作流不能吃电影守卫;
  • 工作流已手动放守卫节点(提示词包含 【一致性守卫)则不重复注入;
  • 任何异常只打印,不抛出。

这是"横切关注点"接入共享队列时的最低要求:默认放行,失败放行,只改明确匹配的路径。


10. 案例 C:云端免费模型的节点化与安全边界

CogView-3-Flash 是云端免费模型。节点:

css 复制代码
CogView-3-Flash (H3)
  inputs:  prompt, size, quality, remove_watermark, watermark_enabled
  outputs: IMAGE, STRING(path)

内部调用本地客户端,把生成结果读成 RGB tensor 返回,因此能直接接 PreviewImage / SaveImage。

10.1 多 provider 注册表

javascript 复制代码
H:/H3DirectorDesk/cloud_image_providers.json

适配器支持 zhipu_images / minimax_image / openai_images / openai_chat。侧栏通过 /cogview3/providers 读取(key 仅返回掩码),/cogview3/generate 生成,/cogview3/history 查历史,/cogview3/file 取图。

10.2 三条安全约束

  1. 密钥只在服务端进程内读取,响应/日志不回显;
  2. 错误文本统一 _redact(),抹掉 Bearer 令牌和长随机串;

图片目录 :H:\h3cache\发布\00_通用素材\导演台自动化(下面每张图的括号里都标了该目录下的文件名,直接去这个目录找)

云端节点最常见的漏洞不是功能,是密钥泄漏和路径穿越。这两件事必须在第一版就堵死。


11. 权衡与限制

决策 代价 为什么接受
单 worker 串行 吞吐受限于单任务 8GB 显存并发会 OOM;稳定优先
4 秒轮询 + 大小稳定 最坏 8 秒延迟 跨平台、无文件事件依赖
状态落盘 JSON 高频写盘 任务分钟级,写频率可忽略;重启可恢复
队列钩子 与 ComfyUI 内部 API 耦合 只 hook PromptServer.prompt_queue.put,升级需回归
插件补丁 升级会覆盖 补丁小、可脚本化重打;比 fork 主程序可控
JEV 质检 8 帧启发式,可能漏检 定位是"筛废片",不是"保证质量"

明确的非目标:

  • 不做多机调度;
  • 不做 token 级生成进度(采样进度在 ComfyUI 队列里);
  • 不让 JEV 参与生成;
  • 不保证角色 100% 一致(提示词级守卫有上限)。

12. 结论

这套东西的价值不在"功能多",而在三个工程属性:

  1. 可恢复:阶段状态落盘,重启从失败点继续;
  2. 可隔离:单段失败不影响整片,插件异常不影响 ComfyUI;
  3. 可回滚:长片只读分段写新文件,插件删除目录即回滚。

免费模型接入 ComfyUI 的标准路径也已经很清楚:

官方/社区插件 + 标准模型目录 + 原生采样器 + 最小补丁。

不要把"模型能跑"当成"工程可用"。真正决定能不能长期跑的,是失败路径怎么处理。


文中截图均为本机真实运行;MOSS-Image 为 Apache 2.0,Ideogram 4 为非商业许可,商用前请核对模型卡。

相关推荐
2601_965454231 小时前
开源项目发布会不用剪视频,AI一键生成宣传片
人工智能·音视频
工具派1 小时前
不重写一行老代码:给 Spring Boot 系统加一层 AI 能力
java·人工智能·spring boot
武子康1 小时前
Coding Agent 的知识也会漂移:用固定快照检查 Vercel Plugin 升级
人工智能·后端·agent
黑妹天下第一乖1 小时前
255 次尝试就能恢复 CA 私钥?这次 TLS 信任链事故,值得每个 AI 开发者警惕
人工智能·python·https·tls·信任链·ca
昇腾CANN1 小时前
10月23日直播丨CANN挑战赛决赛赛题解析
人工智能·昇腾·cann·cann开源
牛战士coding1 小时前
用户点了停止生成,上游还在烧 token 吗?取消传播链路拆解
java·人工智能
长不大的杨瑞杰1 小时前
给 OpenCode v2 接入大模型:Provider、模型选择与 API Key 安全配置实战
人工智能·安全·ai·语言模型
虎鲸不是鱼2 小时前
AI 自动化逆向(九)AI 会记住错的公共常量
人工智能·自动化·大模型·逆向·ai逆向
飞猫的边缘AI2 小时前
芯片设计会不会全自动化?什么时候到来?
人工智能·ic设计·芯片设计·智能体·边缘ai·ai岗位·ai职业前景