TLX-test / app.py
sanetium's picture
Update app.py
6288fa4 verified
Raw
History Blame Contribute Delete
15.6 kB
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
# --- PERSISTENT HARDWARE & RUNTIME ACCELERATION CONFIGS ---
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
# --- SYSTEM DIRECTORIES CONFIGURATION MAPPED TO BUCKET (/data) ---
ROOT = pathlib.Path(__file__).resolve().parent
COMFY = ROOT / "ComfyUI"
INPUT = COMFY / "input"
OUTPUT = COMFY / "output"
# Robust Storage Detection: Use persistent /data volume if available, fallback safely if not.
DATA_DIR = pathlib.Path("/data")
try:
if not DATA_DIR.exists():
DATA_DIR.mkdir(parents=True, exist_ok=True)
# Test file write to confirm permissions are active
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"
# Inject the extracted prompt enhancer engine libraries directly into the container's environment paths
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"
# Core Graph Identifiers
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"),
]
# Universal model component configuration mapping directly into persistent directory targets
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"},
# Prompt Enhancer Engines saved to permanent /data volume
{"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
# 1. Base Workspace Cloning
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)
# 2. Fault-Tolerant Sequential Direct Storage Processing
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)
# FIXED: Added try-except mapping block to survive upstream repository file deletion or 404 errors
try:
hf_hub_download(repo_id=dl["repo"], filename=dl["file"], local_dir=dl["dest"].parent)
# Normalize complex path naming maps if present
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)
# 3. Dynamic Unpacking and Execution Permissions Modification
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)
# 4. Start ComfyUI Server Interface (Optimized for fast swapping)
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)
# 5. Start Prompt Enhancer Server Process (Only if the model file successfully downloaded)
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", # Force 0 GPU layers to run efficiently on CPU background thread without consuming core GPU tokens
"-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 # Return the original prompt cleanly if the enhancer server isn't available
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)
# Fast pruning of audio nodes
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())