Files
BoosAPI/docs/superpowers/plans/2026-06-18-ltx2-digital-human-workflow.md
T
boos 937442df04 docs: LTX2.3 数字人工作流实现计划(5 任务)
获取API导出 → 改造工作流(标记+删RecordAudio) → 补 media.py image_path 转发
→ 校验探测 → 真实联调。硬依赖用户导出 API 格式 JSON。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 02:20:36 +08:00

15 KiB

LTX2.3 数字人工作流(Comfy Cloud)实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 让 Pixelle 数字人(说话头像)走 Comfy Cloud:把官方 LTX2.3 id_lora 模板做成 comfy/digital_ltx2.json 工作流,补上 Pixelle media.py 的 image_path 转发缺口,经已建好的 comfy-cloud 适配器执行。

Architecture: 工作流文件落 Pixelle 宿主 data/workflows/comfy/(经 ./data 挂载),用 comfykit 的 _meta.title 标记($~image_path!/$~audio_path!/$prompt!)注入肖像/音频/脚本;media.py 非 api 分支补 image_path 进 workflow_params;comfy/ 通道经 comfyui_url(=BoosAPI /comfy-cloud 适配器)打到 Comfy Cloud。

Tech Stack: Python (Pixelle/comfykit), ComfyUI API-format JSON, Comfy Cloud API, Docker。

环境约定: Pixelle 跑在 Docker 容器 pixelle-video-api,代码 /app/pixelle_video/,工作流 /app/data/workflows/(宿主 /opt/Pixelle-Video/data/workflows/,bind mount,改宿主即生效)。容器内 python:/app/.venv/bin/python。BoosAPI 适配端点:容器内经 http://172.17.0.1:4000/comfy-cloud,宿主经 http://127.0.0.1:4000/comfy-cloud。改 media.py 后需 docker compose restart api。

硬依赖(Task 1 阻塞全局): 需用户从 Comfy Cloud 网页导出 video_ltx2_3_id_lora 的 API 格式 JSON 到 /tmp/ltx2_api.json。服务端无法代取。


文件结构

  • 用户提供:/tmp/ltx2_api.json —— Comfy Cloud 导出的 API 格式工作流(子图已展开)。
  • Create /opt/Pixelle-Video/data/workflows/comfy/digital_ltx2.json —— 改造后的数字人工作流(加标记、删 RecordAudio)。
  • Modify(容器内)/app/pixelle_video/services/media.py —— 非 api 分支把 image_path 加入 workflow_params;同步宿主持久化方案。
  • 临时:/tmp/digital_ltx2_probe.json —— 校验探测用 payload。

Task 1: 获取并校验 API 格式导出(硬依赖)

Files:

  • 用户提供:/tmp/ltx2_api.json

  • Step 1: 确认导出文件存在且是 API 格式

用户在 Comfy Cloud 网页打开 video_ltx2_3_id_lora(或 share 71f221d28df8)→ Workflow → Export (API Format) → 放到 /tmp/ltx2_api.json。

Run: rtk proxy docker exec pixelle-video-api sh -lc "ls -la /tmp/ltx2_api.json 2>/dev/null || echo MISSING" 注意:文件在宿主还是容器?用户可能放宿主 /tmp。两处都查: Run(宿主): ls -la /tmp/ltx2_api.json 2>/dev/null || echo HOST_MISSING Expected: 文件存在(任一处)。若两处都 MISSING → BLOCKED,等用户提供。

  • Step 2: 校验是 API 格式(不是 UI 格式)

API 格式是 {节点id: {class_type, inputs, ...}},无顶层 nodes 数组。

Run:

python3 -c "
import json
wf=json.load(open('/tmp/ltx2_api.json'))
is_ui = isinstance(wf,dict) and 'nodes' in wf
print('UI格式(需重导)' if is_ui else 'API格式 OK')
if not is_ui:
    print('节点数:', len(wf))
    has_sub = any('subgraph' in str(v.get('class_type','')).lower() for v in wf.values() if isinstance(v,dict))
    print('含未展开子图?', has_sub)
"

Expected: API格式 OK,节点数 >10,无未展开子图。若是 UI 格式或含子图 → 让用户用 API Format 重新导出(普通 Export 是 UI 格式)。

  • Step 3: 列出节点类型 + 模型引用,核对 Comfy Cloud 兼容

Run:

python3 -c "
import json
wf=json.load(open('/tmp/ltx2_api.json'))
from collections import Counter
c=Counter(v.get('class_type') for v in wf.values() if isinstance(v,dict))
print('节点类型:')
for t,n in c.most_common(): print(f'  {n}x {t}')
print('--- 模型引用 ---')
for nid,nd in wf.items():
    if not isinstance(nd,dict): continue
    for k,v in (nd.get('inputs') or {}).items():
        if isinstance(v,str) and v.endswith('.safetensors'):
            print(f'  {nid}.{k} = {v}')
"

Expected: 看到 LTX2.3 节点 + 模型文件名。记录下来,Task 2 核对它们在 Comfy Cloud 池里(之前已验证 ltx-2.3-* 系列在;若导出引用了不同文件名,需在此对照 /tmp/cc_nodes.json 的池子)。

  • Step 4: 无需提交(纯校验,无文件改动)

本任务只确认输入就绪。无 commit。

Task 2: 改造工作流(加标记 + 删 RecordAudio + 落 comfy/)

Files:

  • Create: /opt/Pixelle-Video/data/workflows/comfy/digital_ltx2.json

  • Step 1: 找到要标记的节点 id(LoadImage/LoadAudio/文本/RecordAudio)

Run:

python3 -c "
import json
wf=json.load(open('/tmp/ltx2_api.json'))
for nid,nd in wf.items():
    if not isinstance(nd,dict): continue
    ct=nd.get('class_type','')
    if ct in ('LoadImage','LoadAudio','RecordAudio','SaveVideo','PrimitiveStringMultiline','CLIPTextEncode'):
        print(nid, ct, '| inputs:', json.dumps(nd.get('inputs',{}),ensure_ascii=False)[:100])
"

Expected: 看到 LoadImage、LoadAudio、RecordAudio、SaveVideo、文本节点各自的 id。记下:

  • LoadImage 的 id(标 $~image_path!)

  • LoadAudio 的 id(标 $~audio_path!)

  • 文本提示节点 id(标 $prompt!)—— 优先 PrimitiveStringMultiline;若文本经 CLIPTextEncode 的 text 字段直接给,则标 CLIPTextEncode

  • RecordAudio 的 id(删除)

  • Step 2: 写改造脚本生成 comfy/digital_ltx2.json

把下面 IMAGE_ID/AUDIO_ID/TEXT_ID/RECORD_ID 替换为 Step 1 查到的实际 id。 文本节点字段名:若 PrimitiveStringMultiline 用 value 字段,标记写 $prompt.value!; 若 CLIPTextEncode 用 text 字段,写 $prompt.text!。按 Step 1 实际字段定。

python3 -c "
import json
wf=json.load(open('/tmp/ltx2_api.json'))

IMAGE_ID='REPLACE_IMAGE_ID'
AUDIO_ID='REPLACE_AUDIO_ID'
TEXT_ID='REPLACE_TEXT_ID'
TEXT_MARKER='\$prompt.value!'   # 或 \$prompt.text! 视字段而定
RECORD_ID='REPLACE_RECORD_ID'   # 若无 RecordAudio 留空字符串

def set_title(nid, marker):
    nd=wf[nid]
    nd.setdefault('_meta',{})['title']=marker

set_title(IMAGE_ID, '\$~image_path!')
set_title(AUDIO_ID, '\$~audio_path!')
set_title(TEXT_ID, TEXT_MARKER)

# 删 RecordAudio 并清理引用它的连线(把指向它的输入置空/删除)
if RECORD_ID and RECORD_ID in wf:
    wf.pop(RECORD_ID)
    for nid,nd in wf.items():
        if not isinstance(nd,dict): continue
        for k,v in list((nd.get('inputs') or {}).items()):
            if isinstance(v,list) and len(v)==2 and str(v[0])==RECORD_ID:
                # 该输入原本接 RecordAudio;数字人用 LoadAudio,删除这个悬空连接
                del nd['inputs'][k]

import os
os.makedirs('/opt/Pixelle-Video/data/workflows/comfy', exist_ok=True)
json.dump(wf, open('/opt/Pixelle-Video/data/workflows/comfy/digital_ltx2.json','w'), ensure_ascii=False, indent=2)
print('written, 节点数:', len(wf))
# 校验标记落位
for nid in [IMAGE_ID,AUDIO_ID,TEXT_ID]:
    print(nid, '->', wf.get(nid,{}).get('_meta',{}).get('title'))
"

Expected: 写入成功,三个标记落位正确,RecordAudio 已删。

⚠️ RecordAudio 与 LoadAudio 的关系:数字人用 LoadAudio 输入旁白。若导出的图里音频源是 RecordAudio 而非 LoadAudio(录音节点直接接到音频处理),则不能简单删 RecordAudio——要把音频处理节点的输入改接到 LoadAudio。Step 1 查清音频实际从哪个节点流出;若是 RecordAudio 流出,本步改为:保留一个 LoadAudio(标 $~audio_path!)并把下游音频输入接到 LoadAudio 的输出。具体连接以 Step 1 的图为准。

  • Step 3: 校验生成的 JSON 合法 + comfykit 能解析出参数

Run(容器内,用 comfykit 解析):

rtk proxy docker exec pixelle-video-api /app/.venv/bin/python -c "
import json
from comfykit.comfyui.workflow_parser import WorkflowParser
wf=json.load(open('/app/data/workflows/comfy/digital_ltx2.json'))
p=WorkflowParser()
params=[]
for nid,nd in wf.items():
    if not isinstance(nd,dict): continue
    pl,ml,om=p.parse_node(nid,nd)
    for x in pl: params.append((x.name,x.required,x.need_upload))
print('解析出的参数:', params)
"

Expected: 看到 image_path(upload=True)、audio_path(upload=True)、prompt 三个参数。

  • Step 4: Commit(工作流文件在 Pixelle 宿主目录,非 BoosAPI 仓库——仅记录到计划备注)

工作流文件不在 git 仓库内。无 git commit;在此记录文件已落 /opt/Pixelle-Video/data/workflows/comfy/digital_ltx2.json。

Task 3: 补 media.py 转发 image_path 到 comfy 通道

Files:

  • Modify(容器内):/app/pixelle_video/services/media.py

注:pixelle_video 源码在镜像内置,不在挂载卷。容器内改 docker compose restart 能留, 但 docker compose down && up/重建镜像会丢。持久化方案见 Step 4。

  • Step 1: 定位 workflow_params 构建处

media.py 的 __call__ 里,api/ 分支之后、workflow_params = {"prompt": prompt} 起的那段 (约 line 232)。image_path 是 __call__ 的具名参数但没进 workflow_params。

Run: rtk proxy docker exec pixelle-video-api sh -lc "grep -n 'workflow_params = {\"prompt\"' /app/pixelle_video/services/media.py" Expected: 定位到该行号(约 232)。

  • Step 2: 在 workflow_params 构建处加入 image_path

在 workflow_params = {"prompt": prompt} 之后插入(用 sed 或 python 改文件)。注意这是容器内文件, 用 docker exec ... python 做精确插入最稳妥:

rtk proxy docker exec pixelle-video-api /app/.venv/bin/python -c "
p='/app/pixelle_video/services/media.py'
src=open(p).read()
anchor='workflow_params = {\"prompt\": prompt}'
assert anchor in src, 'anchor not found'
add='''workflow_params = {\"prompt\": prompt}
        if image_path is not None:
            workflow_params[\"image_path\"] = image_path'''
if 'workflow_params[\"image_path\"]' in src:
    print('already patched')
else:
    src=src.replace(anchor, add, 1)
    open(p,'w').write(src)
    print('patched')
"

Expected: patched(或 already patched)。

  • Step 3: 校验语法 + 重启容器
rtk proxy docker exec pixelle-video-api /app/.venv/bin/python -c "import ast; ast.parse(open('/app/pixelle_video/services/media.py').read()); print('syntax OK')"
cd /opt/Pixelle-Video && rtk proxy docker compose restart api && sleep 7 && rtk proxy docker ps --filter name=pixelle --format '{{.Names}} {{.Status}}'

Expected: syntax OK;容器 Up ... (healthy)。

  • Step 4: 持久化方案(防镜像重建丢失)

把改动备份到宿主,并记录到 Pixelle 部署说明:

rtk proxy docker cp pixelle-video-api:/app/pixelle_video/services/media.py /opt/Pixelle-Video/patches/media.py.patched 2>/dev/null || (mkdir -p /opt/Pixelle-Video/patches && rtk proxy docker cp pixelle-video-api:/app/pixelle_video/services/media.py /opt/Pixelle-Video/patches/media.py.patched)
echo "已备份改后 media.py 到 /opt/Pixelle-Video/patches/;重建镜像后需重新 docker cp 回 /app/pixelle_video/services/media.py 或固化进 Dockerfile"

Expected: 备份文件存在。无 git commit(Pixelle 非本仓库)。

Task 4: 校验探测(确认 Comfy Cloud 接受工作流)

Files:

  • 临时:/tmp/digital_ltx2_probe.json

  • Step 1: 用占位输入构建 probe payload

注入标记的字段需要具体值才能提交校验。用占位文件名探测(校验在执行前,不真正读文件):

python3 -c "
import json
wf=json.load(open('/opt/Pixelle-Video/data/workflows/comfy/digital_ltx2.json'))
# probe 直接提交节点图本身(标记字段已有默认值或占位);comfykit 注入是运行时的事,
# 这里验证 Comfy Cloud 是否接受这套节点+模型
json.dump({'prompt':wf}, open('/tmp/digital_ltx2_probe.json','w'), ensure_ascii=False)
print('probe payload bytes:', len(open('/tmp/digital_ltx2_probe.json').read()))
"

Expected: payload 生成。

注:工作流里 LoadImage/LoadAudio 的默认 widget 值(模板自带的示例文件名)可能在 Comfy Cloud 不存在,导致校验报"文件不存在"。这是预期且可接受的——它证明节点+模型校验已过,只差真实输入。 关注点是有没有 unsupported node type / 模型缺失类错误。

  • Step 2: 提交探测,读校验结果
curl -s -w "\nHTTP %{http_code}\n" -X POST http://127.0.0.1:4000/comfy-cloud/prompt -H "Content-Type: application/json" --data @/tmp/digital_ltx2_probe.json 2>&1 | head -15

Expected 判读:

  • {"node_errors":{},"prompt_id":"..."} HTTP 200 → 完美,全兼容。

  • 400 ... unsupported node type 'X' → 节点 X 不在 Comfy Cloud,需在工作流里替换/移除该节点(回 Task 2 处理)。

  • 400 ... 文件/模型 不存在(针对 LoadImage 示例图或某 .safetensors)→ 若是输入文件(LoadImage 默认图)属预期;若是 .safetensors 模型缺失,需换 Comfy Cloud 有的等价模型(对照 /tmp/cc_nodes.json 池)。

  • Step 3: 据结果迭代或放行

  • 全兼容 → 进 Task 5。

  • 节点/模型问题 → 回 Task 2 调整工作流(换节点或模型名),重跑 Task 4。不要跳过。

Task 5: 真实数字人任务联调

Files: 无(运行时验证)

  • Step 1: 在 Pixelle 前端发起数字人任务,视频/动画工作流选 comfy/digital_ltx2.json

数字人流程的 image→video 动画步(api_video_workflow 或对应 UI 项)填/选 comfy/digital_ltx2.json。 触发一个任务(给个肖像图 + 文本)。

  • Step 2: 观察任务流转

预期:LLM 脚本步 → 生成肖像图(comfy 图片工作流)+ TTS 音频 → digital_ltx2 动画步: 注入 image_path/audio_path/prompt → Comfy Cloud 建 job → 轮询 completed → 取回说话视频 → HTML 合成。

监控 BoosAPI 侧 proxy_logs:

rtk proxy sqlite3 boosapi.db "SELECT id, created_at, client_family, http_status, substr(error_message,1,80) FROM proxy_logs ORDER BY id DESC LIMIT 5;"

Expected: 新日志(id 增长),数字人任务最终 success / 出片。

  • Step 3: 出错则按错误类型归因

  • unsupported node / 模型缺失 → 回 Task 2/4。

  • image_path 未注入(LoadImage 仍用默认图)→ 回 Task 3 确认 media.py 改动生效(grep image_path /app/.../media.py)+ Task 2 确认 $~image_path! 标记落位。

  • 执行期失败(Comfy Cloud job failed)→ 真实生成问题(参数/分辨率/时长),据 job 错误调工作流默认值。

  • Step 4: 成功标准

数字人任务在 Comfy Cloud 出现真实 job、跑完、返回带音频的说话视频,Pixelle 合成出片。


完成标准

  • comfy/digital_ltx2.json 落地、comfykit 能解析出 image_path/audio_path/prompt 三参数。
  • media.py 已转发 image_path 到 comfy 通道(并备份到宿主 patches/)。
  • 校验探测确认 Comfy Cloud 接受工作流(节点+模型兼容)。
  • 真实数字人任务经 comfy/Comfy Cloud 出片。

已知风险 / 备注

  • 硬依赖用户导出 API JSON(Task 1);拿不到则全卡。
  • media.py 改动在镜像内置层,重建镜像会丢,已备份到 /opt/Pixelle-Video/patches/,长期应固化进 Dockerfile/启动脚本。
  • 工作流默认 widget 值(示例图/音频文件名)在 Comfy Cloud 不存在属正常,真实运行靠注入覆盖。
  • 单人肖像;多人对话不在范围。