Update app.py
Browse files
app.py
CHANGED
|
@@ -20,7 +20,7 @@ COMFY = Path("/app/ComfyUI")
|
|
| 20 |
MODELS = COMFY / "models"
|
| 21 |
OUTPUT = COMFY / "output"
|
| 22 |
|
| 23 |
-
# ✅ Manifest สำหรับตรวจสอบว่าโมเดลครบ
|
| 24 |
DOWNLOAD_MANIFEST = [
|
| 25 |
{
|
| 26 |
"file": "10Eros_v1-Q3_K_M.gguf",
|
|
@@ -60,7 +60,6 @@ DOWNLOAD_MANIFEST = [
|
|
| 60 |
]
|
| 61 |
|
| 62 |
def _check_models(progress_cb=None):
|
| 63 |
-
"""ตรวจสอบว่าโมเดลที่ Docker ดาวน์โหลดไว้ครบหรือไม่"""
|
| 64 |
if progress_cb:
|
| 65 |
progress_cb(0.0, desc="Verifying models...")
|
| 66 |
missing = []
|
|
@@ -208,8 +207,6 @@ NODE_LABELS = {
|
|
| 208 |
"21": "Preprocessing image",
|
| 209 |
"22": "I2V conditioning",
|
| 210 |
"30": "Applying user LoRA",
|
| 211 |
-
"40": "Encoding prompt",
|
| 212 |
-
"41": "Encoding negative",
|
| 213 |
}
|
| 214 |
|
| 215 |
_comfy_proc = None
|
|
@@ -273,7 +270,6 @@ def _is_ic_lora(lora_path: str) -> bool:
|
|
| 273 |
return False
|
| 274 |
if lora_path in _ic_lora_cache:
|
| 275 |
return _ic_lora_cache[lora_path]
|
| 276 |
-
|
| 277 |
result = _detect_ic_lora(lora_path)
|
| 278 |
_ic_lora_cache[lora_path] = result
|
| 279 |
return result
|
|
@@ -281,14 +277,12 @@ def _is_ic_lora(lora_path: str) -> bool:
|
|
| 281 |
def _detect_ic_lora(lora_path: str) -> bool:
|
| 282 |
if re.search(r"ic[-_]?lora", lora_path, re.IGNORECASE):
|
| 283 |
return True
|
| 284 |
-
|
| 285 |
local = MODELS / "loras" / lora_path
|
| 286 |
if local.exists():
|
| 287 |
try:
|
| 288 |
return _check_safetensors_header(str(local))
|
| 289 |
except Exception:
|
| 290 |
return False
|
| 291 |
-
|
| 292 |
if "/" in lora_path:
|
| 293 |
parts = lora_path.split("/")
|
| 294 |
if len(parts) >= 3:
|
|
@@ -333,23 +327,33 @@ def _download_user_lora(repo_id: str, filename: str) -> str | None:
|
|
| 333 |
return None
|
| 334 |
|
| 335 |
def _build_workflow(prompt: str, steps: int, duration_sec: float, seed: int,
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
wf = json.loads(json.dumps(WORKFLOW_TEMPLATE))
|
| 340 |
wf["40"]["inputs"]["text"] = prompt
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
wf["5"]["inputs"]["length"] = frames
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
if not enable_audio:
|
| 344 |
-
|
| 345 |
-
wf.pop(n, None)
|
| 346 |
-
wf["11"]["inputs"]["latent_image"] = ["5", 0]
|
| 347 |
-
wf["13"]["inputs"]["samples"] = ["11", 0]
|
| 348 |
else:
|
| 349 |
wf["50"]["inputs"]["frames_number"] = frames
|
|
|
|
| 350 |
if vid_w and vid_h:
|
| 351 |
wf["5"]["inputs"]["width"] = vid_w
|
| 352 |
wf["5"]["inputs"]["height"] = vid_h
|
|
|
|
| 353 |
wf["8"]["inputs"]["steps"] = steps
|
| 354 |
wf["10"]["inputs"]["noise_seed"] = seed
|
| 355 |
|
|
@@ -563,7 +567,7 @@ def generate(prompt, duration_sec, steps, seed, image_path=None,
|
|
| 563 |
|
| 564 |
if progress:
|
| 565 |
progress(0.0, desc="Checking models...")
|
| 566 |
-
_check_models(progress)
|
| 567 |
|
| 568 |
if progress:
|
| 569 |
progress(0.15, desc="Starting ComfyUI...")
|
|
@@ -659,6 +663,7 @@ def generate(prompt, duration_sec, steps, seed, image_path=None,
|
|
| 659 |
print(f"[output] mp4 conversion failed: {e}, returning webp", flush=True)
|
| 660 |
out_path = out_dir / "output.webp"
|
| 661 |
shutil.copy2(result, out_path)
|
|
|
|
| 662 |
elapsed = status_lines[0].split(":")[0] if ":" in status_lines[0] else "?"
|
| 663 |
lora_info = f" | LoRA: {user_lora_file}" if user_lora_file else ""
|
| 664 |
return str(out_path), f"Done {elapsed} | {mode} | {steps} steps | {duration_sec}s | seed {int(seed)}{lora_info}"
|
|
@@ -676,17 +681,14 @@ import gradio as gr
|
|
| 676 |
import random
|
| 677 |
|
| 678 |
_all_lora_choices = []
|
| 679 |
-
|
| 680 |
_lora_state = {"mode": "search"}
|
| 681 |
|
| 682 |
def _on_lora_interact(value):
|
| 683 |
if not value or len(value) < 2:
|
| 684 |
repos = _search_hf_loras("ltx 2.3 lora")
|
| 685 |
return gr.update(choices=repos, value=None)
|
| 686 |
-
|
| 687 |
if value.endswith(".safetensors"):
|
| 688 |
return gr.update(value=value)
|
| 689 |
-
|
| 690 |
if "/" in value:
|
| 691 |
parts = value.split("/")
|
| 692 |
if len(parts) >= 2:
|
|
@@ -702,7 +704,6 @@ def _on_lora_interact(value):
|
|
| 702 |
if len(choices) == 1:
|
| 703 |
return gr.update(choices=choices, value=choices[0])
|
| 704 |
return gr.update(choices=choices, value=None)
|
| 705 |
-
|
| 706 |
repos = _search_hf_loras(value)
|
| 707 |
return gr.update(choices=repos, value=None)
|
| 708 |
|
|
@@ -760,10 +761,8 @@ with gr.Blocks(title="LTX 2.3 CPU") as demo:
|
|
| 760 |
global _all_lora_choices
|
| 761 |
selected = list(selected_values) if selected_values else []
|
| 762 |
print(f"[lora] pick: {selected}", flush=True)
|
| 763 |
-
|
| 764 |
valid = [v for v in selected if "/" in v]
|
| 765 |
search_terms = [v for v in selected if "/" not in v and v.strip()]
|
| 766 |
-
|
| 767 |
if search_terms:
|
| 768 |
query = " ".join(search_terms)
|
| 769 |
repos = _search_hf_loras(query)
|
|
@@ -781,7 +780,6 @@ with gr.Blocks(title="LTX 2.3 CPU") as demo:
|
|
| 781 |
_all_lora_choices.append(r)
|
| 782 |
print(f"[lora] search '{query}': {len(resolved)} new, {len(_all_lora_choices)} total", flush=True)
|
| 783 |
return gr.update(choices=_all_lora_choices, value=valid[:9])
|
| 784 |
-
|
| 785 |
if len(valid) > 9:
|
| 786 |
valid = valid[:9]
|
| 787 |
return gr.update(choices=_all_lora_choices, value=valid)
|
|
@@ -850,7 +848,6 @@ with gr.Blocks(title="LTX 2.3 CPU") as demo:
|
|
| 850 |
fn=_gen,
|
| 851 |
inputs=[prompt_in, image_in, lora_picker, lora_strength, audio_in, duration_in, steps_in, seed_in],
|
| 852 |
outputs=[video_out, status_out],
|
| 853 |
-
#api_name="generate",
|
| 854 |
api_name="predict",
|
| 855 |
)
|
| 856 |
gr.Button(visible=False).click(fn=health, outputs=[gr.Textbox(visible=False)], api_name="health")
|
|
|
|
| 20 |
MODELS = COMFY / "models"
|
| 21 |
OUTPUT = COMFY / "output"
|
| 22 |
|
| 23 |
+
# ✅ Manifest สำหรับตรวจสอบว่าโมเดลครบ
|
| 24 |
DOWNLOAD_MANIFEST = [
|
| 25 |
{
|
| 26 |
"file": "10Eros_v1-Q3_K_M.gguf",
|
|
|
|
| 60 |
]
|
| 61 |
|
| 62 |
def _check_models(progress_cb=None):
|
|
|
|
| 63 |
if progress_cb:
|
| 64 |
progress_cb(0.0, desc="Verifying models...")
|
| 65 |
missing = []
|
|
|
|
| 207 |
"21": "Preprocessing image",
|
| 208 |
"22": "I2V conditioning",
|
| 209 |
"30": "Applying user LoRA",
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
_comfy_proc = None
|
|
|
|
| 270 |
return False
|
| 271 |
if lora_path in _ic_lora_cache:
|
| 272 |
return _ic_lora_cache[lora_path]
|
|
|
|
| 273 |
result = _detect_ic_lora(lora_path)
|
| 274 |
_ic_lora_cache[lora_path] = result
|
| 275 |
return result
|
|
|
|
| 277 |
def _detect_ic_lora(lora_path: str) -> bool:
|
| 278 |
if re.search(r"ic[-_]?lora", lora_path, re.IGNORECASE):
|
| 279 |
return True
|
|
|
|
| 280 |
local = MODELS / "loras" / lora_path
|
| 281 |
if local.exists():
|
| 282 |
try:
|
| 283 |
return _check_safetensors_header(str(local))
|
| 284 |
except Exception:
|
| 285 |
return False
|
|
|
|
| 286 |
if "/" in lora_path:
|
| 287 |
parts = lora_path.split("/")
|
| 288 |
if len(parts) >= 3:
|
|
|
|
| 327 |
return None
|
| 328 |
|
| 329 |
def _build_workflow(prompt: str, steps: int, duration_sec: float, seed: int,
|
| 330 |
+
img_name: str | None = None, user_lora: str | None = None,
|
| 331 |
+
lora_strength: float = 0.6, vid_w: int | None = None,
|
| 332 |
+
vid_h: int | None = None, enable_audio: bool = True) -> dict:
|
| 333 |
wf = json.loads(json.dumps(WORKFLOW_TEMPLATE))
|
| 334 |
wf["40"]["inputs"]["text"] = prompt
|
| 335 |
+
|
| 336 |
+
# 🔧 FIX 1: LTX Video ต้องการจำนวนเฟรมเป็นสูตร 8 * N + 1 เสมอ (เช่น 9, 17, 25, 33, 41, 49)
|
| 337 |
+
# หากคำนวณได้ค่าอื่น ระบบจะบีบอัด Latent แล้วทำให้มิติเวลา (T) กลายเป็น 0 และเกิด Error
|
| 338 |
+
raw_frames = int(float(duration_sec) * 24) + 1
|
| 339 |
+
frames = ((raw_frames - 1) // 8) * 8 + 1
|
| 340 |
+
frames = max(9, frames) # กำหนดขั้นต่ำที่ 9 เฟรม
|
| 341 |
+
print(f"[workflow] Calculated valid frames: {frames} (for {duration_sec}s duration)", flush=True)
|
| 342 |
+
|
| 343 |
wf["5"]["inputs"]["length"] = frames
|
| 344 |
+
|
| 345 |
+
# 🔧 FIX 2: ไม่ลบ Node สร้าง Audio Latent (50, 51, 52, 53) ออกทั้งหมด
|
| 346 |
+
# เพราะโมเดล AV ต้องการโครงสร้าง Latent ที่ครบถ้วน หากขาดไปจะทำให้มิติ T=0 และ Crash
|
| 347 |
+
# แต่เราจะลบเฉพาะ Node Decode เสียง (54) ออกเพื่อประหยัดเวลา CPU ~4 ชั่วโมง
|
| 348 |
if not enable_audio:
|
| 349 |
+
wf.pop("54", None) # ลบเฉพาะส่วน Decode เสียง
|
|
|
|
|
|
|
|
|
|
| 350 |
else:
|
| 351 |
wf["50"]["inputs"]["frames_number"] = frames
|
| 352 |
+
|
| 353 |
if vid_w and vid_h:
|
| 354 |
wf["5"]["inputs"]["width"] = vid_w
|
| 355 |
wf["5"]["inputs"]["height"] = vid_h
|
| 356 |
+
|
| 357 |
wf["8"]["inputs"]["steps"] = steps
|
| 358 |
wf["10"]["inputs"]["noise_seed"] = seed
|
| 359 |
|
|
|
|
| 567 |
|
| 568 |
if progress:
|
| 569 |
progress(0.0, desc="Checking models...")
|
| 570 |
+
_check_models(progress)
|
| 571 |
|
| 572 |
if progress:
|
| 573 |
progress(0.15, desc="Starting ComfyUI...")
|
|
|
|
| 663 |
print(f"[output] mp4 conversion failed: {e}, returning webp", flush=True)
|
| 664 |
out_path = out_dir / "output.webp"
|
| 665 |
shutil.copy2(result, out_path)
|
| 666 |
+
|
| 667 |
elapsed = status_lines[0].split(":")[0] if ":" in status_lines[0] else "?"
|
| 668 |
lora_info = f" | LoRA: {user_lora_file}" if user_lora_file else ""
|
| 669 |
return str(out_path), f"Done {elapsed} | {mode} | {steps} steps | {duration_sec}s | seed {int(seed)}{lora_info}"
|
|
|
|
| 681 |
import random
|
| 682 |
|
| 683 |
_all_lora_choices = []
|
|
|
|
| 684 |
_lora_state = {"mode": "search"}
|
| 685 |
|
| 686 |
def _on_lora_interact(value):
|
| 687 |
if not value or len(value) < 2:
|
| 688 |
repos = _search_hf_loras("ltx 2.3 lora")
|
| 689 |
return gr.update(choices=repos, value=None)
|
|
|
|
| 690 |
if value.endswith(".safetensors"):
|
| 691 |
return gr.update(value=value)
|
|
|
|
| 692 |
if "/" in value:
|
| 693 |
parts = value.split("/")
|
| 694 |
if len(parts) >= 2:
|
|
|
|
| 704 |
if len(choices) == 1:
|
| 705 |
return gr.update(choices=choices, value=choices[0])
|
| 706 |
return gr.update(choices=choices, value=None)
|
|
|
|
| 707 |
repos = _search_hf_loras(value)
|
| 708 |
return gr.update(choices=repos, value=None)
|
| 709 |
|
|
|
|
| 761 |
global _all_lora_choices
|
| 762 |
selected = list(selected_values) if selected_values else []
|
| 763 |
print(f"[lora] pick: {selected}", flush=True)
|
|
|
|
| 764 |
valid = [v for v in selected if "/" in v]
|
| 765 |
search_terms = [v for v in selected if "/" not in v and v.strip()]
|
|
|
|
| 766 |
if search_terms:
|
| 767 |
query = " ".join(search_terms)
|
| 768 |
repos = _search_hf_loras(query)
|
|
|
|
| 780 |
_all_lora_choices.append(r)
|
| 781 |
print(f"[lora] search '{query}': {len(resolved)} new, {len(_all_lora_choices)} total", flush=True)
|
| 782 |
return gr.update(choices=_all_lora_choices, value=valid[:9])
|
|
|
|
| 783 |
if len(valid) > 9:
|
| 784 |
valid = valid[:9]
|
| 785 |
return gr.update(choices=_all_lora_choices, value=valid)
|
|
|
|
| 848 |
fn=_gen,
|
| 849 |
inputs=[prompt_in, image_in, lora_picker, lora_strength, audio_in, duration_in, steps_in, seed_in],
|
| 850 |
outputs=[video_out, status_out],
|
|
|
|
| 851 |
api_name="predict",
|
| 852 |
)
|
| 853 |
gr.Button(visible=False).click(fn=health, outputs=[gr.Textbox(visible=False)], api_name="health")
|