#!/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 schedule(4步) 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()