"""
FikoYT MVD — Wan 2.2-S2V endpoint'leri (Sprint 12).

Routes:
  GET  /fikoyt_mvd/wan/s2v/status          → tüm durum (node + modeller + öneriler)
  GET  /fikoyt_mvd/wan/s2v/presets         → preset listesi
  POST /fikoyt_mvd/wan/s2v/install         → tek tık model download (4090 fp16 önerilen)
  GET  /fikoyt_mvd/wan/s2v/install_status  → download progress
  POST /fikoyt_mvd/wan/s2v/install_node    → ComfyUI-WanVideoWrapper git clone (Kijai)
  POST /fikoyt_mvd/character/{cid}/wan_s2v_render → karakter referansı + ses → video
"""
import os
import sys
import shutil
import asyncio
import datetime
import threading
import urllib.request

from .s2v_helpers import (
    s2v_status_dict, WAN_S2V_MODELS, WAN_S2V_RES_PRESETS,
    WAN_S2V_DEFAULT_CONFIG, recommend_models_for_vram,
    detect_wan_s2v_nodes,
)
from .infinitetalk_helpers import (
    infinitetalk_status_dict, INFINITETALK_MODELS,
    INFINITETALK_DEFAULT_CONFIG, estimate_chunks,
)


_install_state = {
    "in_progress": False, "log": "", "done": False, "error": "",
    "files": [], "current_file": "", "current_pct": 0,
}
_node_install_state = {
    "in_progress": False, "log": "", "done": False, "error": "",
}
_it_install_state = {
    "in_progress": False, "log": "", "done": False, "error": "",
    "files": [], "current_file": "", "current_pct": 0,
}
_install_lock = threading.Lock()


def register_endpoints():
    try:
        from server import PromptServer
        from aiohttp import web
        import folder_paths
    except ImportError as e:
        print(f"[FikoYT Wan] endpoint deps yok: {e}")
        return

    server = getattr(PromptServer, "instance", None)
    if server is None:
        print("[FikoYT Wan] PromptServer.instance hazır değil — endpoint skip")
        return

    @server.routes.get("/fikoyt_mvd/wan/s2v/status")
    async def _status(request):
        # VRAM tespit
        vram_gb = 0
        try:
            import torch
            if torch.cuda.is_available():
                vram_gb = int(torch.cuda.get_device_properties(0).total_memory / 1_073_741_824)
        except Exception:
            pass
        status = s2v_status_dict()
        status["vram_gb"] = vram_gb
        status["recommended_models"] = recommend_models_for_vram(vram_gb or 24)
        return web.json_response(status)

    @server.routes.get("/fikoyt_mvd/wan/s2v/presets")
    async def _presets(request):
        return web.json_response({
            "presets": [
                {"id": k, "width": v[0], "height": v[1],
                 "frames": v[2], "label": v[3]}
                for k, v in WAN_S2V_RES_PRESETS.items()
            ],
            "default_config": WAN_S2V_DEFAULT_CONFIG,
        })

    @server.routes.post("/fikoyt_mvd/wan/s2v/install")
    async def _install(request):
        """Body: {model_keys?: [str], force?: bool}
        model_keys verilmezse VRAM'a göre otomatik öneri (4090=fp16)."""
        try:
            body = await request.json()
        except Exception:
            body = {}

        with _install_lock:
            if _install_state["in_progress"]:
                return web.json_response({"error": "Zaten indiriliyor"}, status=409)

        # VRAM tespit + öneri
        vram_gb = 0
        try:
            import torch
            if torch.cuda.is_available():
                vram_gb = int(torch.cuda.get_device_properties(0).total_memory / 1_073_741_824)
        except Exception:
            pass
        model_keys = body.get("model_keys") or recommend_models_for_vram(vram_gb or 24)
        force = bool(body.get("force", False))

        # Filter: sadece WAN_S2V_MODELS'te olanlar
        model_keys = [k for k in model_keys if k in WAN_S2V_MODELS]
        if not model_keys:
            return web.json_response({"error": "Geçerli model_keys yok"}, status=400)

        _install_state.update({
            "in_progress": True, "log": "", "done": False, "error": "",
            "files": model_keys, "current_file": "", "current_pct": 0,
            "started_at": datetime.datetime.now().isoformat(),
            "vram_gb": vram_gb,
        })

        async def _bg():
            try:
                models_dir = folder_paths.models_dir
                _install_state["log"] += f"VRAM: {vram_gb}GB → seçili modeller: {model_keys}\n"
                for key in model_keys:
                    m = WAN_S2V_MODELS[key]
                    dest = os.path.join(models_dir, m["rel_path"])
                    os.makedirs(os.path.dirname(dest), exist_ok=True)
                    if os.path.isfile(dest) and os.path.getsize(dest) >= m["min_size"] and not force:
                        _install_state["log"] += f"[skip] {os.path.basename(dest)} mevcut\n"
                        continue
                    _install_state["current_file"] = key
                    _install_state["log"] += f"\n[dl] {m['label']}\n   → {dest}\n"

                    def _dl(_url=m["url"], _dest=dest, _key=key):
                        def _hook(blocks, block_size, total):
                            if total > 0:
                                pct = min(100, int(blocks * block_size * 100 / total))
                                _install_state["current_pct"] = pct
                        try:
                            urllib.request.urlretrieve(_url, _dest, reporthook=_hook)
                        except Exception as e:
                            _install_state["log"] += f"   ❌ {e}\n"
                            if os.path.isfile(_dest):
                                try: os.remove(_dest)
                                except Exception: pass
                            raise

                    await asyncio.get_event_loop().run_in_executor(None, _dl)
                    _install_state["log"] += "   ✓ Tamam\n"
                    _install_state["current_pct"] = 100

                _install_state["log"] += "\n✓ Tüm modeller indirildi. ComfyUI'yi RESTART et!\n"
                print("[FikoYT Wan] ✓ Wan 2.2-S2V modelleri indirildi")
            except Exception as e:
                _install_state["error"] = str(e)
                _install_state["log"] += f"\n❌ HATA: {e}\n"
            finally:
                _install_state["done"] = True
                _install_state["in_progress"] = False
                _install_state["current_file"] = ""

        asyncio.create_task(_bg())
        return web.json_response({
            "ok": True, "model_keys": model_keys, "vram_gb": vram_gb,
        })

    @server.routes.get("/fikoyt_mvd/wan/s2v/install_status")
    async def _install_status(request):
        return web.json_response(_install_state)

    @server.routes.post("/fikoyt_mvd/wan/s2v/install_node")
    async def _install_node(request):
        """Kijai ComfyUI-WanVideoWrapper git clone (ComfyUI core S2V yoksa)."""
        with _install_lock:
            if _node_install_state["in_progress"]:
                return web.json_response({"error": "Zaten yükleniyor"}, status=409)
            _node_install_state.update({
                "in_progress": True, "log": "", "done": False, "error": "",
            })

        async def _bg():
            try:
                # custom_nodes dizinini bul (wan/endpoints.py → custom_nodes/<plugin>/wan/endpoints.py)
                here = os.path.dirname(os.path.abspath(__file__))
                cn_dir = os.path.dirname(os.path.dirname(here))
                target = os.path.join(cn_dir, "ComfyUI-WanVideoWrapper")
                git_bin = shutil.which("git")
                if not git_bin:
                    raise RuntimeError("git PATH'te yok")
                if os.path.isdir(target):
                    _node_install_state["log"] += "[skip] ComfyUI-WanVideoWrapper zaten var\n"
                else:
                    _node_install_state["log"] += "[git] clone Kijai/ComfyUI-WanVideoWrapper\n"
                    proc = await asyncio.create_subprocess_exec(
                        git_bin, "clone",
                        "https://github.com/kijai/ComfyUI-WanVideoWrapper.git",
                        target,
                        stdout=asyncio.subprocess.PIPE,
                        stderr=asyncio.subprocess.STDOUT,
                    )
                    while True:
                        line = await proc.stdout.readline()
                        if not line: break
                        _node_install_state["log"] += line.decode("utf-8", errors="replace")
                    rc = await proc.wait()
                    if rc != 0:
                        raise RuntimeError(f"git clone exit {rc}")

                # Requirements
                req = os.path.join(target, "requirements.txt")
                if os.path.isfile(req):
                    _node_install_state["log"] += "\n[pip] requirements\n"
                    proc = await asyncio.create_subprocess_exec(
                        sys.executable, "-s", "-m", "pip", "install", "-r", req,
                        stdout=asyncio.subprocess.PIPE,
                        stderr=asyncio.subprocess.STDOUT,
                    )
                    while True:
                        line = await proc.stdout.readline()
                        if not line: break
                        _node_install_state["log"] += line.decode("utf-8", errors="replace")
                    await proc.wait()

                _node_install_state["log"] += "\n✓ Kijai WanVideoWrapper yüklendi. RESTART!\n"
            except Exception as e:
                _node_install_state["error"] = str(e)
                _node_install_state["log"] += f"\n❌ HATA: {e}\n"
            finally:
                _node_install_state["done"] = True
                _node_install_state["in_progress"] = False

        asyncio.create_task(_bg())
        return web.json_response({"ok": True})

    @server.routes.get("/fikoyt_mvd/wan/s2v/install_node_status")
    async def _node_status(request):
        return web.json_response(_node_install_state)

    @server.routes.post("/fikoyt_mvd/character/{cid}/wan_s2v_render")
    async def _char_s2v(request):
        """Karakter referansı + ses → Wan 2.2-S2V video.
        Body: {
          audio_path, prompt?, negative?, preset?, frames?, fps?, steps?, cfg?, seed?,
          ref_image_idx?: int (ana referans yoksa kütüphaneden seç)
        }
        Workflow template (wan_s2v_segment.json) doldurulup ComfyUI queue'ye gönderilir.
        """
        from nodes._legacy import (_load_character, _slug, PROJECT_BASE_DIR)
        cid = request.match_info["cid"]
        char = _load_character(cid)
        if not char:
            return web.json_response({"error": "Karakter bulunamadı"}, status=404)
        try:
            body = await request.json()
        except Exception as e:
            return web.json_response({"error": f"JSON parse: {e}"}, status=400)

        # Karakter ana referans
        ref_path = char.get("image_path")
        if not ref_path or not os.path.isfile(ref_path):
            return web.json_response(
                {"error": "Karakterin ana referansı yok"}, status=400)

        audio_path = (body.get("audio_path") or "").strip()
        if not audio_path or not os.path.isfile(audio_path):
            return web.json_response(
                {"error": "audio_path geçersiz"}, status=400)

        # Auto-pick model dosyaları
        s = s2v_status_dict()
        if not s["ready"]:
            return web.json_response({
                "error": "Wan 2.2-S2V hazır değil",
                "status": s,
                "hint": "/fikoyt_mvd/wan/s2v/install endpoint'ini çağır",
            }, status=400)
        diff = s["diffusion_models"][0]["filename"]
        umt5 = s["text_encoders"][0]["filename"]
        vae = s["vaes"][0]["filename"]
        audio_enc = s["audio_encoders"][0]["filename"]

        # Preset
        preset = body.get("preset", "1080x1920_portrait_fullhd")
        if preset in WAN_S2V_RES_PRESETS:
            w, h, default_frames, _ = WAN_S2V_RES_PRESETS[preset]
        else:
            w, h, default_frames = 1080, 1920, 121

        frames = int(body.get("frames", default_frames))
        fps = int(body.get("fps", WAN_S2V_DEFAULT_CONFIG["fps"]))
        steps = int(body.get("steps", WAN_S2V_DEFAULT_CONFIG["steps"]))
        cfg = float(body.get("cfg", WAN_S2V_DEFAULT_CONFIG["guidance_scale"]))
        import random as _r, time as _t
        seed = int(body.get("seed") or _r.randint(1, 0xFFFFFFFF))
        prompt = (body.get("prompt") or
                  f"the character is performing a song, expressive face, "
                  f"dynamic motion, music video aesthetic")
        neg = body.get("negative") or WAN_S2V_DEFAULT_CONFIG.get("negative",
            "blurry, deformed, watermark, low quality, static, distorted")

        # Audio kırpma (W30): start_sec/end_sec verilirse ffmpeg ile slice
        audio_start_sec = float(body.get("audio_start_sec") or 0)
        audio_end_sec = body.get("audio_end_sec")
        audio_end_sec = float(audio_end_sec) if audio_end_sec is not None else None

        # Ref image + audio'yu ComfyUI input/ altına kopyala
        inp = folder_paths.get_input_directory()
        inp_sub = os.path.join(inp, "fikoyt_mvd_wan_s2v")
        os.makedirs(inp_sub, exist_ok=True)
        ts = int(_t.time())
        ref_dest = os.path.join(inp_sub, f"ref_{cid}_{ts}{os.path.splitext(ref_path)[1]}")
        audio_dest = os.path.join(inp_sub, f"aud_{cid}_{ts}{os.path.splitext(audio_path)[1]}")
        try:
            shutil.copy2(ref_path, ref_dest)
            if audio_start_sec > 0 or audio_end_sec is not None:
                # ffmpeg slice
                import subprocess as _sp
                ff = shutil.which("ffmpeg")
                if not ff:
                    try:
                        import imageio_ffmpeg
                        ff = imageio_ffmpeg.get_ffmpeg_exe()
                    except Exception:
                        ff = None
                if ff:
                    args = [ff, "-y", "-i", audio_path,
                            "-ss", str(audio_start_sec)]
                    if audio_end_sec is not None and audio_end_sec > audio_start_sec:
                        args += ["-t", str(audio_end_sec - audio_start_sec)]
                    args += ["-acodec", "copy", audio_dest]
                    r = _sp.run(args, capture_output=True, text=True, timeout=120)
                    if r.returncode != 0:
                        # codec copy başarısız → re-encode
                        args = [ff, "-y", "-i", audio_path,
                                "-ss", str(audio_start_sec)]
                        if audio_end_sec is not None and audio_end_sec > audio_start_sec:
                            args += ["-t", str(audio_end_sec - audio_start_sec)]
                        args += ["-acodec", "libmp3lame", "-b:a", "192k", audio_dest]
                        _sp.run(args, capture_output=True, text=True, timeout=120)
                else:
                    shutil.copy2(audio_path, audio_dest)  # ffmpeg yoksa fallback full copy
            else:
                shutil.copy2(audio_path, audio_dest)
        except Exception as e:
            return web.json_response({"error": f"Dosya kopya hata: {e}"}, status=500)

        ref_rel = f"fikoyt_mvd_wan_s2v/{os.path.basename(ref_dest)}"
        audio_rel = f"fikoyt_mvd_wan_s2v/{os.path.basename(audio_dest)}"

        # Workflow template doldur
        wf_template_path = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "workflow", "wan_s2v_segment.json")
        if not os.path.isfile(wf_template_path):
            return web.json_response(
                {"error": "wan_s2v_segment.json template yok"}, status=500)
        with open(wf_template_path, encoding="utf-8") as f:
            wf_str = f.read()
        replacements = {
            "@@MODEL@@": diff,
            "@@UMT5@@": umt5,
            "@@WAN_VAE@@": vae,
            "@@AUDIO_ENC@@": audio_enc,
            "@@IMAGE@@": ref_rel,
            "@@AUDIO@@": audio_rel,
            "@@POSITIVE@@": prompt.replace('"', '\\"'),
            "@@NEGATIVE@@": neg.replace('"', '\\"'),
            "@@WIDTH@@": str(w),
            "@@HEIGHT@@": str(h),
            "@@FRAMES@@": str(frames),
            "@@FPS@@": str(fps),
            "@@STEPS@@": str(steps),
            "@@CFG@@": str(cfg),
            "@@SEED@@": str(seed),
            "@@FILENAME_PREFIX@@": f"fikoyt_mvd/_characters/{_slug(cid, 40)}/wan_s2v/seg_{ts}",
        }
        for k, v in replacements.items():
            wf_str = wf_str.replace(k, v)

        import json as _json
        try:
            wf = _json.loads(wf_str)
        except Exception as e:
            return web.json_response({"error": f"Workflow parse: {e}"}, status=500)

        # ComfyUI queue
        try:
            import uuid as _uuid
            pid = str(_uuid.uuid4())
            # Sprint 27: validate + outputs_to_execute auto
            import time as _tm
            import execution as _exec
            valid = await _exec.validate_prompt(pid, wf, None)
            if not valid[0]:
                return web.json_response({
                    "error": f"Wan S2V validation: {valid[1]}",
                    "node_errors": valid[3] if len(valid) > 3 else {},
                }, status=400)
            outputs_to_exec = valid[2] if len(valid) > 2 else []
            server.prompt_queue.put((0, pid, wf,
                                      {"client_id": "fmvd_wan_s2v",
                                       "create_time": int(_tm.time() * 1000)},
                                      outputs_to_exec, {}))
        except Exception as e:
            return web.json_response({"error": f"Queue put: {e}"}, status=500)

        return web.json_response({
            "ok": True, "prompt_id": pid,
            "config": {"width": w, "height": h, "frames": frames, "fps": fps,
                       "steps": steps, "cfg": cfg, "seed": seed},
            "files": {"ref": ref_rel, "audio": audio_rel,
                       "diffusion": diff, "umt5": umt5, "vae": vae,
                       "audio_encoder": audio_enc},
            "duration_sec": round(frames / fps, 2),
        })

    # ========================================================================
    # Sprint 13 — InfiniteTalk endpoint'leri
    # ========================================================================
    @server.routes.get("/fikoyt_mvd/wan/infinitetalk/status")
    async def _it_status(request):
        st = infinitetalk_status_dict()
        # VRAM detect
        try:
            import torch
            if torch.cuda.is_available():
                st["vram_gb"] = int(torch.cuda.get_device_properties(0).total_memory / 1_073_741_824)
        except Exception:
            st["vram_gb"] = 0
        return web.json_response(st)

    @server.routes.post("/fikoyt_mvd/wan/infinitetalk/install")
    async def _it_install(request):
        """InfiniteTalk diffusion modelini indir. Body: {variant?: 'fp16'|'fp8'|'multi'}"""
        try:
            body = await request.json()
        except Exception:
            body = {}

        with _install_lock:
            if _it_install_state["in_progress"]:
                return web.json_response({"error": "Zaten indiriliyor"}, status=409)

        vram_gb = 0
        try:
            import torch
            if torch.cuda.is_available():
                vram_gb = int(torch.cuda.get_device_properties(0).total_memory / 1_073_741_824)
        except Exception:
            pass

        # VRAM'a göre default
        variant = body.get("variant")
        if not variant:
            variant = "diffusion_fp16" if vram_gb >= 24 else "diffusion_fp8"
        elif variant in ("fp16", "fp8", "multi"):
            variant = f"diffusion_{variant}"

        if variant not in INFINITETALK_MODELS:
            return web.json_response({"error": f"Geçersiz variant: {variant}"}, status=400)

        _it_install_state.update({
            "in_progress": True, "log": "", "done": False, "error": "",
            "files": [variant], "current_file": variant, "current_pct": 0,
            "started_at": datetime.datetime.now().isoformat(),
        })

        async def _bg():
            try:
                m = INFINITETALK_MODELS[variant]
                dest = os.path.join(folder_paths.models_dir, m["rel_path"])
                os.makedirs(os.path.dirname(dest), exist_ok=True)
                if os.path.isfile(dest) and os.path.getsize(dest) >= m["min_size"]:
                    _it_install_state["log"] += f"[skip] {os.path.basename(dest)} mevcut\n"
                else:
                    _it_install_state["log"] += f"[dl] {m['label']}\n→ {dest}\n"
                    def _dl():
                        def _hook(blocks, block_size, total):
                            if total > 0:
                                pct = min(100, int(blocks * block_size * 100 / total))
                                _it_install_state["current_pct"] = pct
                        urllib.request.urlretrieve(m["url"], dest, reporthook=_hook)
                    await asyncio.get_event_loop().run_in_executor(None, _dl)
                    _it_install_state["log"] += "✓ Tamam\n"
                _it_install_state["log"] += "\n✓ InfiniteTalk modeli hazır. ComfyUI RESTART!\n"
                _it_install_state["log"] += ("ℹ Wan 2.2-S2V'nin umT5/VAE/Wav2Vec2 dosyaları "
                                              "paylaşılır — onlar zaten varsa ekstra yok.\n")
            except Exception as e:
                _it_install_state["error"] = str(e)
                _it_install_state["log"] += f"\n❌ HATA: {e}\n"
                if 'dest' in locals() and os.path.isfile(dest):
                    try: os.remove(dest)
                    except Exception: pass
            finally:
                _it_install_state["done"] = True
                _it_install_state["in_progress"] = False
                _it_install_state["current_file"] = ""

        asyncio.create_task(_bg())
        return web.json_response({"ok": True, "variant": variant, "vram_gb": vram_gb})

    @server.routes.get("/fikoyt_mvd/wan/infinitetalk/install_status")
    async def _it_install_status(request):
        return web.json_response(_it_install_state)

    @server.routes.get("/fikoyt_mvd/wan/infinitetalk/estimate")
    async def _it_estimate(request):
        """Audio süresinden chunk + render süresi tahmini.
        Query: ?duration_sec=180&chunk_frames=81&fps=24"""
        try:
            dur = float(request.query.get("duration_sec", 60))
            cf = int(request.query.get("chunk_frames",
                INFINITETALK_DEFAULT_CONFIG["chunk_frames"]))
            fps = int(request.query.get("fps", INFINITETALK_DEFAULT_CONFIG["fps"]))
            ov = int(request.query.get("overlap",
                INFINITETALK_DEFAULT_CONFIG["chunk_overlap_frames"]))
            est = estimate_chunks(dur, cf, fps, ov)
            est["audio_duration_sec"] = dur
            return web.json_response(est)
        except Exception as e:
            return web.json_response({"error": str(e)}, status=400)

    @server.routes.post("/fikoyt_mvd/character/{cid}/infinitetalk_render")
    async def _char_it(request):
        """Karakter ref + audio → InfiniteTalk lipsync video (sınırsız uzunluk).
        Body: {
          audio_path (zorunlu),
          mode?: 'i2v'|'v2v' (default i2v),
          video_path?: str (v2v için),
          prompt?, negative?,
          width?, height?, fps?,
          chunk_frames?, steps?, cfg?, audio_scale?, seed?,
          denoise?: float (v2v için, 0.5-0.9 önerilen)
        }
        """
        from nodes._legacy import _load_character, _slug
        cid = request.match_info["cid"]
        char = _load_character(cid)
        if not char:
            return web.json_response({"error": "Karakter bulunamadı"}, status=404)
        try:
            body = await request.json()
        except Exception as e:
            return web.json_response({"error": f"JSON parse: {e}"}, status=400)

        mode = body.get("mode", "i2v")
        if mode not in ("i2v", "v2v"):
            return web.json_response({"error": "mode: i2v|v2v"}, status=400)

        audio_path = (body.get("audio_path") or "").strip()
        if not audio_path or not os.path.isfile(audio_path):
            return web.json_response({"error": "audio_path geçersiz"}, status=400)

        # Sprint W30: audio_start_sec / audio_end_sec slice
        audio_start_sec = float(body.get("audio_start_sec") or 0)
        audio_end_sec_raw = body.get("audio_end_sec")
        audio_end_sec = float(audio_end_sec_raw) if audio_end_sec_raw is not None else None

        # Audio süresini öğren (librosa)
        audio_dur = 0
        try:
            import librosa
            audio_dur, _sr = librosa.get_duration(filename=audio_path), 22050
        except Exception:
            try:
                import wave
                with wave.open(audio_path, "rb") as w:
                    audio_dur = w.getnframes() / w.getframerate()
            except Exception:
                audio_dur = 60  # fallback
        # Slice süresine göre audio_dur güncelle
        if audio_end_sec is not None and audio_end_sec > audio_start_sec:
            audio_dur = min(audio_dur, audio_end_sec) - audio_start_sec
        elif audio_start_sec > 0:
            audio_dur = max(0.1, audio_dur - audio_start_sec)

        # Sistem durumu
        st = infinitetalk_status_dict()
        if not st["ready"]:
            return web.json_response({
                "error": "InfiniteTalk hazır değil",
                "status": st,
                "hint": "/fikoyt_mvd/wan/infinitetalk/install endpoint'ini çağır",
            }, status=400)
        diff = st["diffusion_models"][0]["filename"]
        umt5 = st["shared_text_encoders"][0]["filename"]
        vae = st["shared_vaes"][0]["filename"]
        audio_enc = st["shared_audio_encoders"][0]["filename"]

        # Default config + body override
        cfg_d = INFINITETALK_DEFAULT_CONFIG
        w = int(body.get("width", 832))
        h = int(body.get("height", 1280))
        fps = int(body.get("fps", cfg_d["fps"]))
        chunk_frames = int(body.get("chunk_frames", cfg_d["chunk_frames"]))
        steps = int(body.get("steps", cfg_d["steps"]))
        cfg_val = float(body.get("cfg", cfg_d["cfg"]))
        audio_scale = float(body.get("audio_scale", cfg_d["audio_scale"]))
        denoise = float(body.get("denoise", 0.7))
        import random as _r, time as _t
        seed = int(body.get("seed") or _r.randint(1, 0xFFFFFFFF))
        prompt = body.get("prompt") or (
            "the character is performing/singing, expressive face, "
            "natural body motion, music video aesthetic")
        neg = body.get("negative") or (
            "blurry, deformed, watermark, distorted face, static")

        # Total frames audio süresine göre
        est = estimate_chunks(audio_dur, chunk_frames, fps,
                              cfg_d["chunk_overlap_frames"])
        total_frames = est["total_frames"]

        # Audio + ref dosyalarını input/'a kopyala (W30: gerekirse slice)
        inp = folder_paths.get_input_directory()
        inp_sub = os.path.join(inp, "fikoyt_mvd_infinitetalk")
        os.makedirs(inp_sub, exist_ok=True)
        ts = int(_t.time())
        audio_dest = os.path.join(inp_sub, f"aud_{cid}_{ts}{os.path.splitext(audio_path)[1]}")
        if audio_start_sec > 0 or audio_end_sec is not None:
            import subprocess as _sp
            ff = shutil.which("ffmpeg")
            if not ff:
                try:
                    import imageio_ffmpeg
                    ff = imageio_ffmpeg.get_ffmpeg_exe()
                except Exception:
                    ff = None
            if ff:
                args = [ff, "-y", "-i", audio_path, "-ss", str(audio_start_sec)]
                if audio_end_sec is not None and audio_end_sec > audio_start_sec:
                    args += ["-t", str(audio_end_sec - audio_start_sec)]
                args += ["-acodec", "copy", audio_dest]
                r = _sp.run(args, capture_output=True, text=True, timeout=120)
                if r.returncode != 0:
                    args = [ff, "-y", "-i", audio_path, "-ss", str(audio_start_sec)]
                    if audio_end_sec is not None and audio_end_sec > audio_start_sec:
                        args += ["-t", str(audio_end_sec - audio_start_sec)]
                    args += ["-acodec", "libmp3lame", "-b:a", "192k", audio_dest]
                    _sp.run(args, capture_output=True, text=True, timeout=120)
            else:
                shutil.copy2(audio_path, audio_dest)
        else:
            shutil.copy2(audio_path, audio_dest)
        audio_rel = f"fikoyt_mvd_infinitetalk/{os.path.basename(audio_dest)}"

        # I2V için karakter ref, V2V için kaynak video
        replacements = {
            "@@MODEL@@": diff,
            "@@UMT5@@": umt5,
            "@@WAN_VAE@@": vae,
            "@@AUDIO_ENC@@": audio_enc,
            "@@AUDIO@@": audio_rel,
            "@@POSITIVE@@": prompt.replace('"', '\\"'),
            "@@NEGATIVE@@": neg.replace('"', '\\"'),
            "@@WIDTH@@": str(w),
            "@@HEIGHT@@": str(h),
            "@@FRAMES@@": str(total_frames),
            "@@FPS@@": str(fps),
            "@@STEPS@@": str(steps),
            "@@CFG@@": str(cfg_val),
            "@@SEED@@": str(seed),
            "@@AUDIO_SCALE@@": str(audio_scale),
            "@@FILENAME_PREFIX@@": (f"fikoyt_mvd/_characters/{_slug(cid, 40)}"
                                     f"/infinitetalk_{mode}/{ts}"),
        }

        if mode == "i2v":
            ref_path = char.get("image_path")
            if not ref_path or not os.path.isfile(ref_path):
                return web.json_response(
                    {"error": "Karakterin ana referansı yok"}, status=400)
            ref_dest = os.path.join(inp_sub, f"ref_{cid}_{ts}{os.path.splitext(ref_path)[1]}")
            shutil.copy2(ref_path, ref_dest)
            replacements["@@IMAGE@@"] = f"fikoyt_mvd_infinitetalk/{os.path.basename(ref_dest)}"
            tpl = "wan_infinitetalk_i2v.json"
        else:
            video_path = (body.get("video_path") or "").strip()
            if not video_path or not os.path.isfile(video_path):
                return web.json_response({"error": "v2v için video_path zorunlu"}, status=400)
            video_dest = os.path.join(inp_sub, f"vid_{cid}_{ts}{os.path.splitext(video_path)[1]}")
            shutil.copy2(video_path, video_dest)
            replacements["@@VIDEO@@"] = f"fikoyt_mvd_infinitetalk/{os.path.basename(video_dest)}"
            replacements["@@DENOISE@@"] = str(denoise)
            tpl = "wan_infinitetalk_v2v.json"

        wf_path = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
            "workflow", tpl)
        if not os.path.isfile(wf_path):
            return web.json_response({"error": f"Template yok: {tpl}"}, status=500)
        with open(wf_path, encoding="utf-8") as f:
            wf_str = f.read()
        for k, v in replacements.items():
            wf_str = wf_str.replace(k, v)

        import json as _json
        try:
            wf = _json.loads(wf_str)
        except Exception as e:
            return web.json_response({"error": f"Workflow parse: {e}"}, status=500)

        try:
            import uuid as _uuid
            pid = str(_uuid.uuid4())
            # Sprint 27: validate + outputs_to_execute auto
            import time as _tm
            import execution as _exec
            valid = await _exec.validate_prompt(pid, wf, None)
            if not valid[0]:
                return web.json_response({
                    "error": f"InfiniteTalk validation: {valid[1]}",
                    "node_errors": valid[3] if len(valid) > 3 else {},
                }, status=400)
            outputs_to_exec = valid[2] if len(valid) > 2 else []
            server.prompt_queue.put((0, pid, wf,
                                      {"client_id": "fmvd_infinitetalk",
                                       "create_time": int(_tm.time() * 1000)},
                                      outputs_to_exec, {}))
        except Exception as e:
            return web.json_response({"error": f"Queue: {e}"}, status=500)

        return web.json_response({
            "ok": True, "prompt_id": pid, "mode": mode,
            "audio_duration_sec": audio_dur,
            "estimate": est,
            "config": {"width": w, "height": h, "frames": total_frames,
                       "fps": fps, "steps": steps, "cfg": cfg_val,
                       "audio_scale": audio_scale, "seed": seed},
        })
