Zhuyuan Operations ce32073d07 D205 面板功能大升级+本地漫剧管线+经验自动机制+飞书资料归档
- 面板: 会话持久化(oc_sess.json)/余额实时/文件栏(搜索+MD+拖拽)/朗读(edge-tts)/工具过程实时显示/漫剧画布
- 面板: 经验自动检索注入+存经验按钮(experience.py)
- 管线: local_motion本地运镜/IMAGE-FIRST-GUIDE/LOCAL-PIPELINE-V1
- 经验: EED-EXPER-014 面板工程+管线+飞书扒取全记录
- 资料: 飞书《清欢AIGC伪真人短剧全流程》归档
2026-08-03 22:55:02 +08:00

380 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
LTX 2.3 I2V - ComfyUI API 调用脚本
直接用 ComfyUI API 提交 LTX 2.3 蒸馏版图生视频工作流
"""
import json, sys, os, time, uuid, requests
COMFY_HOST = "http://localhost:8188"
def submit_workflow(input_image="test_scene.png", prompt="A cinematic shot:", output_prefix="ltx23_output", seed=42, width=768, height=512, num_frames=25):
"""
构建并提交 LTX 2.3 蒸馏版 I2V 工作流
节点结构基于 ComfyUI 内置模板展开
"""
# 节点 ID 分配
N = {k: i for i, k in enumerate([
"ckpt_loader", "text_encoder_loader",
"load_image", "preprocess",
"clip_positive", "clip_negative", "cond_zero",
"ltxv_cond", "crop_guides",
"empty_video_latent", "empty_audio_latent",
"concat_av", "img_to_video",
"cfgguider", "ksampler", "sigmas", "noise",
"sampler_custom",
"separate_av",
"vae_decode", "audio_vae_loader", "audio_vae_decode",
"create_video", "save_video",
])}
prompt_json = {
# 1. 加载模型
str(N["ckpt_loader"]): {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "ltx-2.3-22b-dev.safetensors"}
},
# 2. 文本编码器Gemma
str(N["text_encoder_loader"]): {
"class_type": "LTXAVTextEncoderLoader",
"inputs": {
"text_encoder": "gemma_3_12B_it.safetensors",
"ckpt_name": "ltx-2.3-22b-dev.safetensors",
"device": "default"
}
},
# 3. 加载输入图
str(N["load_image"]): {
"class_type": "LoadImage",
"inputs": {"image": input_image}
},
# 4. 预处理(缩放到目标尺寸)
str(N["preprocess"]): {
"class_type": "LTXVPreprocess",
"inputs": {
"image": [str(N["load_image"]), 0],
"size": 18
}
},
# 5. 正向提示词
str(N["clip_positive"]): {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [str(N["text_encoder_loader"]), 0],
"text": prompt
}
},
# 6. 负向提示词
str(N["clip_negative"]): {
"class_type": "CLIPTextEncode",
"inputs": {
"clip": [str(N["text_encoder_loader"]), 0],
"text": "bad quality, ugly, blurry, distorted, deformed"
}
},
# 7. 归零负向条件
str(N["cond_zero"]): {
"class_type": "ConditioningZeroOut",
"inputs": {
"conditioning": [str(N["clip_negative"]), 0]
}
},
# 8. LTXV 条件处理
str(N["ltxv_cond"]): {
"class_type": "LTXVConditioning",
"inputs": {
"positive": [str(N["clip_positive"]), 0],
"negative": [str(N["cond_zero"]), 0],
"frame_rate": 25.0
}
},
# 9. 空视频 latent
str(N["empty_video_latent"]): {
"class_type": "EmptyLTXVLatentVideo",
"inputs": {
"width": width,
"height": height,
"length": num_frames,
"batch_size": 1
}
},
# 10. 空音频 latent
str(N["empty_audio_latent"]): {
"class_type": "LTXVEmptyLatentAudio",
"inputs": {
"length": num_frames,
"num_frames_per_batch": 25,
"batch_size": 1
}
},
# 11. 拼接音视频 latent
str(N["concat_av"]): {
"class_type": "LTXVConcatAVLatent",
"inputs": {
"video_latent": [str(N["empty_video_latent"]), 0],
"audio_latent": [str(N["empty_audio_latent"]), 0]
}
},
# 12. 图生视频条件in-place
str(N["img_to_video"]): {
"class_type": "LTXVImgToVideoInplace",
"inputs": {
"vae": [str(N["ckpt_loader"]), 2],
"image": [str(N["preprocess"]), 0],
"latent": [str(N["concat_av"]), 0],
"strength": 1.0,
"bypass": False
}
},
# 13. 裁剪 guide对齐 latent 尺寸)
str(N["crop_guides"]): {
"class_type": "LTXVCropGuides",
"inputs": {
"positive": [str(N["ltxv_cond"]), 0],
"negative": [str(N["ltxv_cond"]), 1],
"latent": [str(N["img_to_video"]), 0]
}
},
# 14. CFG Guider蒸馏版 CFG=1
str(N["cfgguider"]): {
"class_type": "CFGGuider",
"inputs": {
"model": [str(N["ckpt_loader"]), 0],
"positive": [str(N["crop_guides"]), 0],
"negative": [str(N["crop_guides"]), 1],
"cfg": 1.0
}
},
# 15. KSampler 选择
str(N["ksampler"]): {
"class_type": "KSamplerSelect",
"inputs": {"sampler_name": "euler"}
},
# 16. 蒸馏版 sigma schedule4步
str(N["sigmas"]): {
"class_type": "ManualSigmas",
"inputs": {"sigmas": "0.909375, 0.725, 0.421875, 0.0"}
},
# 17. 随机噪声
str(N["noise"]): {
"class_type": "RandomNoise",
"inputs": {"noise_seed": seed}
},
# 18. 自定义采样器
str(N["sampler_custom"]): {
"class_type": "SamplerCustomAdvanced",
"inputs": {
"noise": [str(N["noise"]), 0],
"guider": [str(N["cfgguider"]), 0],
"sampler": [str(N["ksampler"]), 0],
"sigmas": [str(N["sigmas"]), 0],
"latent_image": [str(N["crop_guides"]), 2]
}
},
# 19. 分离音视频 latent
str(N["separate_av"]): {
"class_type": "LTXVSeparateAVLatent",
"inputs": {
"av_latent": [str(N["sampler_custom"]), 0]
}
},
# 20. VAE 解码视频
str(N["vae_decode"]): {
"class_type": "VAEDecode",
"inputs": {
"vae": [str(N["ckpt_loader"]), 2],
"samples": [str(N["separate_av"]), 0]
}
},
# 21. 加载音频 VAE
str(N["audio_vae_loader"]): {
"class_type": "LTXVAudioVAELoader",
"inputs": {"ckpt_name": "ltx-2.3-22b-dev.safetensors"}
},
# 22. 音频 VAE 解码
str(N["audio_vae_decode"]): {
"class_type": "LTXVAudioVAEDecode",
"inputs": {
"samples": [str(N["separate_av"]), 1],
"audio_vae": [str(N["audio_vae_loader"]), 0]
}
},
# 23. 合成视频
str(N["create_video"]): {
"class_type": "CreateVideo",
"inputs": {
"images": [str(N["vae_decode"]), 0],
"audio": [str(N["audio_vae_decode"]), 0],
"fps": 25,
"frame_rate": 25,
"bit_depth": 8
}
},
# 24. 保存视频
str(N["save_video"]): {
"class_type": "SaveVideo",
"inputs": {
"video": [str(N["create_video"]), 0],
"filename_prefix": output_prefix,
"format": "mp4",
"codec": "h264"
}
},
}
return prompt_json
def queue_prompt(prompt_workflow):
"""提交工作流到 ComfyUI 并返回 prompt_id"""
payload = {"prompt": prompt_workflow, "client_id": str(uuid.uuid4())}
r = requests.post(f"{COMFY_HOST}/prompt", json=payload)
r.raise_for_status()
data = r.json()
return data.get("prompt_id"), data
def get_history(prompt_id):
"""查询 prompt 执行历史"""
r = requests.get(f"{COMFY_HOST}/history/{prompt_id}")
if r.status_code == 200:
return r.json().get(prompt_id)
return None
def wait_for_completion(prompt_id, timeout=600, check_interval=10):
"""等待 prompt 执行完成"""
start = time.time()
while time.time() - start < timeout:
history = get_history(prompt_id)
if history and history.get("status", {}).get("completed") is True:
outputs = history.get("outputs", {})
elapsed = time.time() - start
return {"status": "completed", "outputs": outputs, "elapsed": elapsed}
if history and history.get("status", {}).get("status_str") == "error":
return {"status": "error", "error": history.get("status", {}).get("error_message", "未知错误")}
# 查询队列
r = requests.get(f"{COMFY_HOST}/queue")
if r.status_code == 200:
queue_data = r.json()
r2 = requests.get(f"{COMFY_HOST}/execution/{prompt_id}")
if r2.status_code == 200:
exec_data = r2.json()
progress = exec_data.get("data", {})
print(f" 进度: {progress.get('progress', 0)*100:.0f}% ({elapsed:.0f}s)" if progress.get('progress') else f" 运行中... ({time.time()-start:.0f}s)")
time.sleep(check_interval)
return {"status": "timeout", "elapsed": time.time() - start}
def check_available_models():
"""检查 ComfyUI 可用模型"""
r = requests.get(f"{COMFY_HOST}/object_info/CheckpointLoaderSimple")
if r.status_code == 200:
info = r.json()
models = info.get("CheckpointLoaderSimple", {}).get("input", {}).get("required", {}).get("ckpt_name", [])
return models
return []
def get_queue_status():
"""获取队列状态"""
try:
r = requests.get(f"{COMFY_HOST}/queue")
if r.status_code == 200:
return r.json()
except:
pass
return {}
def clear_queue():
"""清空队列"""
try:
r = requests.post(f"{COMFY_HOST}/queue", json={"clear": True})
if r.status_code == 200:
print("✅ 队列已清空")
except:
pass
def main():
print("=" * 60)
print("🎬 LTX 2.3 I2V - ComfyUI API 测试脚本")
print("=" * 60)
# 参数
input_image = sys.argv[1] if len(sys.argv) > 1 else "test_scene.png"
prompt_text = sys.argv[2] if len(sys.argv) > 2 else "A cinematic shot of a character in a fantasy scene, epic, dramatic lighting, motion blur"
output_prefix = sys.argv[3] if len(sys.argv) > 3 else "ltx23_output"
print(f"\n📷 输入图: {input_image}")
print(f"📝 Prompt: {prompt_text[:50]}...")
print(f"💾 输出: {output_prefix}")
# 检查队列
q = get_queue_status()
running = q.get("queue_running", [])
pending = q.get("queue_pending", [])
if running:
print(f"\n⚠️ 队列中已有 {len(running)} 个任务在跑")
running_ids = [str(item[1]) if len(item) > 1 else "?" for item in running]
print(f" 运行中 prompt_id: {running_ids}")
if pending:
print(f"⚠️ 队列中 {len(pending)} 个任务待执行")
# 检查模型
models = check_available_models()
ltx_models = [m for m in models if isinstance(m, str) and "ltx" in m.lower()] + [m for m in models if isinstance(m, dict) and "ltx" in str(m).lower()]
print(f"\n📦 可用 LTX 模型: {ltx_models}")
# 构建工作流
print(f"\n🔧 构建工作流...")
workflow = submit_workflow(
input_image=input_image,
prompt=prompt_text,
output_prefix=output_prefix,
seed=42,
width=768,
height=512,
num_frames=25
)
print(f"✅ 工作流构建完成 (25个节点)")
# 提交
print(f"\n🚀 提交任务到 ComfyUI...")
try:
prompt_id, resp = queue_prompt(workflow)
print(f"✅ 任务已提交! prompt_id: {prompt_id}")
print(f" 队列中还有 {len(pending)} 个任务待执行")
except Exception as e:
print(f"❌ 提交失败: {e}")
# 打印详细错误
if hasattr(e, 'response') and e.response is not None:
print(f" 响应: {e.response.text[:500]}")
sys.exit(1)
# 等待完成
print(f"\n⏳ 等待任务完成...")
result = wait_for_completion(prompt_id, timeout=600)
if result["status"] == "completed":
print(f"\n✅ 任务完成! 耗时: {result['elapsed']:.1f}")
outputs = result["outputs"]
for node_id, node_out in outputs.items():
if "videos" in node_out:
for vid in node_out["videos"]:
print(f" 🎬 视频: {vid['filename']} ({vid.get('type','')})")
if "images" in node_out:
for img in node_out["images"]:
print(f" 🖼️ 图片: {img['filename']}")
# 查找输出文件
out_dir = "/home/ls/comfy/ComfyUI/output"
import glob
files = sorted(glob.glob(f"{out_dir}/{output_prefix}*"))
print(f"\n📁 输出文件:")
for f in files:
size = os.path.getsize(f) / 1024 / 1024
print(f" {f} ({size:.1f}MB)")
elif result["status"] == "error":
print(f"\n❌ 任务失败: {result.get('error', '未知错误')}")
else:
print(f"\n⏰ 超时 ({result['elapsed']:.0f}秒)")
if __name__ == "__main__":
main()