| from __future__ import annotations |
|
|
| import asyncio |
| import glob |
| import json |
| import os |
| import pathlib |
| import random |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import traceback |
| import uuid |
| from typing import Any |
|
|
| import base64 |
| import threading |
| import time |
|
|
| |
| os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" |
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" |
| os.environ["OMP_NUM_THREADS"] = "4" |
| os.environ["MKL_NUM_THREADS"] = "4" |
|
|
| import gradio as gr |
| import requests as http_requests |
| import spaces |
| import torch |
| from huggingface_hub import hf_hub_download |
| from PIL import Image |
|
|
| if torch.cuda.is_available(): |
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.allow_tf32 = True |
|
|
| |
| ROOT = pathlib.Path(__file__).resolve().parent |
| COMFY = ROOT / "ComfyUI" |
| INPUT = COMFY / "input" |
| OUTPUT = COMFY / "output" |
|
|
| |
| DATA_DIR = pathlib.Path("/data") |
| try: |
| if not DATA_DIR.exists(): |
| DATA_DIR.mkdir(parents=True, exist_ok=True) |
| |
| test_dummy = DATA_DIR / f".perm_test_{os.getpid()}" |
| test_dummy.touch() |
| test_dummy.unlink() |
| except Exception: |
| print("Warning: Persistent storage volume '/data' not accessible. Falling back to local workspace storage.", flush=True) |
| DATA_DIR = ROOT / "data" |
| DATA_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| MODELS = DATA_DIR / "models" |
| ENHANCER_DIR = DATA_DIR / "prompt_enhancer" |
|
|
| |
| os.environ["LD_LIBRARY_PATH"] = f"{ENHANCER_DIR}:{os.environ.get('LD_LIBRARY_PATH', '')}" |
|
|
| WORKFLOW_REPO = "TenStrip/LTX2.3-10Eros_Workflows" |
| WORKFLOW_REVISION = "1b8e8988842a5850dbba58d732c3e29ce430c1c7" |
| WORKFLOW_FILENAME = "10Eros_10SNodes_LikenessGuideHelper_I2V_v3.2.json" |
|
|
| |
| RUNEXX_NODE_LOAD_IMAGE_REF2 = 29 |
| RUNEXX_NODE_LOAD_IMAGE_BG = 30 |
| NODE_OUTPUT = "597" |
| NODE_LOAD_IMAGE = "834" |
| NODE_POSITIVE = "536" |
| NODE_NEGATIVE = "537" |
| NODE_SEED = "524" |
| NODE_WIDTH = "791" |
| NODE_HEIGHT = "792" |
| NODE_LENGTH = "796" |
|
|
| CUSTOM_NODES = [ |
| ("ComfyUI-GGUF", "https://github.com/city96/ComfyUI-GGUF.git"), |
| ("ComfyUI-LTXVideo", "https://github.com/Lightricks/ComfyUI-LTXVideo.git"), |
| ("10S-Comfy-nodes", "https://github.com/TenStrip/10S-Comfy-nodes.git"), |
| ("ComfyUI-KJNodes", "https://github.com/kijai/ComfyUI-KJNodes.git"), |
| ("rgthree-comfy", "https://github.com/rgthree/rgthree-comfy.git"), |
| ("ComfyUI-VideoHelperSuite", "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite.git"), |
| ("RES4LYF", "https://github.com/ClownsharkBatwing/RES4LYF.git"), |
| ("ComfyUI-Easy-Use", "https://github.com/yolain/ComfyUI-Easy-Use.git"), |
| ("ComfyUI-mxToolkit", "https://github.com/Smirnov75/ComfyUI-mxToolkit.git"), |
| ("ComfyMath", "https://github.com/evanspearman/ComfyMath.git"), |
| ("ComfyUI-Licon-MSR", "https://github.com/liconstudio/ComfyUI-Licon-MSR.git"), |
| ("ComfyUI-RMBG", "https://github.com/1038lab/ComfyUI-RMBG.git"), |
| ("ComfyUI-PromptRelay", "https://github.com/kijai/ComfyUI-PromptRelay.git"), |
| ("ComfyUI-FunPack", "https://github.com/digital-garbage/ComfyUI-FunPack.git"), |
| ("ComfyUI-MultiLoRALoader", "https://github.com/phazei/ComfyUI-MultiLoRALoader.git"), |
| ] |
|
|
| |
| DOWNLOADS = [ |
| {"repo": "TenStrip/LTX2.3-10Eros", "file": "10Eros_v1-fp8mixed_learned.safetensors", "dest": MODELS / "checkpoints" / "10Eros_v1-fp8mixed_learned.safetensors"}, |
| {"repo": "Comfy-Org/ltx-2", "file": "split_files/text_encoders/gemma_3_12B_it_fp8_scaled.safetensors", "dest": MODELS / "text_encoders" / "gemma_3_12B_it_fp8_scaled.safetensors"}, |
| {"repo": "TenStrip/LTX2.3_Distilled_Lora_1.1_Experiments", "file": "ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors", "dest": MODELS / "loras" / "ltx23" / "ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors"}, |
| {"repo": "VasiliyWeb/OmniNFT_ComfyUI", "file": "OmniNFT_converted_lora.safetensors", "dest": MODELS / "loras" / "ltx23" / "OmniNFT_converted_lora.safetensors"}, |
| {"repo": "LiconStudio/LTX-2.3-Multiple-Subject-Reference", "file": "LTX2.3-Licon-MSR-test_version.safetensors", "dest": MODELS / "loras" / "ltx23" / "LTX2.3-Licon-MSR-test_version.safetensors"}, |
| {"repo": "Lightricks/LTX-2.3", "file": "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", "dest": MODELS / "latent_upscale_models" / "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"}, |
| {"repo": "Kijai/MelBandRoFormer_comfy", "file": "MelBandRoformer_fp16.safetensors", "dest": MODELS / "diffusion_models" / "MelBandRoformer_fp16.safetensors"}, |
| |
| {"repo": "signsur4739379373/ltx-dependencies", "file": "llama-server", "dest": ENHANCER_DIR / "llama-server"}, |
| {"repo": "signsur4739379373/ltx-dependencies", "file": "llama-server-libs.tar.gz", "dest": ENHANCER_DIR / "llama-server-libs.tar.gz"}, |
| {"repo": "signsur4739379373/ltx-dependencies", "file": "prompt_enhancer/sulphur_prompt_enhancer_model-q8_0.gguf", "dest": ENHANCER_DIR / "sulphur_prompt_enhancer_model-q8_0.gguf"} |
| ] |
|
|
| INPUT_MODES = ["anchor only", "multi-reference (MSR)", "multi-reference (original)"] |
| PRESETS = ["original", "tuned", "high-fidelity tryon"] |
| DEFAULT_NEGATIVE = "captions, bad quality, static, low quality, noise, mutant, blur, text, watermark" |
|
|
| _comfy_ready = False |
| _enhancer_ready = False |
|
|
| def init_services(): |
| global _comfy_ready, _enhancer_ready |
| |
| |
| if not COMFY.exists(): |
| subprocess.run(["git", "clone", "https://github.com/comfyanonymous/ComfyUI.git", str(COMFY)], check=True) |
| |
| for name, repo in CUSTOM_NODES: |
| node_dir = COMFY / "custom_nodes" / name |
| if not node_dir.exists(): |
| subprocess.run(["git", "clone", repo, str(node_dir)], check=True) |
|
|
| |
| for dl in DOWNLOADS: |
| dl["dest"].parent.mkdir(parents=True, exist_ok=True) |
| if not dl["dest"].exists(): |
| print(f"Downloading asset to storage bucket: {dl['dest'].name}...", flush=True) |
| |
| try: |
| hf_hub_download(repo_id=dl["repo"], filename=dl["file"], local_dir=dl["dest"].parent) |
| |
| basename = os.path.basename(dl["file"]) |
| if (dl["dest"].parent / basename).exists() and (dl["dest"].parent / basename) != dl["dest"]: |
| shutil.move(str(dl["dest"].parent / basename), str(dl["dest"])) |
| except Exception as download_error: |
| print(f"Error skipping non-critical asset download failure for {dl['file']}: {download_error}", flush=True) |
|
|
| |
| tar_file = ENHANCER_DIR / "llama-server-libs.tar.gz" |
| if tar_file.exists() and not (ENHANCER_DIR / "libggml.so").exists(): |
| print("Extracting prompt enhancer shared systems library binaries...", flush=True) |
| try: |
| subprocess.run(["tar", "-xzvf", str(tar_file), "-C", str(ENHANCER_DIR)], check=True) |
| except Exception as e: |
| print(f"Failed parsing enhancer libraries: {e}", flush=True) |
|
|
| server_bin = ENHANCER_DIR / "llama-server" |
| if server_bin.exists(): |
| os.chmod(str(server_bin), 0o755) |
|
|
| |
| if not _comfy_ready: |
| print("Starting CPU-Cached ComfyUI Node Server...", flush=True) |
| subprocess.Popen([ |
| sys.executable, str(COMFY / "main.py"), |
| "--listen", "127.0.0.1", "--port", "8188", |
| "--normalvram", "--fp8_e4m3fn-text-enc", "--fp8_e4m3fn-unet" |
| ]) |
| for _ in range(60): |
| try: |
| if http_requests.get("http://127.0.0.1:8188/object_info").status_code == 200: |
| _comfy_ready = True |
| break |
| except Exception: |
| time.sleep(1.5) |
|
|
| |
| model_file = ENHANCER_DIR / "sulphur_prompt_enhancer_model-q8_0.gguf" |
| if not _enhancer_ready and server_bin.exists() and model_file.exists(): |
| print("Starting Prompt Enhancer Text Server Local Engine...", flush=True) |
| subprocess.Popen([ |
| str(server_bin), |
| "-m", str(model_file), |
| "--port", "8080", |
| "--host", "127.0.0.1", |
| "-ngl", "0", |
| "-c", "2048" |
| ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| |
| for _ in range(45): |
| try: |
| if http_requests.get("http://127.0.0.1:8080/health").status_code == 200: |
| _enhancer_ready = True |
| break |
| except Exception: |
| time.sleep(1.5) |
|
|
| def run_prompt_enhancer(user_prompt: str) -> str: |
| init_services() |
| if not _enhancer_ready: |
| return user_prompt |
| try: |
| payload = { |
| "prompt": f"<|im_start|>system\nYou are a precise prompt expansion engine. Expand this raw visual description for high-fidelity rendering details:<|im_end|>\n<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n", |
| "n_predict": 128, |
| "temperature": 0.7 |
| } |
| res = http_requests.post("http://127.0.0.1:8080/completion", json=payload, timeout=10).json() |
| return res.get("content", user_prompt).strip() |
| except Exception: |
| return user_prompt |
|
|
| @spaces.GPU(duration=120) |
| def process_generation(prompt, neg_prompt, mode, image, ref2, ref3, ref4, background, width, height, length, seed, preset_name): |
| init_services() |
| if not _comfy_ready: return None, "Backend pipeline initiation failure." |
|
|
| workflow_path = ROOT / WORKFLOW_FILENAME |
| if not workflow_path.exists(): |
| hf_hub_download(repo_id=WORKFLOW_REPO, filename=WORKFLOW_FILENAME, local_dir=ROOT, revision=WORKFLOW_REVISION) |
|
|
| with open(workflow_path, "r", encoding="utf-8") as f: |
| graph = json.load(f) |
|
|
| INPUT.mkdir(parents=True, exist_ok=True) |
| OUTPUT.mkdir(parents=True, exist_ok=True) |
| run_uuid = str(uuid.uuid4()) |
| |
| if image: |
| image.save(INPUT / f"{run_uuid}_ref1.png") |
| graph[NODE_LOAD_IMAGE]["widgets_values"][0] = f"{run_uuid}_ref1.png" |
| if ref2 and mode != "anchor only": |
| ref2.save(INPUT / f"{run_uuid}_ref2.png") |
| if str(RUNEXX_NODE_LOAD_IMAGE_REF2) in graph: |
| graph[str(RUNEXX_NODE_LOAD_IMAGE_REF2)]["widgets_values"][0] = f"{run_uuid}_ref2.png" |
| if background and mode != "anchor only": |
| background.save(INPUT / f"{run_uuid}_bg.png") |
| if str(RUNEXX_NODE_LOAD_IMAGE_BG) in graph: |
| graph[str(RUNEXX_NODE_LOAD_IMAGE_BG)]["widgets_values"][0] = f"{run_uuid}_bg.png" |
|
|
| w_aligned = (int(width) // 32) * 32 |
| h_aligned = (int(height) // 32) * 32 |
|
|
| graph[NODE_POSITIVE]["widgets_values"][0] = prompt |
| graph[NODE_NEGATIVE]["widgets_values"][0] = neg_prompt if neg_prompt else DEFAULT_NEGATIVE |
| graph[NODE_SEED]["widgets_values"][0] = int(seed) if seed != -1 else random.randint(0, 1000000) |
| graph[NODE_WIDTH]["widgets_values"][0] = w_aligned |
| graph[NODE_HEIGHT]["widgets_values"][0] = h_aligned |
| graph[NODE_LENGTH]["widgets_values"][0] = int(length) |
|
|
| |
| for key in ["274", "535", "550", "617", "789"]: |
| if key in graph: del graph[key] |
|
|
| try: |
| res = http_requests.post("http://127.0.0.1:8188/prompt", json={"prompt": graph}).json() |
| prompt_id = res["prompt_id"] |
| except Exception as e: |
| return None, f"Execution dispatch error: {str(e)}" |
|
|
| while True: |
| try: |
| history = http_requests.get(f"http://127.0.0.1:8188/history/{prompt_id}").json() |
| if prompt_id in history: break |
| except Exception: |
| pass |
| time.sleep(0.5) |
|
|
| output_files = glob.glob(str(OUTPUT / "*.*")) |
| if output_files: |
| return max(output_files, key=os.path.getctime), "Generation Complete!" |
| |
| return None, "Pipeline completed without producing render tracks." |
|
|
| def _on_input_mode_change(m): |
| any_msr = m in ["multi-reference (MSR)", "multi-reference (original)"] |
| is_msr_ours = m == "multi-reference (MSR)" |
| return ( |
| gr.update(label="reference 1" if any_msr else "reference image"), |
| gr.update(visible=any_msr), |
| gr.update(visible=is_msr_ours), |
| gr.update(visible=is_msr_ours), |
| gr.update(visible=any_msr), |
| ) |
|
|
| with gr.Blocks() as demo: |
| gr.Markdown("### ⚡ ZeroGPU Multi-Subject Persistent High-Fidelity Studio") |
| |
| with gr.Row(): |
| with gr.Column(scale=4): |
| prompt = gr.Textbox(label="Positive Structural Script Prompt", lines=3, placeholder="Describe scene movement...") |
| enhance_btn = gr.Button("🔮 Auto-Enhance Description Prompt", variant="secondary") |
| neg_prompt = gr.Textbox(label="Negative Restrictions", value=DEFAULT_NEGATIVE, lines=2) |
| input_mode = gr.Dropdown(choices=INPUT_MODES, value="anchor only", label="Workflow Input Matrix Configuration Type") |
| preset = gr.Dropdown(choices=PRESETS, value="tuned", label="Likeness Precision Profiles") |
| |
| with gr.Row(): |
| width = gr.Slider(minimum=256, maximum=1024, value=768, step=32, label="Width") |
| height = gr.Slider(minimum=256, maximum=1024, value=512, step=32, label="Height") |
| length = gr.Slider(minimum=8, maximum=80, value=41, step=4, label="Frame Length") |
| |
| seed = gr.Number(label="Manual Seed (-1 = Random)", value=-1, precision=0) |
| generate_btn = gr.Button("Render Frame Generation Sequence", variant="primary") |
| |
| with gr.Column(scale=3): |
| image = gr.Image(type="pil", label="reference image") |
| msr_ref2 = gr.Image(type="pil", label="reference 2 (Pose/Cloth Asset)", visible=False) |
| msr_ref3 = gr.Image(type="pil", label="reference 3", visible=False) |
| msr_ref4 = gr.Image(type="pil", label="reference 4", visible=False) |
| msr_background = gr.Image(type="pil", label="target background", visible=False) |
| |
| video_output = gr.Video(label="Processed Result Generation Sequence") |
| status_text = gr.Textbox(label="System Status Log", interactive=False) |
|
|
| input_mode.change( |
| fn=_on_input_mode_change, |
| inputs=[input_mode], |
| outputs=[image, msr_ref2, msr_ref3, msr_ref4, msr_background], |
| ) |
|
|
| enhance_btn.click( |
| fn=run_prompt_enhancer, |
| inputs=[prompt], |
| outputs=[prompt] |
| ) |
|
|
| generate_btn.click( |
| fn=process_generation, |
| inputs=[prompt, neg_prompt, input_mode, image, msr_ref2, msr_ref3, msr_ref4, msr_background, width, height, length, seed, preset], |
| outputs=[video_output, status_text] |
| ) |
|
|
| if __name__ == "__main__": |
| demo.queue().launch(theme=gr.themes.Soft()) |