"""
FikoYT MVD — LTX Video REST endpoints.

ComfyUI'nin PromptServer'ina LTX video uretimi icin HTTP route'lari ekler.
Tum endpoint'ler `/fikoyt_mvd/ltx/...` prefix'ini kullanir.

register_endpoints() ana __init__.py veya nodes.py tarafindan cagrilir.
"""

from __future__ import annotations

import asyncio
import datetime
import glob
import json
import os
import re
import subprocess
import time
import traceback
from typing import Any, Dict, List, Optional
from urllib.parse import quote

from aiohttp import web

try:
    import folder_paths  # ComfyUI
except Exception:  # pragma: no cover
    folder_paths = None  # type: ignore

from .helpers import (
    LTX_FPS_DEFAULT,
    LTX_FRAME_PRESETS,
    LTX_RES_PRESETS,
    MODEL_DOWNLOAD_HINTS,
    _audio_duration_sec,
    _estimate_frames_for_duration,
    _find_ffmpeg,
    _image_dimensions_from_url_or_path,
    _list_ltx_checkpoints,
    _resolution_match,
    _run_ffmpeg,
    _safe_filename,
    _video_duration_sec,
)

# nodes.py helper'larina defansif erisim
try:
    from .. import nodes as _mvd_nodes  # type: ignore
    _ensure_project_dir = getattr(_mvd_nodes, "_ensure_project_dir", None)
    _load_project_meta = getattr(_mvd_nodes, "_load_project_meta", None)
    _save_project_meta = getattr(_mvd_nodes, "_save_project_meta", None)
    _view_url = getattr(_mvd_nodes, "_view_url", None)
    PROJECT_BASE_DIR = getattr(_mvd_nodes, "PROJECT_BASE_DIR", "")
except Exception:
    _ensure_project_dir = None
    _load_project_meta = None
    _save_project_meta = None
    _view_url = None
    PROJECT_BASE_DIR = ""


# ---------------------------------------------------------------------------
# Yardimcilar
# ---------------------------------------------------------------------------

_COMFY_BASE_URL = os.environ.get("FIKOYT_COMFY_URL", "http://127.0.0.1:8188")
_CLIENT_ID = "fikoyt_mvd_ltx"
# Full template — LatentUpscaleModelLoader içerir (upscaler GEREKLİ)
_FULL_TEMPLATE_PATHS = [
    os.path.join(os.path.dirname(__file__), "..", "workflow", "ltx_segment_full.json"),
    os.path.join(os.path.dirname(__file__), "workflow", "ltx_segment_full.json"),
]
# Simple template — upscaler yok, GUIDANCE/MOTION/STEPS placeholder'ları var
_SIMPLE_TEMPLATE_PATHS = [
    os.path.join(os.path.dirname(__file__), "..", "workflow", "ltx_segment.json"),
    os.path.join(os.path.dirname(__file__), "workflow", "ltx_segment.json"),
    os.path.join(os.path.dirname(__file__), "..", "workflow_ltx_segment.json"),
]


def _ltx_node_available() -> bool:
    """ComfyUI NODE_CLASS_MAPPINGS icinde LTX node'lari var mi?
    Hem eski (LTXVCheckpointLoader) hem yeni (LTX 2.3) isim setini kontrol eder."""
    try:
        from nodes import NODE_CLASS_MAPPINGS as _g  # type: ignore
        ltx_node_names = (
            # Eski LTX node'ları
            "LTXVCheckpointLoader", "LTXVImgToVideo", "LTXVScheduler",
            # LTX 2.3 modern node'ları (Resimden Videoya / LtxMTV workflow'larında)
            "LTXAVTextEncoderLoader", "LTXVAudioVAELoader", "LTXVImgToVideoInplace",
            "LTXVConditioning", "LTXVConcatAVLatent", "LTXVSeparateAVLatent",
            "LTXVAudioVAEEncode", "LTXVAudioVAEDecode", "LTXVPreprocess",
            "EmptyLTXVLatentVideo", "LTXVEmptyLatentAudio",
        )
        return any(k in _g for k in ltx_node_names)
    except Exception:
        pass
    return False


def _meta_for(project_dir: str) -> Dict[str, Any]:
    if _load_project_meta is None or not project_dir:
        return {}
    try:
        if os.path.isdir(project_dir):
            return _load_project_meta(project_dir) or {}
    except Exception:
        pass
    return {}


def _save_meta(project_dir: str, meta: Dict[str, Any]) -> bool:
    if _save_project_meta is None or not project_dir:
        return False
    try:
        _save_project_meta(project_dir, meta)
        return True
    except Exception as e:
        print(f"[FikoYT LTX] meta kaydedilemedi: {e}")
        return False


def _latest_project_dir() -> str:
    if not PROJECT_BASE_DIR or not os.path.isdir(PROJECT_BASE_DIR):
        return ""
    try:
        adaylar = []
        for ad in os.listdir(PROJECT_BASE_DIR):
            p = os.path.join(PROJECT_BASE_DIR, ad)
            if os.path.isdir(p) and os.path.isfile(os.path.join(p, "project.json")):
                adaylar.append((os.path.getmtime(p), p))
        adaylar.sort(reverse=True)
        return adaylar[0][1] if adaylar else ""
    except Exception:
        return ""


def _view_url_for(abs_path: str) -> str:
    if _view_url is not None:
        try:
            return _view_url(abs_path) or ""
        except Exception:
            pass
    # Fallback
    if folder_paths is None or not abs_path:
        return ""
    try:
        out_dir = folder_paths.get_output_directory()
        rel = os.path.relpath(abs_path, out_dir).replace("\\", "/")
        parts = rel.rsplit("/", 1)
        if len(parts) == 2:
            sub, fname = parts
        else:
            sub, fname = "", parts[0]
        return f"/view?filename={quote(fname)}&subfolder={quote(sub)}&type=output"
    except Exception:
        return ""


def _crop_audio_file(input_dir: str, audio_filename: str,
                     start_sec: float, dur_sec: Optional[float] = None) -> str:
    """Audio'yu ffmpeg ile start_sec'ten kırp, yeni filename döner (input/ içinde).
    start_sec ≤ 0 ise orijinal isim. Cropped dosya cached (aynı offset+dur tekrar üretilmez).

    Çıktı: cropped filename (input klasörü içinde). Hata olursa orijinal isim döner.
    """
    if not audio_filename or start_sec is None or start_sec <= 0.001:
        return audio_filename
    src = os.path.join(input_dir, audio_filename)
    if not os.path.isfile(src):
        return audio_filename
    name, ext = os.path.splitext(audio_filename)
    if not ext:
        ext = ".mp3"
    # Cached filename — start ve dur'a göre unique
    dur_tag = f"_d{int(dur_sec*1000)}" if dur_sec and dur_sec > 0 else ""
    out_name = f"{name}_off{int(start_sec * 1000)}{dur_tag}{ext}"
    out_path = os.path.join(input_dir, out_name)
    if os.path.isfile(out_path) and os.path.getsize(out_path) > 1024:
        print(f"[FikoYT LTX] Audio crop cached: {out_name}")
        return out_name
    ff = _find_ffmpeg() or "ffmpeg"
    cmd = [ff, "-y", "-ss", f"{float(start_sec):.3f}", "-i", src]
    if dur_sec and dur_sec > 0:
        cmd += ["-t", f"{float(dur_sec):.3f}"]
    # Re-encode (copy bazen seek hatası yapar)
    cmd += ["-c:a", "libmp3lame" if ext.lower() == ".mp3" else "aac",
            "-q:a", "2", out_path]
    try:
        r = subprocess.run(cmd, capture_output=True, timeout=60, text=True)
        if r.returncode != 0:
            print(f"[FikoYT LTX] Audio crop fail rc={r.returncode}: {r.stderr[:200]}")
            return audio_filename
        if os.path.isfile(out_path) and os.path.getsize(out_path) > 1024:
            print(f"[FikoYT LTX] Audio cropped: {audio_filename} → {out_name} (offset {start_sec:.2f}s)")
            return out_name
        return audio_filename
    except Exception as e:
        print(f"[FikoYT LTX] Audio crop exception: {e}")
        return audio_filename


def _ltx_upscaler_present() -> bool:
    """Disk'te LTX spatial upscaler var mı? upscale_models + checkpoints + diffusion_models tara."""
    if folder_paths is None:
        return False
    try:
        cand = []
        for folder in ("upscale_models", "checkpoints", "diffusion_models"):
            try:
                cand.extend(folder_paths.get_filename_list(folder) or [])
            except Exception:
                pass
        # "ltx" + ("upscal" | "spatial-upscaler") kombosu
        return any(
            ("ltx" in f.lower()) and (("upscal" in f.lower()) or ("spatial" in f.lower()))
            for f in cand
        )
    except Exception:
        return False


def _load_workflow_template(force_simple: bool = False) -> Optional[Dict[str, Any]]:
    """Workflow template yükle. Upscaler diskte yoksa veya force_simple ise
    simple template (ltx_segment.json) tercih edilir — full template'in
    LatentUpscaleModelLoader node'u modelsiz crash etmesin."""
    # Disk'te upscaler var mı? Yoksa simple template otomatik seçilir
    use_simple = bool(force_simple) or not _ltx_upscaler_present()
    order = (_SIMPLE_TEMPLATE_PATHS + _FULL_TEMPLATE_PATHS) if use_simple \
            else (_FULL_TEMPLATE_PATHS + _SIMPLE_TEMPLATE_PATHS)
    for p in order:
        try:
            ap = os.path.abspath(p)
            if os.path.isfile(ap):
                with open(ap, "r", encoding="utf-8") as f:
                    print(f"[FikoYT LTX] Template: {os.path.basename(ap)} "
                          f"(simple_mode={use_simple}, force={bool(force_simple)})")
                    return json.load(f)
        except Exception as e:
            print(f"[FikoYT LTX] workflow yukleme hata ({p}): {e}")
    return None


def _prepare_image_for_load(image_url_or_path: str) -> str:
    """LoadImage için uygun input/-relative path döndür.
    /view?filename=X&subfolder=Y&type=output URL'ini parse eder,
    output/temp'taki dosyayı input/fikoyt_mvd_input/'a kopyalar."""
    import urllib.parse, shutil
    try:
        import folder_paths
    except Exception:
        return image_url_or_path or ""
    s = (image_url_or_path or "").strip()
    if not s:
        return ""
    if "filename=" in s or s.startswith("/view"):
        # URL parse
        try:
            qs = urllib.parse.urlparse(s).query or s.split("?", 1)[-1]
            params = urllib.parse.parse_qs(qs)
            filename = (params.get("filename") or [""])[0]
            subfolder = (params.get("subfolder") or [""])[0]
            type_ = (params.get("type") or ["input"])[0]
            if not filename:
                return s
            # type=input → direkt subfolder/filename
            subfolder_clean = subfolder.replace("\\", "/").replace("%5C", "/").strip("/")
            if type_ == "input":
                return f"{subfolder_clean}/{filename}" if subfolder_clean else filename
            # output/temp → input'a kopyala
            src_base = (folder_paths.get_output_directory()
                        if type_ == "output"
                        else folder_paths.get_temp_directory())
            # subfolder içinde escape var olabilir
            candidate_subs = [subfolder, subfolder_clean]
            src = None
            for sub in candidate_subs:
                test = os.path.join(src_base, sub.replace("/", os.sep), filename)
                if os.path.isfile(test):
                    src = test
                    break
            if not src:
                # Belki direkt src_base'de
                test = os.path.join(src_base, filename)
                if os.path.isfile(test):
                    src = test
            if not src:
                print(f"[FikoYT LTX] Image source not found: {filename} (sub={subfolder}, type={type_})")
                return filename
            dst_subfolder = "fikoyt_mvd_input"
            dst_dir = os.path.join(folder_paths.get_input_directory(), dst_subfolder)
            os.makedirs(dst_dir, exist_ok=True)
            dst = os.path.join(dst_dir, filename)
            if not os.path.isfile(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
                shutil.copy2(src, dst)
            return f"{dst_subfolder}/{filename}"
        except Exception as e:
            print(f"[FikoYT LTX] _prepare_image_for_load hata: {e}")
            return s
    # Düz dosya adı
    return s


def _prepare_audio_for_load(audio_url_or_path: str, project_dir: str = "") -> str:
    """LoadAudio için input/-relative path döndür.
    URL, absolute path veya project-relative path verilebilir;
    çıkış dosyası input/fikoyt_mvd_audio/<filename> formatına kopyalanır."""
    import urllib.parse, shutil
    try:
        import folder_paths
    except Exception:
        return audio_url_or_path or ""
    s = (audio_url_or_path or "").strip()
    if not s:
        return ""

    def _copy_to_input(src_abs: str) -> str:
        """src_abs → input/fikoyt_mvd_audio/<filename> kopyala, relative döndür."""
        try:
            filename = os.path.basename(src_abs)
            # LoadAudio subfolder kabul etmiyor — direkt input/ köküne kopyala
            # Filename çakışması olmasın diye prefix ekle
            safe_filename = f"fikoyt_mvd_{filename}"
            dst = os.path.join(folder_paths.get_input_directory(), safe_filename)
            if not os.path.isfile(dst) or os.path.getmtime(src_abs) > os.path.getmtime(dst):
                shutil.copy2(src_abs, dst)
            return safe_filename
        except Exception as e:
            print(f"[FikoYT LTX] audio copy hata: {e}")
            return ""

    # URL formu
    if "filename=" in s or s.startswith("/view"):
        try:
            qs = urllib.parse.urlparse(s).query or s.split("?", 1)[-1]
            params = urllib.parse.parse_qs(qs)
            filename = (params.get("filename") or [""])[0]
            subfolder = (params.get("subfolder") or [""])[0].replace("\\", "/").strip("/")
            type_ = (params.get("type") or ["input"])[0]
            if not filename:
                return s
            if type_ == "input":
                # LoadAudio subfolder kabul etmiyor — subfolder varsa input/ köküne kopyala
                if subfolder:
                    src = os.path.join(folder_paths.get_input_directory(), subfolder.replace("/", os.sep), filename)
                    if os.path.isfile(src):
                        res = _copy_to_input(src)
                        return res or filename
                return filename
            src_base = (folder_paths.get_output_directory()
                        if type_ == "output"
                        else folder_paths.get_temp_directory())
            src = os.path.join(src_base, subfolder.replace("/", os.sep), filename)
            if not os.path.isfile(src):
                src = os.path.join(src_base, filename)
            if not os.path.isfile(src):
                print(f"[FikoYT LTX] Audio source not found (URL): {filename}")
                return filename
            res = _copy_to_input(src)
            return res or filename
        except Exception as e:
            print(f"[FikoYT LTX] _prepare_audio_for_load URL hata: {e}")
            return s

    # Absolute path
    if os.path.isabs(s) and os.path.isfile(s):
        res = _copy_to_input(s)
        return res or os.path.basename(s)

    # Relative path — birkaç base dizinde dene
    candidates = []
    if project_dir and os.path.isdir(project_dir):
        candidates.append(os.path.join(project_dir, s))
        # Pure filename verildiyse audio/ alt klasörünü dene
        candidates.append(os.path.join(project_dir, "audio", os.path.basename(s)))
    try:
        candidates.append(os.path.join(folder_paths.get_output_directory(), s))
    except Exception:
        pass
    try:
        candidates.append(os.path.join(folder_paths.get_input_directory(), s))
    except Exception:
        pass
    for cand in candidates:
        if os.path.isfile(cand):
            res = _copy_to_input(cand)
            if res:
                return res
    print(f"[FikoYT LTX] Audio relative path resolve edilemedi: {s} (project_dir={project_dir})")
    # Son çare — input/'ta arıyor olabilir LoadAudio
    return s


def _swap_audio_to_lipdub(workflow: Dict[str, Any], audio_filename: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    """LipDub için audio kanalı kurulumu.

    Full template (ltx_segment_full.json) zaten LoadAudio + LTXVAudioVAEEncode ile
    geliyor; bu durumda audio yoksa LTXVAudioVAEEncode → LTXVEmptyLatentAudio çevir.

    Eski template (ltx_segment.json) için: LTXVEmptyLatentAudio varsa ve audio
    geldiyse, onu LTXVAudioVAEEncode'a çevir.
    """
    audio_filename = (audio_filename or "").strip()
    params = params or {}
    frames = params.get("frames", 121)
    fps = params.get("fps", LTX_FPS_DEFAULT)

    if audio_filename:
        # Eski template uyumluluğu: 6a EmptyLatentAudio varsa swap et
        node_6a = workflow.get("6a") or {}
        if node_6a.get("class_type") == "LTXVEmptyLatentAudio":
            workflow["audio_load"] = {
                "class_type": "LoadAudio",
                "inputs": {"audio": audio_filename},
                "_meta": {"title": "Load Segment Audio"},
            }
            workflow["6a"] = {
                "class_type": "LTXVAudioVAEEncode",
                "inputs": {"audio": ["audio_load", 0], "audio_vae": ["1av", 0]},
                "_meta": {"title": "Audio VAE Encode (LipDub)"},
            }
            for nid, node in workflow.items():
                if isinstance(node, dict) and node.get("class_type") == "CreateVideo":
                    node.setdefault("inputs", {})["audio"] = ["audio_load", 0]
                    break
            print(f"[FikoYT LTX] Audio LipDub mode (legacy): {audio_filename}")
        else:
            # Full template — LoadAudio + LTXVAudioVAEEncode zaten kurulu;
            # @@AUDIO@@ placeholder _inject_workflow_params'ta dolduruluyor.
            print(f"[FikoYT LTX] Full template audio: {audio_filename}")
        return workflow

    # Audio YOK — LTXVAudioVAEEncode'u LTXVEmptyLatentAudio'ya çevir
    audio_vae_ref = None
    for nid, node in list(workflow.items()):
        if not isinstance(node, dict):
            continue
        if node.get("class_type") == "LTXVAudioVAEEncode":
            audio_vae_ref = node.get("inputs", {}).get("audio_vae")
            workflow[nid] = {
                "class_type": "LTXVEmptyLatentAudio",
                "inputs": {
                    "frames_number": frames,
                    "frame_rate": int(fps),
                    "batch_size": 1,
                    "audio_vae": audio_vae_ref or ["402", 0],
                },
                "_meta": {"title": "Empty Latent Audio (audio yok)"},
            }
            print(f"[FikoYT LTX] No audio — {nid} LTXVAudioVAEEncode → LTXVEmptyLatentAudio")
    # CreateVideo.audio yoksa LTXVAudioVAEDecode'a yönlendir (LTX kendi üretsin)
    audio_decode_ref = None
    for nid, node in workflow.items():
        if isinstance(node, dict) and node.get("class_type") == "LTXVAudioVAEDecode":
            audio_decode_ref = [nid, 0]
            break
    if audio_decode_ref:
        for nid, node in workflow.items():
            if isinstance(node, dict) and node.get("class_type") == "CreateVideo":
                inp = node.setdefault("inputs", {})
                if inp.get("audio") == ["load_audio", 0]:
                    inp["audio"] = audio_decode_ref
                break
    # load_audio node'unu kaldır
    workflow.pop("load_audio", None)
    return workflow


# Template'in default 8 LoRA chain node ID'leri (ltx_segment_full.json)
_DEFAULT_LORA_NODE_IDS = ("408", "445", "448", "447", "449", "450", "451", "446")
# Default chain'in son LoRA'sı — CFGGuider model ref'leri buraya bağlı
_DEFAULT_LORA_LAST_ID = "446"
# Checkpoint loader (model kaynağı)
_CHECKPOINT_NODE_ID = "439"


def _inject_loras(workflow: Dict[str, Any], loras_list: Optional[List[Dict[str, Any]]]) -> Dict[str, Any]:
    """Per-segment LoRA desteği — body'den gelen `loras` listesiyle template'in
    8-LoRA zincirini dinamik olarak değiştirir.

    loras_list: [{filename, strength_model, enabled?}, ...] veya None/boş
      - None/boş → template'in default 8 LoRA chain'i korunur
      - Liste → eski chain silinir, yenisi kurulur, CFGGuider ref'leri güncellenir
    """
    active = [l for l in (loras_list or []) if l and l.get("enabled", True) and l.get("filename")]
    if not active:
        return workflow  # default chain korunsun
    # Bu template standart LoRA zincirini desteklemiyorsa (ör. simple template,
    # upscaler yok) — checkpoint node'u yok → enjeksiyon bağlantısız kalır, atla.
    if _CHECKPOINT_NODE_ID not in workflow:
        print(f"[FikoYT LTX] LoRA atlandı: template'te checkpoint node '{_CHECKPOINT_NODE_ID}' yok "
              f"(muhtemelen simple template — LTX upscaler kurulu değil)")
        return workflow

    # Eski LoRA node'larını sil
    for nid in _DEFAULT_LORA_NODE_IDS:
        workflow.pop(nid, None)

    # Yeni chain kur — ilk LoRA model'i checkpoint'ten alır
    prev_ref = [_CHECKPOINT_NODE_ID, 0]
    new_last_id = _CHECKPOINT_NODE_ID
    for i, lora in enumerate(active):
        node_id = f"lora_{i}"
        try:
            strength = float(lora.get("strength_model", lora.get("strength", 1.0)))
        except Exception:
            strength = 1.0
        workflow[node_id] = {
            "class_type": "LoraLoaderModelOnly",
            "inputs": {
                "model": prev_ref,
                "lora_name": str(lora["filename"]),
                "strength_model": strength,
            },
            "_meta": {"title": f"LoRA: {lora['filename']}"},
        }
        prev_ref = [node_id, 0]
        new_last_id = node_id

    # Eski chain'i referans alan node'ları (CFGGuider vb.) yeni chain'in sonuna bağla
    old_last_ref = [_DEFAULT_LORA_LAST_ID, 0]
    new_last_ref = [new_last_id, 0]
    for nid, node in workflow.items():
        if not isinstance(node, dict):
            continue
        if nid.startswith("lora_"):
            continue
        inputs = node.get("inputs", {})
        for k, v in list(inputs.items()):
            if isinstance(v, list) and len(v) == 2 and v[0] == _DEFAULT_LORA_LAST_ID and v[1] == 0:
                inputs[k] = new_last_ref

    print(f"[FikoYT LTX] LoRA override: {len(active)} LoRA injected, last={new_last_id}")
    for l in active:
        marker = " ⭐LipDub" if "lipdub" in str(l.get("filename","")).lower() else ""
        print(f"    - {l['filename']} @ {l.get('strength_model', l.get('strength', 1.0))}{marker}")
    return workflow


def _inject_workflow_params(workflow: Dict[str, Any], params: Dict[str, Any]) -> Dict[str, Any]:
    """Sablon icindeki @@KEY@@ yer tutucularini params ile degistir.

    Sablon icindeki string degerlerde su anahtarlari arar:
      @@CHECKPOINT@@, @@WIDTH@@, @@HEIGHT@@, @@FRAMES@@, @@FPS@@,
      @@STEPS@@, @@SEED@@, @@GUIDANCE@@, @@MOTION@@,
      @@POSITIVE@@, @@NEGATIVE@@, @@IMAGE@@, @@AUDIO@@,
      @@OUTPUT_DIR@@, @@OUTPUT_PREFIX@@
    """
    wf_json = json.dumps(workflow, ensure_ascii=False)
    width_i = int(params.get("width", 768))
    height_i = int(params.get("height", 512))
    # Latent space = width/2, height/2 (Resimden Videoya: 1st pass half-res sonra LatentUpsampler 2x)
    # 32 multiple snap (LTX gerektirir)
    latent_w = max(64, (width_i // 2) // 32 * 32)
    latent_h = max(64, (height_i // 2) // 32 * 32)
    seed_v = int(params.get("seed", 0) or 0)
    mapping = {
        "@@CHECKPOINT@@": params.get("checkpoint", "ltx-2.3-22b-dev-fp8.safetensors"),
        "@@CLIP_NAME@@": params.get("clip_name", "ltx-2.3_text_projection_bf16.safetensors"),
        "@@TEXT_ENCODER@@": params.get("text_encoder", "gemma_3_12B_it_fp4_mixed.safetensors"),
        "@@VAE_NAME@@": params.get("vae_name", "ltx-2.3-22b-dev_video_vae.safetensors"),
        "@@UPSCALE_MODEL@@": params.get("upscale_model", "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"),
        "@@DISTILLED_LORA@@": params.get("distilled_lora", "ltx-2.3-22b-distilled-lora-384.safetensors"),
        "@@WIDTH@@": width_i,
        "@@HEIGHT@@": height_i,
        "@@LATENT_W@@": latent_w,
        "@@LATENT_H@@": latent_h,
        "@@FRAMES@@": params.get("frames", 121),
        "@@FPS@@": params.get("fps", LTX_FPS_DEFAULT),
        "@@FPS_INT@@": int(params.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT),
        "@@DURATION@@": float(params.get("duration", float(params.get("frames", 121)) / float(params.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT))),
        "@@AUDIO_START@@": float(params.get("audio_start", 0.0)),
        "@@STEPS@@": params.get("steps", 30),
        "@@SEED@@": seed_v,
        "@@SEED2@@": (seed_v + 1) if seed_v else 0,
        "@@GUIDANCE@@": params.get("guidance_scale", 3.0),
        "@@MOTION@@": params.get("motion_strength", 0.85),
        "@@POSITIVE@@": params.get("positive_prompt", ""),
        "@@NEGATIVE@@": params.get("negative_prompt", ""),
        "@@IMAGE@@": _prepare_image_for_load(params.get("image", "")),
        "@@AUDIO@@": params.get("audio_filename", "") or params.get("audio", ""),
        "@@OUTPUT_DIR@@": params.get("output_dir", ""),
        "@@OUTPUT_PREFIX@@": params.get("output_prefix", "ltx_out"),
    }
    for k, v in mapping.items():
        if isinstance(v, str):
            wf_json = wf_json.replace(f'"{k}"', json.dumps(v))
            wf_json = wf_json.replace(k, v)
        else:
            wf_json = wf_json.replace(f'"{k}"', json.dumps(v))
            wf_json = wf_json.replace(k, str(v))
    try:
        return json.loads(wf_json)
    except Exception as e:
        print(f"[FikoYT LTX] workflow inject hata: {e}")
        return workflow


def _validate_and_clean_workflow(workflow: Dict[str, Any]) -> Dict[str, Any]:
    """Top-level node value'ların dict olduğunu garanti et — int/str/None gibi
    hatalı value'lar varsa kaldır (ComfyUI apply_replacements crash etmesin)."""
    bad_keys = []
    for k, v in list(workflow.items()):
        if not isinstance(v, dict):
            bad_keys.append((k, type(v).__name__, repr(v)[:80]))
            del workflow[k]
            continue
        if "class_type" not in v or "inputs" not in v:
            bad_keys.append((k, "missing_class_or_inputs", repr(v)[:80]))
            del workflow[k]
    if bad_keys:
        # Debug için
        try:
            dbg_path = os.path.join(
                folder_paths.get_temp_directory() if folder_paths else os.getcwd(),
                "fikoyt_bad_workflow.json"
            )
            with open(dbg_path, "w", encoding="utf-8") as f:
                json.dump({"bad_keys": bad_keys, "remaining": workflow}, f, indent=2, default=str)
            print(f"[FikoYT LTX] BAD workflow nodes: {bad_keys} (dump: {dbg_path})")
        except Exception:
            pass
    return workflow


async def _submit_to_comfy(workflow: Dict[str, Any]) -> Dict[str, Any]:
    """ComfyUI /prompt endpoint'ine POST et."""
    import aiohttp
    payload = {"prompt": workflow, "client_id": _CLIENT_ID}
    timeout = aiohttp.ClientTimeout(total=30)
    async with aiohttp.ClientSession(timeout=timeout) as sess:
        async with sess.post(f"{_COMFY_BASE_URL}/prompt", json=payload) as resp:
            text = await resp.text()
            if resp.status >= 400:
                raise RuntimeError(f"ComfyUI /prompt hata {resp.status}: {text[:300]}")
            try:
                return json.loads(text)
            except Exception:
                raise RuntimeError(f"ComfyUI cevap JSON degil: {text[:200]}")


async def _fetch_history(prompt_id: str) -> Dict[str, Any]:
    import aiohttp
    timeout = aiohttp.ClientTimeout(total=10)
    async with aiohttp.ClientSession(timeout=timeout) as sess:
        async with sess.get(f"{_COMFY_BASE_URL}/history/{prompt_id}") as resp:
            if resp.status >= 400:
                return {}
            try:
                return await resp.json()
            except Exception:
                return {}


async def _interrupt_comfy() -> bool:
    import aiohttp
    timeout = aiohttp.ClientTimeout(total=10)
    try:
        async with aiohttp.ClientSession(timeout=timeout) as sess:
            async with sess.post(f"{_COMFY_BASE_URL}/interrupt") as resp:
                return resp.status < 400
    except Exception:
        return False


async def _cancel_prompt_id(prompt_id: str) -> Dict[str, Any]:
    """prompt_id'ye göre seçici iptal — başka job'ları etkilemez.

    Akış:
      1. /queue oku
      2. Eğer prompt_id queue_running içindeyse → /interrupt (ComfyUI tek slot çalıştırır)
      3. Eğer queue_pending içindeyse → POST /queue {"delete":[prompt_id]}
      4. Hiçbirinde yoksa → "not_found" (zaten bitti)

    Dönüş: { ok, scope: "running"|"pending"|"not_found", prompt_id }
    """
    import aiohttp
    if not prompt_id:
        return {"ok": False, "scope": "invalid", "error": "prompt_id bos"}
    timeout = aiohttp.ClientTimeout(total=10)
    try:
        async with aiohttp.ClientSession(timeout=timeout) as sess:
            async with sess.get(f"{_COMFY_BASE_URL}/queue") as r:
                if r.status >= 400:
                    return {"ok": False, "scope": "queue_read_fail",
                            "error": f"queue okunamadi HTTP {r.status}"}
                q = await r.json()
            running_ids = {str(item[1]) for item in (q.get("queue_running") or [])
                           if isinstance(item, list) and len(item) >= 2}
            pending_ids = {str(item[1]) for item in (q.get("queue_pending") or [])
                           if isinstance(item, list) and len(item) >= 2}
            if prompt_id in running_ids:
                async with sess.post(f"{_COMFY_BASE_URL}/interrupt") as ir:
                    return {"ok": ir.status < 400, "scope": "running",
                            "prompt_id": prompt_id}
            if prompt_id in pending_ids:
                async with sess.post(f"{_COMFY_BASE_URL}/queue",
                                     json={"delete": [prompt_id]}) as dr:
                    return {"ok": dr.status < 400, "scope": "pending",
                            "prompt_id": prompt_id}
            return {"ok": True, "scope": "not_found", "prompt_id": prompt_id,
                    "note": "queue'da yok — zaten bitmis veya hic baslamamis"}
    except Exception as e:
        return {"ok": False, "scope": "exception", "error": str(e),
                "prompt_id": prompt_id}


def _err(msg: str, status: int = 400, **extra) -> web.Response:
    body = {"ok": False, "error": msg}
    body.update(extra)
    return web.json_response(body, status=status)


# ---------------------------------------------------------------------------
# Endpoint kayit fonksiyonu
# ---------------------------------------------------------------------------

def register_endpoints() -> bool:
    """LTX HTTP endpoint'lerini kaydet. ComfyUI PromptServer hazir olmali."""
    try:
        from server import PromptServer  # type: ignore
        srv = getattr(PromptServer, "instance", None)
        if srv is None:
            print("[FikoYT LTX] PromptServer.instance henuz hazir degil")
            return False
    except Exception as e:
        print(f"[FikoYT LTX] PromptServer alinamadi: {e}")
        return False

    # 1) GET /fikoyt_mvd/ltx/capabilities --------------------------------------
    @srv.routes.get("/fikoyt_mvd/ltx/capabilities")
    async def _ltx_caps(request):
        try:
            ckpts = _list_ltx_checkpoints()
            # T5XXL text encoder var mı? (tokenizer içeren tam dosya gerekli)
            text_encoders = []
            text_encoder_status = "missing"
            text_encoder_note = ""
            try:
                import folder_paths
                # text_encoders + clip klasörlerini tara
                for folder in ("text_encoders", "clip"):
                    try:
                        items = folder_paths.get_filename_list(folder)
                        for f in items:
                            lf = f.lower()
                            if "gemma" in lf or "t5xxl" in lf:
                                text_encoders.append({"folder": folder, "filename": f})
                    except Exception:
                        pass
                # Gemma var mı? (LTX 2.3 için tercih edilen)
                has_gemma = any("gemma" in t["filename"].lower() for t in text_encoders)
                has_t5xxl_fp16 = any("t5xxl" in t["filename"].lower() and "fp16" in t["filename"].lower() for t in text_encoders)
                if has_gemma:
                    text_encoder_status = "ok"
                    text_encoder_note = "Gemma text encoder mevcut — LTX 2.3 için ideal (test edildi, video üretildi)."
                elif has_t5xxl_fp16:
                    text_encoder_status = "partial"
                    text_encoder_note = "T5XXL fp16 var ama LTX 2.3 Gemma kullanır. Gemma indirin."
                elif text_encoders:
                    text_encoder_status = "incomplete"
                    text_encoder_note = "Text encoder dosyası var ama LTX 2.3 uyumluluğu bilinmiyor — test edilmeli."
                else:
                    text_encoder_status = "missing"
                    text_encoder_note = (
                        "Text encoder bulunamadı. LTX 2.3 için Gemma gerekli "
                        "(gemma_3_12B_it_fp4_mixed.safetensors veya benzeri). "
                        "İndir: https://huggingface.co/Lightricks/LTX-Video "
                        "→ models/text_encoders/ klasörüne koy."
                    )
            except Exception as _te_err:
                text_encoder_note = f"Text encoder kontrol hata: {_te_err}"

            # LTX text encoder loader (özel)
            ltxav_loader_available = False
            try:
                from nodes import NODE_CLASS_MAPPINGS as _GLOBAL_M
                ltxav_loader_available = "LTXAVTextEncoderLoader" in _GLOBAL_M
            except Exception:
                pass

            return web.json_response({
                "ok": True,
                "ltx_installed": _ltx_node_available(),
                "ffmpeg": _find_ffmpeg(),
                "checkpoints": ckpts,
                "checkpoint_recommended_url": MODEL_DOWNLOAD_HINTS.get(
                    "ltx-2.3-22b-distilled-1.1.safetensors", ""
                ),
                "text_encoders": text_encoders,
                "text_encoder_status": text_encoder_status,  # "ok" | "incomplete" | "missing"
                "text_encoder_note": text_encoder_note,
                "ltxav_loader_available": ltxav_loader_available,
                "resolutions": ["auto"] + list(LTX_RES_PRESETS.keys()),
                "frame_presets": list(LTX_FRAME_PRESETS),
                "fps_default": LTX_FPS_DEFAULT,
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"capabilities hata: {e}", 500)

    # 2) GET /fikoyt_mvd/ltx/list_segments -------------------------------------
    @srv.routes.get("/fikoyt_mvd/ltx/list_segments")
    async def _ltx_list_segments(request):
        try:
            project_dir = request.rel_url.query.get("project_dir", "").strip()
            if not project_dir:
                project_dir = _latest_project_dir()
            if not project_dir or not os.path.isdir(project_dir):
                return _err("project_dir bulunamadi", 404)

            meta = _meta_for(project_dir)
            segments = meta.get("segments", []) or []
            cikti = []
            for i, seg in enumerate(segments):
                vpath = seg.get("video_path") or ""
                dur = _video_duration_sec(vpath) if vpath and os.path.isfile(vpath) else 0.0
                cikti.append({
                    "index": i,
                    "type": seg.get("type", ""),
                    "start": seg.get("start", 0),
                    "end": seg.get("end", 0),
                    "text": seg.get("text", ""),
                    "image_url": seg.get("image_url", ""),
                    "image_path": seg.get("image_path", ""),
                    "audio_url": seg.get("audio_url", ""),
                    "audio_path": seg.get("audio_path", ""),
                    "video_url": seg.get("video_url", ""),
                    "video_path": vpath,
                    "video_duration": round(dur, 2),
                    "video_variants": seg.get("video_variants", []),
                })
            return web.json_response({
                "ok": True,
                "project_dir": project_dir,
                "segments": cikti,
                "count": len(cikti),
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"list_segments hata: {e}", 500)

    # 3) POST /fikoyt_mvd/ltx/generate_segment ---------------------------------
    @srv.routes.post("/fikoyt_mvd/ltx/generate_segment")
    async def _ltx_generate_segment(request):
        try:
            data = await request.json()
        except Exception:
            return _err("gecersiz JSON body", 400)

        try:
            project_dir = (data.get("project_dir") or "").strip()
            seg_index = int(data.get("segment_index", 0))
            image_url = (data.get("image_url") or "").strip()
            audio_path = (data.get("audio_path") or "").strip()
            pos_prompt = data.get("positive_prompt") or ""
            neg_prompt = data.get("negative_prompt") or ""
            settings = data.get("settings") or {}
            # Segment süresi (start/end farkı) — frame hesabı için öncelikli
            try:
                seg_start = float(data.get("segment_start", data.get("start", 0)) or 0)
                seg_end = float(data.get("segment_end", data.get("end", 0)) or 0)
            except Exception:
                seg_start, seg_end = 0.0, 0.0
            seg_duration = max(0.0, seg_end - seg_start)
            # DEBUG log
            print(f"[FikoYT LTX] >>> generate_segment çağrıldı:")
            print(f"    segment_index: {data.get('segment_index')}")
            print(f"    segment_start/end: {seg_start} -> {seg_end} (duration: {seg_duration:.2f}s)")
            print(f"    audio_path: {audio_path!r}")
            print(f"    settings.frames: {settings.get('frames')}")
            print(f"    settings.frames_locked: {settings.get('frames_locked')}")
            print(f"    settings.fps: {settings.get('fps')}")
            print(f"    settings.steps: {settings.get('steps')}")

            # ─── Server-side defansif düzeltmeler ────────────────────────────
            # 1) Steps > 20 ise 8'e indir (LTX 2.3 distilled için 6-12 sweet spot)
            try:
                _orig_steps = int(settings.get("steps", 8) or 8)
                if _orig_steps > 20:
                    settings["steps"] = 8
                    print(f"[FikoYT LTX] CLAMP: steps {_orig_steps} -> 8 (LTX 2.3 distilled için)")
            except Exception:
                settings["steps"] = 8
            # 2) Resolution: image ile oran uyumsuzsa otomatik auto'ya çevir
            try:
                _img_dims = _image_dimensions_from_url_or_path(image_url)
                _cur_res = settings.get("resolution", "auto")
                if _img_dims and _cur_res not in ("auto", "", None) and _cur_res in LTX_RES_PRESETS:
                    _preset_wh = LTX_RES_PRESETS[_cur_res]
                    _img_ar = _img_dims[0] / max(_img_dims[1], 1)
                    _vid_ar = _preset_wh[0] / max(_preset_wh[1], 1)
                    if abs(_img_ar - _vid_ar) > 0.15:
                        settings["resolution"] = "auto"
                        print(f"[FikoYT LTX] AUTO-FIX: image {_img_dims[0]}x{_img_dims[1]} (ar={_img_ar:.2f}) "
                              f"vs preset {_cur_res} (ar={_vid_ar:.2f}) FARKLI → resolution=auto")
            except Exception as _e:
                print(f"[FikoYT LTX] resolution auto-fix hata: {_e}")

            if not project_dir or not os.path.isdir(project_dir):
                return _err("project_dir gecersiz", 404)

            # ─── Pre-flight validation (üretimde sıkıntı yaşamamak için) ───
            # 1) Image zorunlu (I2V)
            if not image_url:
                return _err(
                    "image_url boş — LTX I2V için görsel zorunlu. "
                    "Segmente bir görsel yükleyin veya üretin.",
                    400, missing_dependency="image",
                )
            if not _image_dimensions_from_url_or_path(image_url):
                return _err(
                    f"Görsel okunamadı/bulunamadı: {image_url[:200]}. "
                    "Segment görselinin var olduğundan emin olun (yükleyin veya yeniden üretin).",
                    404, missing_dependency="image_file",
                )
            # 2) Checkpoint disk'te var mı? Yoksa otomatik LTX fallback dene.
            try:
                import folder_paths as _fp_ck
                ckpt_req = settings.get("checkpoint") or "ltx-2.3-22b-distilled-1.1.safetensors"
                ckpt_list = _fp_ck.get_filename_list("checkpoints") or []
                if ckpt_req not in ckpt_list:
                    cand_ltx = [c for c in ckpt_list if "ltx" in c.lower()]
                    if not cand_ltx:
                        return _err(
                            f"LTX checkpoint bulunamadı (istenen: {ckpt_req}). "
                            "İndir: https://huggingface.co/Lightricks/LTX-Video → "
                            "models/checkpoints/ klasörüne koyun.",
                            424, missing_dependency=ckpt_req,
                        )
                    print(f"[FikoYT LTX] Checkpoint '{ckpt_req}' yok, fallback: {cand_ltx[0]}")
                    settings["checkpoint"] = cand_ltx[0]
            except Exception as _ck_err:
                print(f"[FikoYT LTX] Checkpoint kontrol hata: {_ck_err}")

            # Text encoder kontrolü — LTX 2.3 GEMMA kullanır (T5XXL değil!)
            te_name = settings.get("text_encoder") or "gemma_3_12B_it_fp4_mixed.safetensors"
            try:
                import folder_paths as _fp
                te_paths = []
                for folder in ("text_encoders", "clip"):
                    try:
                        te_paths.extend(_fp.get_filename_list(folder))
                    except Exception:
                        pass
                if te_name not in te_paths:
                    # Gemma varyantlarını ara
                    fallback = next((f for f in te_paths if "gemma" in f.lower()), None)
                    if not fallback:
                        # T5XXL fallback (deneme)
                        fallback = next((f for f in te_paths if "t5xxl" in f.lower()), None)
                    if not fallback:
                        return _err(
                            "Text encoder bulunamadı. LTX 2.3 için Gemma önerilir. "
                            "İndir: https://huggingface.co/Lightricks/LTX-Video → gemma_3_12B_it_*.safetensors "
                            "→ models/text_encoders/ klasörüne koy.",
                            424,
                            missing_dependency="gemma_3_12B_it_fp4_mixed.safetensors",
                        )
                    te_name = fallback
                    print(f"[FikoYT LTX] Tercih edilen text encoder yok, fallback: {te_name}")
                settings["text_encoder"] = te_name
            except Exception as _te_err:
                print(f"[FikoYT LTX] Text encoder kontrol hata: {_te_err}")

            # VAE kontrolü — simple template @@VAE_NAME@@ kullanır
            vae_name = settings.get("vae_name") or "ltx-2.3-22b-dev_video_vae.safetensors"
            try:
                import folder_paths as _fpv
                vae_list = _fpv.get_filename_list("vae") or []
                if vae_name not in vae_list:
                    fallback = next((f for f in vae_list if "ltx" in f.lower() and "video" in f.lower()), None) \
                        or next((f for f in vae_list if "ltx" in f.lower()), None)
                    if not fallback:
                        return _err(
                            f"LTX VAE bulunamadı (istenen: {vae_name}). "
                            "İndir: ltx-2.3-22b-dev_video_vae.safetensors → models/vae/",
                            424, missing_dependency=vae_name,
                        )
                    vae_name = fallback
                    print(f"[FikoYT LTX] VAE fallback: {vae_name}")
                settings["vae_name"] = vae_name
            except Exception as _v_err:
                print(f"[FikoYT LTX] VAE kontrol hata: {_v_err}")

            # Cozunurluk — "auto" / preset / "WxH_custom" / "custom" (manuel)
            res_key = settings.get("resolution", "auto")
            target_long = int(settings.get("target_long_edge", 768) or 768)
            default_preset = LTX_RES_PRESETS["768x512"]
            # Custom format: "768x512_custom" veya "custom" + settings.custom_width/custom_height
            custom_w = settings.get("custom_width")
            custom_h = settings.get("custom_height")
            is_custom = False
            if isinstance(res_key, str) and res_key.endswith("_custom"):
                m = re.match(r"^(\d+)x(\d+)_custom$", res_key)
                if m:
                    custom_w, custom_h = int(m.group(1)), int(m.group(2))
                    is_custom = True
            elif res_key == "custom" and custom_w and custom_h:
                is_custom = True
            if is_custom:
                # 32 katına snap, min 64 max 16384
                def _snap32(v): return max(64, min(16384, round(int(v) / 32) * 32))
                width = _snap32(custom_w)
                height = _snap32(custom_h)
                preset = (width, height, default_preset[2])
                print(f"[FikoYT LTX] CUSTOM Resolution: {custom_w}x{custom_h} -> snapped {width}x{height}")
                use_auto = False
            else:
                use_auto = (res_key == "auto") or (not res_key) or (res_key not in LTX_RES_PRESETS)
            if not is_custom and use_auto:
                dims = _image_dimensions_from_url_or_path(image_url)
                if dims:
                    width, height = _resolution_match(dims[0], dims[1], target_long_edge=target_long)
                    preset = (width, height, default_preset[2])
                    aspect = dims[0] / dims[1] if dims[1] else 0
                    print(f"[FikoYT LTX] AUTO Resolution: image {dims[0]}x{dims[1]} "
                          f"-> video {width}x{height} (aspect {aspect:.3f}, target_long={target_long})")
                else:
                    preset = default_preset
                    width, height = preset[0], preset[1]
                    print(f"[FikoYT LTX] WARN: image dims okunamadı, fallback {width}x{height} — "
                          f"image_url={(image_url or '')[:120]}")
            elif not is_custom:
                preset = LTX_RES_PRESETS[res_key]
                width, height = preset[0], preset[1]
                # Manuel preset image oranıyla uyumlu mu? Uyarı ver.
                dims = _image_dimensions_from_url_or_path(image_url)
                if dims and dims[1]:
                    img_ar = dims[0] / dims[1]
                    vid_ar = width / height if height else 0
                    if vid_ar and abs(img_ar - vid_ar) > 0.05:
                        print(f"[FikoYT LTX] WARN MANUEL: image {dims[0]}x{dims[1]} (ar={img_ar:.3f}) ile "
                              f"video {width}x{height} (ar={vid_ar:.3f}) FARKLI oranda — LTX merkez crop yapabilir, "
                              f"yüzler kayabilir. 'auto' modunu öneririz.")
                print(f"[FikoYT LTX] Manual Resolution: {res_key} -> {width}x{height}")

            # Frames belirleme — ÖNCELİK SIRASI:
            #   1) settings.frames_locked + settings.frames → manuel kilitli
            #   2) segment start/end farkı → segment süresi
            #   3) audio dosyası süresi (ffprobe/soundfile)
            #   4) settings.frames (kilit yok ama set'liyse)
            #   5) preset default
            fps_i = int(settings.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT)
            manual_frames = settings.get("frames")
            frames_locked = bool(settings.get("frames_locked", False))

            if manual_frames and frames_locked:
                frames = int(manual_frames)
                print(f"[FikoYT LTX] Frames kilitli (manuel): {frames}")
            elif seg_duration > 0.5:
                frames = _estimate_frames_for_duration(seg_duration, fps_i)
                print(f"[FikoYT LTX] Segment süresi {seg_duration:.2f}s -> {frames} frames @ {fps_i}fps")
            else:
                # Audio path — absolute / project-relative / input-relative çöz
                abs_audio = audio_path
                if abs_audio and not os.path.isabs(abs_audio):
                    candidates = [
                        os.path.join(project_dir, abs_audio),
                        os.path.join(project_dir, "audio", abs_audio),
                    ]
                    if folder_paths is not None:
                        try:
                            candidates.append(
                                os.path.join(folder_paths.get_input_directory(), abs_audio)
                            )
                        except Exception:
                            pass
                    for c in candidates:
                        if os.path.isfile(c):
                            abs_audio = c
                            break
                ses_sn = _audio_duration_sec(abs_audio) if abs_audio and os.path.isfile(abs_audio) else 0.0
                if ses_sn > 0:
                    frames = _estimate_frames_for_duration(ses_sn, fps_i)
                    print(f"[FikoYT LTX] Audio {ses_sn:.2f}s -> {frames} frames @ {fps_i}fps")
                elif manual_frames:
                    frames = int(manual_frames)
                    print(f"[FikoYT LTX] Audio okunamadı, manuel frames: {frames}")
                else:
                    frames = preset[2]
                    print(f"[FikoYT LTX] Süre bilgisi yok, preset fallback frames={frames}")

            videos_dir = os.path.join(project_dir, "videos")
            os.makedirs(videos_dir, exist_ok=True)
            # Proje-relatif output_prefix → ComfyUI output/ tabanından nested path
            slug_type = _safe_filename(settings.get('checkpoint', 'ltx'))[:16]
            seg_filename = f"seg_{seg_index:02d}_{slug_type}"
            try:
                if folder_paths is not None:
                    output_base = folder_paths.get_output_directory()
                    abs_videos = os.path.abspath(videos_dir)
                    abs_base = os.path.abspath(output_base)
                    if abs_videos.startswith(abs_base):
                        rel = os.path.relpath(abs_videos, abs_base).replace("\\", "/")
                        out_prefix = f"{rel}/{seg_filename}"
                    else:
                        out_prefix = f"fikoyt_mvd/videos/{seg_filename}"
                else:
                    out_prefix = f"fikoyt_mvd/videos/{seg_filename}"
            except Exception:
                out_prefix = f"fikoyt_mvd/videos/{seg_filename}"

            workflow = _load_workflow_template()
            if workflow is None:
                return _err(
                    "workflow/ltx_segment.json sablonu bulunamadi. "
                    "workflow/ klasoru olusturulup template eklenmeli.",
                    501,
                )

            params = {
                "checkpoint": settings.get("checkpoint", "ltx-2.3-22b-distilled-1.1.safetensors"),
                "text_encoder": settings.get("text_encoder") or te_name,
                "vae_name": settings.get("vae_name") or vae_name,
                "width": int(width),
                "height": int(height),
                "frames": int(frames),
                "fps": int(settings.get("fps", LTX_FPS_DEFAULT)),
                "duration": float(seg_duration if seg_duration > 0 else int(frames) / float(settings.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT)),
                "audio_start": 0.0,
                "steps": int(settings.get("steps", 8) or 8),
                "seed": int(settings.get("seed", 0)),
                "guidance_scale": float(settings.get("guidance_scale", 3.0)),
                "motion_strength": float(settings.get("motion_strength", 0.85)),
                "positive_prompt": pos_prompt,
                "negative_prompt": neg_prompt,
                "image": image_url,
                "audio": audio_path,
                "audio_filename": _prepare_audio_for_load(
                    data.get("audio_path") or data.get("audio_url") or "",
                    project_dir=project_dir,
                ),
                "output_dir": videos_dir,
                "output_prefix": out_prefix,
            }
            workflow = _inject_loras(workflow, data.get("loras") or settings.get("loras"))
            workflow = _inject_workflow_params(workflow, params)
            workflow = _swap_audio_to_lipdub(workflow, params.get("audio_filename", ""), params)
            workflow = _validate_and_clean_workflow(workflow)

            sonuc = await _submit_to_comfy(workflow)
            prompt_id = sonuc.get("prompt_id") or sonuc.get("promptId") or ""
            if not prompt_id:
                return _err(f"prompt_id alinamadi: {sonuc}", 502)

            # meta'ya gecici kayit
            meta = _meta_for(project_dir)
            jobs = meta.setdefault("ltx_jobs", {})
            jobs[prompt_id] = {
                "segment_index": seg_index,
                "prompt_id": prompt_id,
                "started_at": datetime.datetime.now().isoformat(),
                "params": {k: v for k, v in params.items() if k not in ("positive_prompt", "negative_prompt")},
            }
            _save_meta(project_dir, meta)

            return web.json_response({
                "ok": True,
                "prompt_id": prompt_id,
                "segment_index": seg_index,
                "params": params,
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"generate_segment hata: {e}", 500)

    # 4) POST /fikoyt_mvd/ltx/standalone_generate ------------------------------
    @srv.routes.post("/fikoyt_mvd/ltx/standalone_generate")
    async def _ltx_standalone_generate(request):
        try:
            data = await request.json()
        except Exception:
            return _err("gecersiz JSON body", 400)

        try:
            image_name = (data.get("image") or "").strip()
            audio_name = (data.get("audio") or "").strip()
            pos_prompt = data.get("positive_prompt") or ""
            neg_prompt = data.get("negative_prompt") or ""
            settings = data.get("settings") or {}

            if folder_paths is None:
                return _err("folder_paths kullanilamiyor", 500)

            input_dir = folder_paths.get_input_directory()
            out_dir = folder_paths.get_output_directory()

            image_path = os.path.join(input_dir, image_name) if image_name else ""
            audio_path = os.path.join(input_dir, audio_name) if audio_name else ""

            if not image_name or not os.path.isfile(image_path):
                return _err(
                    f"Resim bulunamadı: {image_name or '(boş)'}. "
                    "Önce ComfyUI input klasörüne yükleyin veya dropzone ile sürükleyin.",
                    404, missing_dependency="image_file",
                )

            # ─── Standalone pre-flight (üretimde sıkıntı yaşamamak için) ───
            # 1) Steps > 20 ise LTX 2.3 distilled için 8'e indir
            try:
                _orig_steps = int(settings.get("steps", 8) or 8)
                if _orig_steps > 20:
                    settings["steps"] = 8
                    print(f"[FikoYT LTX standalone] CLAMP: steps {_orig_steps} -> 8")
            except Exception:
                settings["steps"] = 8

            # 2) Checkpoint disk'te var mı? Yoksa otomatik LTX fallback
            try:
                ckpt_req = settings.get("checkpoint") or "ltx-2.3-22b-distilled-1.1.safetensors"
                ckpt_list = folder_paths.get_filename_list("checkpoints") or []
                if ckpt_req not in ckpt_list:
                    cand_ltx = [c for c in ckpt_list if "ltx" in c.lower()]
                    if not cand_ltx:
                        return _err(
                            f"LTX checkpoint bulunamadı (istenen: {ckpt_req}). "
                            "İndir: https://huggingface.co/Lightricks/LTX-Video → "
                            "models/checkpoints/ klasörüne koyun.",
                            424, missing_dependency=ckpt_req,
                        )
                    print(f"[FikoYT LTX standalone] Checkpoint '{ckpt_req}' yok, fallback: {cand_ltx[0]}")
                    settings["checkpoint"] = cand_ltx[0]
            except Exception as _ck_err:
                print(f"[FikoYT LTX standalone] Checkpoint kontrol hata: {_ck_err}")

            # 3) Text encoder (LTX 2.3 Gemma)
            te_name = settings.get("text_encoder") or "gemma_3_12B_it_fp4_mixed.safetensors"
            try:
                te_paths = []
                for folder in ("text_encoders", "clip"):
                    try:
                        te_paths.extend(folder_paths.get_filename_list(folder))
                    except Exception:
                        pass
                if te_name not in te_paths:
                    fallback = next((f for f in te_paths if "gemma" in f.lower()), None) \
                        or next((f for f in te_paths if "t5xxl" in f.lower()), None)
                    if not fallback:
                        return _err(
                            "Text encoder bulunamadı. LTX 2.3 için Gemma önerilir. "
                            "İndir: https://huggingface.co/Lightricks/LTX-Video → "
                            "gemma_3_12B_it_*.safetensors → models/text_encoders/",
                            424, missing_dependency="gemma_3_12B_it_fp4_mixed.safetensors",
                        )
                    te_name = fallback
                settings["text_encoder"] = te_name
            except Exception as _te_err:
                print(f"[FikoYT LTX standalone] Text encoder kontrol hata: {_te_err}")

            # 3b) VAE — simple template @@VAE_NAME@@ kullanır
            vae_name = settings.get("vae_name") or "ltx-2.3-22b-dev_video_vae.safetensors"
            try:
                vae_list = folder_paths.get_filename_list("vae") or []
                if vae_name not in vae_list:
                    fallback = next((f for f in vae_list if "ltx" in f.lower() and "video" in f.lower()), None) \
                        or next((f for f in vae_list if "ltx" in f.lower()), None)
                    if not fallback:
                        return _err(
                            f"LTX VAE bulunamadı (istenen: {vae_name}). "
                            "İndir: ltx-2.3-22b-dev_video_vae.safetensors → models/vae/",
                            424, missing_dependency=vae_name,
                        )
                    print(f"[FikoYT LTX standalone] VAE '{vae_name}' yok, fallback: {fallback}")
                    vae_name = fallback
                settings["vae_name"] = vae_name
            except Exception as _v_err:
                print(f"[FikoYT LTX standalone] VAE kontrol hata: {_v_err}")

            # 4) Audio opsiyonel — boşsa lipdub swap atlanacak (workflow bozulmasın)
            audio_exists = bool(audio_name) and os.path.isfile(audio_path)
            if audio_name and not audio_exists:
                print(f"[FikoYT LTX standalone] WARN: audio bulunamadı, sessiz devam: {audio_name}")
                audio_name = ""

            # 4b) Audio start zamanı — şarkıdan kullanıcının istediği yerden başlat
            # (müzik klibi için kritik). ffmpeg ile crop edilmiş yeni dosya oluşturulur.
            audio_start_sec = 0.0
            try:
                audio_start_sec = float(
                    data.get("audio_start_sec")
                    or settings.get("audio_start_sec")
                    or settings.get("audio_start")
                    or 0
                )
            except Exception:
                audio_start_sec = 0.0
            if audio_name and audio_start_sec > 0.01:
                # Video süresi kadar crop et (frame/fps), audio video'dan kısa kalmasın
                fps_pre = int(settings.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT)
                frames_pre = settings.get("frames") or 0
                dur_target = (float(frames_pre) / fps_pre) if frames_pre else None
                cropped = _crop_audio_file(input_dir, audio_name, audio_start_sec, dur_target)
                if cropped and cropped != audio_name:
                    audio_name = cropped
                    audio_path = os.path.join(input_dir, audio_name)

            res_key = settings.get("resolution", "auto")
            target_long = int(settings.get("target_long_edge", 768) or 768)
            default_preset = LTX_RES_PRESETS["768x512"]
            if res_key == "auto" or not res_key or res_key not in LTX_RES_PRESETS:
                dims = _image_dimensions_from_url_or_path(image_path)
                if dims:
                    width, height = _resolution_match(dims[0], dims[1], target_long_edge=target_long)
                    preset = (width, height, default_preset[2])
                    aspect = dims[0] / dims[1] if dims[1] else 0
                    print(f"[FikoYT LTX standalone] AUTO Resolution: image {dims[0]}x{dims[1]} "
                          f"-> video {width}x{height} (aspect {aspect:.3f})")
                else:
                    preset = default_preset
                    width, height = preset[0], preset[1]
                    print(f"[FikoYT LTX standalone] WARN: image dims okunamadı, fallback {width}x{height}")
            else:
                preset = LTX_RES_PRESETS[res_key]
                width, height = preset[0], preset[1]
                print(f"[FikoYT LTX standalone] Manual Resolution: {res_key} -> {width}x{height}")

            fps_i = int(settings.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT)
            frames_in = settings.get("frames")
            if frames_in:
                # Frontend frame'i 8k+1 kuralına uymuyorsa snap et (LTX zorunlu)
                fi = int(frames_in)
                if (fi - 1) % 8 != 0 or fi < 49:
                    frames = _estimate_frames_for_duration(float(fi) / fps_i, fps_i)
                    print(f"[FikoYT LTX standalone] frames {fi} → {frames} (8k+1 snap)")
                else:
                    frames = fi
            else:
                ses_sn = _audio_duration_sec(audio_path) if (audio_name and os.path.isfile(audio_path)) else 0.0
                if ses_sn > 0:
                    frames = _estimate_frames_for_duration(ses_sn, fps_i)
                else:
                    frames = preset[2]

            stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
            standalone_dir = os.path.join(out_dir, "fikoyt_mvd_video", "standalone")
            os.makedirs(standalone_dir, exist_ok=True)
            out_prefix = f"standalone_{stamp}"
            out_filename = f"{out_prefix}.mp4"

            workflow = _load_workflow_template()
            if workflow is None:
                return _err("workflow/ltx_segment.json sablonu bulunamadi", 501)

            params = {
                "checkpoint": settings.get("checkpoint", "ltx-2.3-22b-distilled-1.1.safetensors"),
                "text_encoder": settings.get("text_encoder") or te_name,
                "vae_name": settings.get("vae_name") or vae_name,
                "width": int(width),
                "height": int(height),
                "frames": int(frames),
                "fps": int(settings.get("fps", LTX_FPS_DEFAULT)),
                "duration": float(int(frames) / float(settings.get("fps", LTX_FPS_DEFAULT) or LTX_FPS_DEFAULT)),
                "audio_start": 0.0,
                "steps": int(settings.get("steps", 8) or 8),
                "seed": int(settings.get("seed", 0)),
                "guidance_scale": float(settings.get("guidance_scale", 3.0)),
                "motion_strength": float(settings.get("motion_strength", 0.85)),
                "positive_prompt": pos_prompt,
                "negative_prompt": neg_prompt,
                "image": image_name,
                "audio": audio_name,
                "audio_filename": audio_name,
                "output_dir": standalone_dir,
                "output_prefix": out_prefix,
            }
            workflow = _inject_loras(workflow, data.get("loras") or settings.get("loras"))
            workflow = _inject_workflow_params(workflow, params)
            # Helper audio empty durumunda workflow'u sessiz pipeline'a çevirir
            # (LTXVAudioVAEEncode → LTXVEmptyLatentAudio). Her durumda çağrılmalı.
            workflow = _swap_audio_to_lipdub(workflow, audio_name, params)
            workflow = _validate_and_clean_workflow(workflow)

            sonuc = await _submit_to_comfy(workflow)
            prompt_id = sonuc.get("prompt_id") or sonuc.get("promptId") or ""
            if not prompt_id:
                return _err(f"prompt_id alinamadi: {sonuc}", 502)

            return web.json_response({
                "ok": True,
                "prompt_id": prompt_id,
                "out_filename": out_filename,
                "out_dir": standalone_dir,
                "params": params,
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"standalone_generate hata: {e}", 500)

    # 5) GET /fikoyt_mvd/ltx/job_status ----------------------------------------
    @srv.routes.get("/fikoyt_mvd/ltx/job_status")
    async def _ltx_job_status(request):
        prompt_id = request.rel_url.query.get("prompt_id", "").strip()
        if not prompt_id:
            return _err("prompt_id gerekli", 400)
        try:
            hist = await _fetch_history(prompt_id)
            if not hist or prompt_id not in hist:
                return web.json_response({
                    "ok": True, "status": "pending", "prompt_id": prompt_id,
                })

            kayit = hist.get(prompt_id, {}) or {}
            status_obj = kayit.get("status", {}) or {}
            outputs = kayit.get("outputs", {}) or {}

            # Hata kontrolu
            status_str = str(status_obj.get("status_str", "")).lower()
            completed = bool(status_obj.get("completed", False))

            # Video ciktilarini topla
            video_url = ""
            video_path = ""
            video_filename = ""
            video_subfolder = ""
            for node_id, node_out in outputs.items():
                if not isinstance(node_out, dict):
                    continue
                # ComfyUI 0.22 SaveVideo: "images" + animated:true. VHS_VideoCombine: "gifs"/"videos".
                # Tüm olası key'leri tara, mp4/webm/mov/mkv uzantılı dosyaları video say.
                is_animated = bool(node_out.get("animated"))
                for anahtar in ("videos", "gifs", "files", "images"):
                    items = node_out.get(anahtar) or []
                    if not isinstance(items, list) or not items:
                        continue
                    ilk = items[0]
                    if not isinstance(ilk, dict):
                        continue
                    fname_raw = ilk.get("filename", "")
                    # images key için: yalnızca animated=True veya mp4/webm/mov uzantısı varsa video say
                    if anahtar == "images":
                        if not is_animated and not (fname_raw and any(
                            fname_raw.lower().endswith(ext)
                            for ext in (".mp4", ".webm", ".mov", ".mkv", ".gif")
                        )):
                            continue
                    fname = fname_raw
                    subf = ilk.get("subfolder", "")
                    ftype = ilk.get("type", "output")
                    # filename'de '/' veya '\' varsa (path-with-subfolder) parse et
                    if fname and ("/" in fname or "\\" in fname):
                        fname_norm = fname.replace("\\", "/")
                        parts = fname_norm.split("/")
                        pure_name = parts[-1]
                        extra_sub = "/".join(parts[:-1])
                        subf = (subf + "/" + extra_sub).strip("/") if subf else extra_sub
                        fname = pure_name
                    if fname:
                        video_filename = fname
                        video_subfolder = subf
                        video_url = (
                            f"/view?filename={quote(fname)}"
                            f"&subfolder={quote(subf)}&type={quote(ftype)}"
                        )
                        if folder_paths is not None:
                            base = folder_paths.get_output_directory() if ftype == "output" \
                                else folder_paths.get_temp_directory()
                            video_path = os.path.join(base, subf, fname) if subf \
                                else os.path.join(base, fname)
                        break
                if video_url:
                    break

            if "error" in status_str or status_obj.get("messages"):
                msgs = status_obj.get("messages", [])
                err_metin = ""
                for m in msgs:
                    if isinstance(m, list) and len(m) >= 2 and "error" in str(m[0]).lower():
                        err_metin = json.dumps(m[1])[:300]
                        break
                if err_metin:
                    return web.json_response({
                        "ok": False, "status": "error", "prompt_id": prompt_id,
                        "error": err_metin,
                    })

            if completed and video_url:
                return web.json_response({
                    "ok": True, "status": "done", "prompt_id": prompt_id,
                    "video_url": video_url, "video_path": video_path,
                    "video_filename": video_filename,
                    "video_subfolder": video_subfolder,
                })
            if completed:
                return web.json_response({
                    "ok": True, "status": "done", "prompt_id": prompt_id,
                    "video_url": "", "video_path": "",
                    "note": "tamamlandi ama video ciktisi bulunamadi",
                })
            return web.json_response({
                "ok": True, "status": "running", "prompt_id": prompt_id,
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"job_status hata: {e}", 500)

    # 6) POST /fikoyt_mvd/ltx/cancel + /fikoyt_mvd/cancel (universal) ----------
    async def _do_selective_cancel(request):
        """prompt_id'ye göre seçici iptal. force=true ise global interrupt.

        Body: { prompt_id: str, force?: bool }
        """
        try:
            try:
                data = await request.json()
            except Exception:
                data = {}
            prompt_id = (data.get("prompt_id") or "").strip()
            force = bool(data.get("force"))
            if force:
                ok = await _interrupt_comfy()
                return web.json_response({"ok": ok, "scope": "force_global",
                                          "prompt_id": prompt_id})
            if not prompt_id:
                return _err("prompt_id gerekli (veya force=true ile global interrupt)", 400)
            res = await _cancel_prompt_id(prompt_id)
            return web.json_response(res)
        except Exception as e:
            traceback.print_exc()
            return _err(f"cancel hata: {e}", 500)

    srv.routes.post("/fikoyt_mvd/ltx/cancel")(_do_selective_cancel)
    srv.routes.post("/fikoyt_mvd/cancel")(_do_selective_cancel)

    # 7) POST /fikoyt_mvd/ltx/combine_all --------------------------------------
    @srv.routes.post("/fikoyt_mvd/ltx/combine_all")
    async def _ltx_combine_all(request):
        try:
            data = await request.json()
        except Exception:
            return _err("gecersiz JSON body", 400)
        try:
            project_dir = (data.get("project_dir") or "").strip()
            out_name = _safe_filename(data.get("output_name") or "klip_final", 80) + ".mp4"
            include_master = bool(data.get("include_master_audio", True))

            if not project_dir or not os.path.isdir(project_dir):
                return _err("project_dir gecersiz", 404)

            videos_dir = os.path.join(project_dir, "videos")
            if not os.path.isdir(videos_dir):
                return _err("videos klasoru yok", 404)

            paterns = sorted(glob.glob(os.path.join(videos_dir, "seg_*.mp4")))
            if not paterns:
                return _err("birlestirilecek video bulunamadi", 404)

            # concat list dosyasi
            concat_list = os.path.join(videos_dir, "_concat.txt")
            with open(concat_list, "w", encoding="utf-8") as f:
                for v in paterns:
                    # ffmpeg concat demuxer single-quote escape
                    yol = v.replace("\\", "/").replace("'", "'\\''")
                    f.write(f"file '{yol}'\n")

            out_path = os.path.join(videos_dir, out_name)

            # master audio dosyasini bul
            master_audio = ""
            if include_master:
                meta = _meta_for(project_dir)
                master_audio = meta.get("source_audio", "") or ""
                if not master_audio or not os.path.isfile(master_audio):
                    # project_dir icindeki source.* dosyasini ara
                    for ext in (".mp3", ".wav", ".m4a", ".flac", ".ogg"):
                        adi = os.path.join(project_dir, f"source{ext}")
                        if os.path.isfile(adi):
                            master_audio = adi
                            break

            if master_audio and os.path.isfile(master_audio):
                args = [
                    "-y", "-loglevel", "error",
                    "-f", "concat", "-safe", "0", "-i", concat_list,
                    "-i", master_audio,
                    "-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
                    "-map", "0:v:0", "-map", "1:a:0", "-shortest",
                    out_path,
                ]
            else:
                args = [
                    "-y", "-loglevel", "error",
                    "-f", "concat", "-safe", "0", "-i", concat_list,
                    "-c", "copy",
                    out_path,
                ]

            rc, stderr = _run_ffmpeg(args, timeout=900)
            try:
                os.remove(concat_list)
            except Exception:
                pass

            if rc != 0 or not os.path.isfile(out_path):
                return _err(f"ffmpeg basarisiz (rc={rc}): {stderr[:300]}", 500)

            return web.json_response({
                "ok": True,
                "output_path": out_path,
                "output_url": _view_url_for(out_path),
                "duration": round(_video_duration_sec(out_path), 2),
                "segment_count": len(paterns),
                "has_master_audio": bool(master_audio),
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"combine_all hata: {e}", 500)

    # 8) POST /fikoyt_mvd/ltx/save_video_variant -------------------------------
    @srv.routes.post("/fikoyt_mvd/ltx/save_video_variant")
    async def _ltx_save_variant(request):
        try:
            data = await request.json()
        except Exception:
            return _err("gecersiz JSON body", 400)
        try:
            project_dir = (data.get("project_dir") or "").strip()
            seg_index = int(data.get("segment_index", 0))
            video_path = (data.get("video_path") or "").strip()
            thumb_path = (data.get("thumb_path") or "").strip()

            if not project_dir or not os.path.isdir(project_dir):
                return _err("project_dir gecersiz", 404)
            if not video_path or not os.path.isfile(video_path):
                return _err("video_path gecersiz", 404)

            meta = _meta_for(project_dir)
            segments = meta.get("segments", []) or []
            if seg_index < 0 or seg_index >= len(segments):
                return _err(f"segment_index aralik disinda: {seg_index}", 400)

            seg = segments[seg_index]
            variants = seg.get("video_variants", []) or []
            variants.append({
                "video_path": video_path,
                "video_url": _view_url_for(video_path),
                "thumb_path": thumb_path,
                "thumb_url": _view_url_for(thumb_path) if thumb_path else "",
                "added_at": datetime.datetime.now().isoformat(),
                "duration": round(_video_duration_sec(video_path), 2),
            })
            seg["video_variants"] = variants
            segments[seg_index] = seg
            meta["segments"] = segments
            if not _save_meta(project_dir, meta):
                return _err("meta kaydedilemedi", 500)

            return web.json_response({
                "ok": True,
                "variants_count": len(variants),
                "segment_index": seg_index,
            })
        except Exception as e:
            traceback.print_exc()
            return _err(f"save_video_variant hata: {e}", 500)

    # 9) GET /fikoyt_mvd/ltx/loras --------------------------------------------
    @srv.routes.get("/fikoyt_mvd/ltx/loras")
    async def _list_loras(request):
        try:
            from .lora_helpers import list_ltx_loras, list_all_loras, IC_LORA_PRESETS
            return web.json_response({
                "ltx_loras": list_ltx_loras(),
                "all_loras_count": len(list_all_loras()),
                "ic_lora_presets": IC_LORA_PRESETS,
            })
        except Exception as e:
            traceback.print_exc()
            return web.json_response({"error": str(e)}, status=500)

    # 10) GET /fikoyt_mvd/ltx/humo_capabilities -------------------------------
    @srv.routes.get("/fikoyt_mvd/ltx/humo_capabilities")
    async def _humo_caps(request):
        try:
            from .humo_helpers import (
                list_humo_models, humo_available,
                HUMO_DEFAULT_CONFIG, HUMO_RES_PRESETS,
            )
            available, node_cls = humo_available()

            # Wan ekosistemi tam mı? VAE + CLIP (umt5) + Audio encoder kontrolleri
            import folder_paths
            wan_vae = False
            wan_clip = False
            audio_encoder = False
            audio_encoders_list = []
            missing = []
            try:
                for v in folder_paths.get_filename_list("vae"):
                    if "wan" in v.lower() and "2.1" in v.lower():
                        wan_vae = True; break
                for c in folder_paths.get_filename_list("text_encoders") + folder_paths.get_filename_list("clip"):
                    if "umt5" in c.lower():
                        wan_clip = True; break
                # Audio encoders folder
                try:
                    aenc = folder_paths.get_filename_list("audio_encoders")
                    audio_encoders_list = list(aenc)
                    if aenc:
                        audio_encoder = True
                except Exception:
                    pass
            except Exception:
                pass
            if not wan_vae: missing.append("wan_2.1_vae.safetensors")
            if not wan_clip: missing.append("umt5_xxl_*.safetensors")
            if not audio_encoder: missing.append("audio_encoder (wav2vec2)")

            ready = available and wan_vae and wan_clip and audio_encoder
            status_note = ""
            if ready:
                status_note = "✓ HuMo tüm bileşenler hazır — audio-driven I2V çalışmalı."
            elif not available:
                status_note = "❌ HuMo node sınıfları kurulu değil (ComfyUI güncellemeleri gerekebilir)."
            else:
                status_note = "⚠ Eksik dosya(lar): " + ", ".join(missing) + ". LTX 2.3 audio modu alternatif olarak kullanılabilir."

            return web.json_response({
                "available": available,
                "node_class": node_cls,
                "models": list_humo_models(),
                "wan_vae": wan_vae,
                "wan_clip": wan_clip,
                "audio_encoder": audio_encoder,
                "audio_encoders_list": audio_encoders_list,
                "missing": missing,
                "ready": ready,
                "status_note": status_note,
                "default_config": HUMO_DEFAULT_CONFIG,
                "resolutions": list(HUMO_RES_PRESETS.keys()),
                # ─── Otomatik indirme hint'i (frontend "Otomatik İndir" butonu için) ───
                "install_id": "humo_full" if not ready else None,
                "install_endpoint": "/fikoyt_mvd/models/install",
                "install_hint_tr": (
                    "Eksik HuMo bileşenlerini otomatik indir: POST /fikoyt_mvd/models/install "
                    "body { id: 'humo_full' } — ~11GB (wav2vec + Wan HuMo + Wan VAE + umT5)"
                ) if not ready else None,
            })
        except Exception as e:
            traceback.print_exc()
            return web.json_response({"error": str(e)}, status=500)

    # 11) POST /fikoyt_mvd/ltx/humo_generate_segment --------------------------
    @srv.routes.post("/fikoyt_mvd/ltx/humo_generate_segment")
    async def _humo_gen(request):
        """HuMo workflow'u olustur + /prompt'a gonder.
        Body: { project_dir, segment_index, image_url, audio_path,
                positive_prompt, negative_prompt, settings, references }
        Cevap: { ok, prompt_id, number }
        """
        try:
            data = await request.json()
            from pathlib import Path
            tpl_path = None
            for p in (
                Path(__file__).parent.parent / "workflow" / "humo_segment.json",
                Path(__file__).parent / ".." / "workflow" / "humo_segment.json",
            ):
                if p.exists():
                    tpl_path = p
                    break
            if not tpl_path:
                return web.json_response(
                    {"error": "humo_segment.json template bulunamadi"}, status=501
                )
            wf = json.loads(tpl_path.read_text(encoding="utf-8"))
            settings = data.get("settings", {}) or {}

            # ─── Pre-flight: HuMo stack disk'te var mı? Yoksa anlamlı 424 + install_id ───
            _missing = []
            try:
                _te = (folder_paths.get_filename_list("text_encoders") or []) + \
                      (folder_paths.get_filename_list("clip") or [])
                if not any("umt5" in f.lower() for f in _te):
                    _missing.append("umt5_xxl_*.safetensors (text_encoders/)")
                _vae = folder_paths.get_filename_list("vae") or []
                if not any("wan" in f.lower() and "2.1" in f.lower() for f in _vae):
                    _missing.append("wan_2.1_vae.safetensors (vae/)")
                try:
                    _aenc = folder_paths.get_filename_list("audio_encoders") or []
                except Exception:
                    _aenc = []
                if not any("wav2vec" in a.lower() for a in _aenc):
                    _missing.append("wav2vec2_large_english_fp16.safetensors (audio_encoders/)")
                _unet = (folder_paths.get_filename_list("unet") or []) + \
                        (folder_paths.get_filename_list("diffusion_models") or [])
                if not any("humo" in f.lower() for f in _unet):
                    _missing.append("Wan2_1-HuMo-1_7B_fp16.safetensors (unet/)")
            except Exception:
                pass
            if _missing:
                return web.json_response({
                    "error": "HuMo bileşenleri eksik: " + " · ".join(_missing),
                    "missing": _missing,
                    "install_id": "humo_full",
                    "install_endpoint": "/fikoyt_mvd/models/install",
                    "install_hint_tr": (
                        "Otomatik indirme: POST /fikoyt_mvd/models/install body { id: 'humo_full' } — ~11GB. "
                        "Progress: GET /fikoyt_mvd/models/install/status. İptal: POST /fikoyt_mvd/models/install/cancel"
                    ),
                }, status=424)

            # S3 — Image ve audio'yu input/'a kopyala (LoadImage/LoadAudio için)
            project_dir = data.get("project_dir") or ""
            img_url_raw = data.get("image_url") or data.get("image_filename") or ""
            img_name = _prepare_image_for_load(img_url_raw)  # subfolder/filename
            audio_raw = data.get("audio_path") or ""
            audio_name = _prepare_audio_for_load(audio_raw, project_dir)  # input/-relative basename

            # umT5 ve VAE dosya isimlerini gerçek mevcut dosyalardan seç (fallback)
            umt5_name = "umt5_xxl_fp8_e4m3fn_scaled.safetensors"
            wan_vae_name = "wan_2.1_vae.safetensors"
            audio_enc_name = "wav2vec2_large_english_fp16.safetensors"
            try:
                te_files = (folder_paths.get_filename_list("text_encoders")
                            + folder_paths.get_filename_list("clip"))
                cand = [f for f in te_files if "umt5" in f.lower()]
                if cand:
                    umt5_name = cand[0]
                vae_files = folder_paths.get_filename_list("vae")
                cand = [v for v in vae_files if "wan" in v.lower() and "2.1" in v.lower()]
                if cand:
                    wan_vae_name = cand[0]
                try:
                    aenc = folder_paths.get_filename_list("audio_encoders")
                    cand = [a for a in aenc if "wav2vec" in a.lower()]
                    if cand:
                        audio_enc_name = cand[0]
                    elif aenc:
                        audio_enc_name = aenc[0]
                except Exception:
                    pass
            except Exception:
                pass

            replacements = {
                "@@MODEL@@": settings.get("checkpoint", "Wan2_1-HuMo-1_7B_fp16.safetensors"),
                "@@UMT5@@": umt5_name,
                "@@WAN_VAE@@": wan_vae_name,
                "@@AUDIO_ENC@@": audio_enc_name,
                "@@WIDTH@@": settings.get("width", 720),
                "@@HEIGHT@@": settings.get("height", 1024),
                "@@FRAMES@@": settings.get("frames", 81),
                "@@FPS@@": settings.get("fps", 24),
                "@@CFG@@": settings.get("guidance_scale", 5.0),
                "@@STEPS@@": settings.get("steps", 30),
                "@@SEED@@": settings.get("seed", 0),
                "@@AUDIO_WEIGHT@@": settings.get("audio_weight", 0.8),
                "@@LIPSYNC@@": settings.get("lipsync_strength", 0.85),
                "@@POSITIVE@@": data.get("positive_prompt", ""),
                "@@NEGATIVE@@": data.get("negative_prompt", "worst quality, blurry"),
                "@@IMAGE@@": img_name,
                "@@AUDIO@@": audio_name,
                "@@FILENAME_PREFIX@@": f"fikoyt_mvd/humo_seg{int(data.get('segment_index', 0)):02d}",
            }

            def _sub(obj):
                if isinstance(obj, dict):
                    return {k: _sub(v) for k, v in obj.items()}
                if isinstance(obj, list):
                    return [_sub(x) for x in obj]
                if isinstance(obj, str):
                    for k, v in replacements.items():
                        if k in obj:
                            if obj == k:
                                return v  # Tam eslesmede tip korunur (int/float)
                            obj = obj.replace(k, str(v))
                    return obj
                return obj

            wf = _sub(wf)
            import aiohttp
            comfy_url = os.environ.get("FIKOYT_COMFY_URL", "http://127.0.0.1:8188")
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    f"{comfy_url}/prompt",
                    json={"prompt": wf, "client_id": "fikoyt_mvd_humo"},
                    timeout=aiohttp.ClientTimeout(total=30),
                ) as r:
                    if r.status >= 400:
                        txt = await r.text()
                        return web.json_response(
                            {"error": f"ComfyUI /prompt HTTP {r.status}: {txt[:300]}"},
                            status=502,
                        )
                    rj = await r.json()
            return web.json_response({
                "ok": True,
                "prompt_id": rj.get("prompt_id"),
                "number": rj.get("number"),
            })
        except Exception as e:
            traceback.print_exc()
            return web.json_response({"error": str(e)}, status=500)

    # ─── V9 — Frame Interpolation (ffmpeg minterpolate) ───
    def _resolve_output_video(data: Dict[str, Any]) -> str:
        """video_path (mutlak) ya da filename+subfolder (output-göreli) → mutlak yol."""
        vp = (data.get("video_path") or "").strip()
        if vp and os.path.isfile(vp):
            return vp
        fn = (data.get("filename") or "").strip()
        if fn and folder_paths:
            sub = (data.get("subfolder") or "").strip()
            cand = os.path.join(folder_paths.get_output_directory(), sub, fn)
            if os.path.isfile(cand):
                return cand
        return vp

    @srv.routes.post("/fikoyt_mvd/ltx/interpolate_video")
    async def _interpolate_video(request):
        """Bir videoyu yüksek fps'e ffmpeg minterpolate ile çevir.
        Body: { video_path | (filename, subfolder), target_fps? (default 60), method? (mci/dup/blend) }
        """
        try:
            data = await request.json()
        except Exception:
            return _err("body JSON parse hata", 400)
        video_path = _resolve_output_video(data)
        target_fps = int(data.get("target_fps", 60))
        method = (data.get("method") or "mci").strip().lower()
        if method not in ("mci", "dup", "blend"):
            method = "mci"
        if not video_path or not os.path.isfile(video_path):
            return _err("video_path geçerli değil", 400)
        if not (24 <= target_fps <= 120):
            return _err("target_fps 24-120 arası olmalı", 400)

        import shutil as _sh, subprocess as _sp
        ff = _sh.which("ffmpeg")
        if not ff:
            return _err("ffmpeg PATH'te bulunamadı", 501)

        base = os.path.splitext(os.path.basename(video_path))[0]
        ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        out_dir = os.path.dirname(video_path)
        out_name = f"{base}_smooth{target_fps}_{ts}.mp4"
        out_path = os.path.join(out_dir, out_name)

        vf = f"minterpolate=fps={target_fps}:mi_mode={method}"
        if method == "mci":
            vf += ":mc_mode=aobmc:me_mode=bidir"

        cmd = [
            ff, "-y",
            "-i", video_path,
            "-vf", vf,
            "-c:v", "libx264", "-preset", "medium", "-crf", "18",
            "-c:a", "copy",
            out_path,
        ]
        try:
            print(f"[FikoYT V9] interpolate → {out_name} ({method}, {target_fps}fps)")
            proc = _sp.run(cmd, capture_output=True, timeout=1800)
            if proc.returncode != 0:
                err = (proc.stderr or b"").decode("utf-8", errors="replace")[-800:]
                return _err(f"ffmpeg hata: {err}", 500)
        except _sp.TimeoutExpired:
            return _err("ffmpeg 30 dk içinde tamamlanamadı", 504)
        except Exception as e:
            return _err(f"subprocess hata: {e}", 500)

        if not os.path.isfile(out_path):
            return _err("çıktı dosyası oluşturulamadı", 500)
        return web.json_response({
            "ok": True,
            "method": method,
            "target_fps": target_fps,
            "output_path": out_path,
            "output_filename": out_name,
        })

    # ─── V10 — AI Upscale (ffmpeg lanczos veya Real-ESRGAN binary) ───
    @srv.routes.post("/fikoyt_mvd/ltx/upscale_video")
    async def _upscale_video(request):
        """Bir videoyu büyüt — ffmpeg lanczos varsayılan, realesrgan-ncnn-vulkan binary varsa onu kullan.
        Body: { video_path | (filename, subfolder), scale? (2|4, default 2), engine? ('ffmpeg'|'realesrgan'), model? }
        """
        try:
            data = await request.json()
        except Exception:
            return _err("body JSON parse hata", 400)
        video_path = _resolve_output_video(data)
        scale = int(data.get("scale", 2))
        engine = (data.get("engine") or "ffmpeg").strip().lower()
        model = (data.get("model") or "realesrgan-x4plus").strip()

        if not video_path or not os.path.isfile(video_path):
            return _err("video_path geçerli değil", 400)
        if scale not in (2, 4):
            return _err("scale yalnız 2 veya 4 olabilir", 400)

        import shutil as _sh, subprocess as _sp
        ff = _sh.which("ffmpeg")
        if not ff:
            return _err("ffmpeg PATH'te bulunamadı", 501)

        base = os.path.splitext(os.path.basename(video_path))[0]
        ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        out_dir = os.path.dirname(video_path)
        out_name = f"{base}_up{scale}x_{engine}_{ts}.mp4"
        out_path = os.path.join(out_dir, out_name)

        # realesrgan binary kontrolü
        if engine == "realesrgan":
            rgan = _sh.which("realesrgan-ncnn-vulkan") or _sh.which("realesrgan-ncnn-vulkan.exe")
            if not rgan:
                # fallback ffmpeg lanczos
                print("[FikoYT V10] realesrgan binary yok — ffmpeg lanczos fallback")
                engine = "ffmpeg"

        if engine == "ffmpeg":
            cmd = [
                ff, "-y",
                "-i", video_path,
                "-vf", f"scale=iw*{scale}:ih*{scale}:flags=lanczos",
                "-c:v", "libx264", "-preset", "medium", "-crf", "18",
                "-c:a", "copy",
                out_path,
            ]
        else:
            # realesrgan binary — frame-by-frame
            import tempfile
            tmpdir = tempfile.mkdtemp(prefix="fikoyt_v10_")
            frames_in = os.path.join(tmpdir, "frames_in")
            frames_out = os.path.join(tmpdir, "frames_out")
            os.makedirs(frames_in, exist_ok=True)
            os.makedirs(frames_out, exist_ok=True)
            try:
                # Frame'leri çıkar
                _sp.run([ff, "-y", "-i", video_path,
                         "-q:v", "2", os.path.join(frames_in, "%06d.png")],
                        capture_output=True, timeout=600, check=True)
                # Upscale et
                _sp.run([rgan, "-i", frames_in, "-o", frames_out,
                         "-n", model, "-s", str(scale)],
                        capture_output=True, timeout=3600, check=True)
                # Frame'leri tekrar birleştir + orijinal audio
                cmd = [
                    ff, "-y",
                    "-framerate", "24",
                    "-i", os.path.join(frames_out, "%06d.png"),
                    "-i", video_path,
                    "-map", "0:v:0", "-map", "1:a:0?",
                    "-c:v", "libx264", "-preset", "medium", "-crf", "18",
                    "-c:a", "copy",
                    "-shortest",
                    out_path,
                ]
            except Exception as e:
                return _err(f"realesrgan akış hatası: {e}", 500)

        try:
            print(f"[FikoYT V10] upscale {scale}x ({engine}) → {out_name}")
            proc = _sp.run(cmd, capture_output=True, timeout=3600)
            if proc.returncode != 0:
                err = (proc.stderr or b"").decode("utf-8", errors="replace")[-800:]
                return _err(f"ffmpeg hata: {err}", 500)
        except _sp.TimeoutExpired:
            return _err("upscale 60 dk içinde tamamlanamadı", 504)
        except Exception as e:
            return _err(f"subprocess hata: {e}", 500)

        if not os.path.isfile(out_path):
            return _err("çıktı dosyası oluşturulamadı", 500)
        return web.json_response({
            "ok": True,
            "engine": engine,
            "scale": scale,
            "output_path": out_path,
            "output_filename": out_name,
        })

    print("[FikoYT LTX] 13 endpoint kaydedildi: /fikoyt_mvd/ltx/* (V9+V10 dahil)")
    return True


# Geriye uyumluluk: modul import edildiginde otomatik kaydetmeyi dene
def setup(server_instance=None) -> bool:
    """Alternatif giris noktasi. server_instance verilirse onu kullanir."""
    if server_instance is not None:
        try:
            # Manuel register (server_instance.routes uzerinden)
            return register_endpoints()
        except Exception:
            traceback.print_exc()
            return False
    return register_endpoints()
