#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
WanGP Müzik Video Üretici
==========================
WanGP'nin Python API'sini kullanarak şarkı + görsel girişinden
otomatik uzun müzik videosu üretir.

Pipeline:
1. Şarkıyı segmentlere böl (ffmpeg)
2. Her segment için WanGP API ile video üret
   - image_start: giriş görseli (ilk segment) veya önceki videonun son karesi
   - audio_guide: o segmente karşılık gelen ses parçası (lip-sync için)
3. Her videonun son karesini bir sonraki videonun giriş görseli olarak kullan
4. Tüm videoları birleştir (ffmpeg)
5. Orijinal sesi tam olarak ekle (ffmpeg)

Kullanım:
    python wangp_video_uret.py <sarki.mp3> <gorsel.jpg> [seçenekler]

    veya sürükle-bırak ile WanGP-Video-Uret.bat kullanın.

Desteklenen Modeller (örnekler):
    i2v          - Wan2.1 Image2Video 480p 14B
    i2v_720p     - Wan2.1 Image2Video 720p 14B
    i2v_2_2      - Wan2.1 2.2 Image2Video
    t2v          - Wan2.1 Text2Video 14B
    t2v_2_2      - Wan2.1 2.2 Text2Video
    ltx2_22B     - LTX-2 22B
    ltx2_22B_distilled - LTX-2 22B Distilled (hızlı)
    ltx2_19B     - LTX-2 19B
    hunyuan_1_5_i2v - HunyuanVideo 1.5 I2V
    hunyuan_1_5_t2v - HunyuanVideo 1.5 T2V
    ... ve daha fazlası (defaults/ klasöründeki tüm modeller)
"""

import os
import sys
import json
import time
import shutil
import argparse
import subprocess
from pathlib import Path
from datetime import datetime

# ============================================================================
# AYARLAR
# ============================================================================
WANGP_ROOT = Path(__file__).parent / "LTX-Desktop-WanGP" / "LTX-Desktop-WanGP" / "Wan2GP"
WANGP_PYTHON = WANGP_ROOT / "env_venv" / "Scripts" / "python.exe"
FFMPEG = "ffmpeg"
FFPROBE = "ffprobe"

# Varsayılan değerler
DEFAULT_MODEL = "i2v"
DEFAULT_RESOLUTION = "832x480"
DEFAULT_PROMPT = "[VISUAL] cinematic music video, single person performing with emotion, dramatic cinematic lighting with volumetric rays, professional film cinematography, anamorphic lens flare, shallow depth of field, film grain, color graded, 4K ultra high quality, smooth natural motion, dynamic slow camera movement [SPEECH] person singing with expressive lip movements matching the music [SOUND] background music playing"
DEFAULT_NEGATIVE_PROMPT = "blurry, low quality, distorted face, deformed, extra fingers, extra limbs, multiple people, duplicate, clone, cartoon, animated, watermark, text, logo, static, frozen, jerky motion, ugly"
DEFAULT_GUIDANCE_SCALE = 5.0
DEFAULT_STEPS = 30
DEFAULT_SEED = -1  # rastgele
DEFAULT_FLOW_SHIFT = 7.0
DEFAULT_SOLVER = "unipc"
DEFAULT_SEGMENT_DURATION = 5  # saniye (WanGP frame sayısıyla çalışır, fps'e göre hesaplanır)
DEFAULT_FPS = 16  # Wan modelleri genelde 16fps
DEFAULT_VIDEO_LENGTH = 81  # frame (81 frame = ~5s @ 16fps)
DEFAULT_SLIDING_WINDOW_SIZE = 0  # 0 = kapalı
DEFAULT_SLIDING_WINDOW_OVERLAP = 1

# FaceFusion ayarları
FACEFUSION_DIR = Path(__file__).parent / "FaceFusion"
FACEFUSION_VENV_PYTHON = FACEFUSION_DIR / "venv" / "Scripts" / "python.exe"


# ============================================================================
# SES ENERJİSİ ANALİZİ VE PROMPT VARYASYONU
# ============================================================================

ENERGY_PROMPT_TEMPLATES = {
    "silence": {
        "prefix": "dark ambient atmosphere, minimal movement, ",
        "suffix": ", slow fade, contemplative mood, still camera"
    },
    "low": {
        "prefix": "gentle and calm scene, soft lighting, ",
        "suffix": ", subtle motion, intimate mood, slow camera pan"
    },
    "medium": {
        "prefix": "dynamic scene with moderate energy, ",
        "suffix": ", fluid motion, engaging atmosphere, steady camera movement"
    },
    "high": {
        "prefix": "intense and energetic scene, dramatic lighting, ",
        "suffix": ", fast motion, powerful atmosphere, dynamic camera angles"
    },
    "climax": {
        "prefix": "epic climactic moment, explosive visual energy, ",
        "suffix": ", rapid motion, overwhelming atmosphere, sweeping cinematic camera"
    },
}


def analyze_audio_energy(audio_path, segment_boundaries):
    """Her segment için ses enerji seviyesini analiz eder (RMS loudness)."""
    rms_values = []
    for seg_start, seg_end in segment_boundaries:
        cmd = [
            FFPROBE, "-v", "error",
            "-f", "lavfi",
            "-i", f"amovie={str(audio_path)}:sp={seg_start}:ep={seg_end},astats=metadata=1:reset=0",
            "-show_entries", "frame_tags=lavfi.astats.Overall.RMS_level",
            "-of", "default=noprint_wrappers=1:nokey=1"
        ]
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
            values = []
            for line in result.stdout.strip().split('\n'):
                line = line.strip()
                if line and line != '-inf':
                    try:
                        values.append(float(line))
                    except ValueError:
                        pass
            rms_values.append(sum(values) / len(values) if values else -60.0)
        except Exception:
            rms_values.append(-60.0)

    if not rms_values:
        return ["medium"] * len(segment_boundaries)

    min_rms = min(rms_values)
    max_rms = max(rms_values)
    rms_range = max_rms - min_rms if max_rms > min_rms else 1.0

    levels = []
    for rms in rms_values:
        if rms <= -50:
            levels.append("silence")
        else:
            norm = (rms - min_rms) / rms_range
            if norm < 0.2:
                levels.append("low")
            elif norm < 0.5:
                levels.append("medium")
            elif norm < 0.8:
                levels.append("high")
            else:
                levels.append("climax")
    return levels


def build_dynamic_prompt(base_prompt, energy_level):
    """Enerji seviyesine göre prompt'u zenginleştirir."""
    template = ENERGY_PROMPT_TEMPLATES.get(energy_level, ENERGY_PROMPT_TEMPLATES["medium"])
    return template["prefix"] + base_prompt + template["suffix"]


# ============================================================================
# YARDIMCI FONKSİYONLAR
# ============================================================================

def get_available_models():
    """defaults/ klasöründeki tüm kullanılabilir modelleri listeler."""
    defaults_dir = WANGP_ROOT / "defaults"
    models = {}
    if defaults_dir.exists():
        for f in sorted(defaults_dir.glob("*.json")):
            if f.name == "ReadMe.txt":
                continue
            model_id = f.stem
            try:
                with open(f, "r", encoding="utf-8") as fh:
                    data = json.load(fh)
                name = data.get("model", {}).get("name", model_id)
                desc = data.get("model", {}).get("description", "")
                arch = data.get("model", {}).get("architecture", model_id)
                models[model_id] = {"name": name, "description": desc, "architecture": arch}
            except Exception:
                models[model_id] = {"name": model_id, "description": "", "architecture": model_id}
    return models


def get_available_loras():
    """loras/ klasöründeki tüm LoRA dosyalarını listeler."""
    loras_dir = WANGP_ROOT / "loras"
    loras = []
    if loras_dir.exists():
        for f in loras_dir.rglob("*.safetensors"):
            loras.append(str(f.relative_to(loras_dir)))
        for f in loras_dir.rglob("*.pt"):
            loras.append(str(f.relative_to(loras_dir)))
    return sorted(loras)


def get_audio_duration(audio_path):
    """Ses dosyasının süresini saniye olarak döndürür."""
    cmd = [
        FFPROBE, "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        str(audio_path)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    return float(result.stdout.strip())


def split_audio_into_segments(audio_path, output_dir, segment_dur, total_duration):
    """Ses dosyasını segmentlere böler."""
    segments = []
    num_segments = int(total_duration // segment_dur)
    remainder = total_duration % segment_dur
    if remainder >= 3.0:
        num_segments += 1

    print(f"[*] Ses {num_segments} parçaya bölünüyor ({segment_dur}s)...")

    for i in range(num_segments):
        seg_start = i * segment_dur
        seg_end = min((i + 1) * segment_dur, total_duration)
        seg_actual_dur = seg_end - seg_start

        seg_audio = output_dir / f"audio_segment_{i:03d}.wav"

        cmd = [
            FFMPEG, "-y",
            "-i", str(audio_path),
            "-ss", str(seg_start),
            "-t", str(seg_actual_dur),
            "-acodec", "pcm_s16le",
            "-ar", "44100",
            str(seg_audio)
        ]
        subprocess.run(cmd, capture_output=True, check=True)

        segments.append((seg_audio, seg_start, seg_end, seg_actual_dur))
        print(f"  Segment {i+1}/{num_segments}: {seg_start:.1f}s - {seg_end:.1f}s ({seg_actual_dur:.1f}s)")

    return segments


def extract_last_frame(video_path, output_path, skip_last_frames=4):
    """Videonun son karesini çıkarır (son N kare atlanır, bulanık olabilir)."""
    cmd = [
        FFPROBE, "-v", "error",
        "-show_entries", "stream=r_frame_rate,duration",
        "-select_streams", "v:0",
        "-of", "default=noprint_wrappers=1",
        str(video_path)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    duration = 0
    fps = 24
    for line in result.stdout.strip().split('\n'):
        if line.startswith('duration='):
            duration = float(line.split('=')[1])
        elif line.startswith('r_frame_rate='):
            fr = line.split('=')[1]
            if '/' in fr:
                num, den = fr.split('/')
                fps = float(num) / float(den)
            else:
                fps = float(fr)

    # Son N kareyi atla (son kareler genelde bulanık/bozuk olur)
    skip_seconds = skip_last_frames / fps
    seek_time = max(0, duration - skip_seconds - 0.05)

    cmd = [
        FFMPEG, "-y",
        "-ss", str(seek_time),
        "-i", str(video_path),
        "-frames:v", "1",
        "-q:v", "1",
        str(output_path)
    ]
    subprocess.run(cmd, capture_output=True, check=True)
    print(f"  Son kare çıkarıldı: {output_path.name}")


def extract_best_frame(video_path, output_path, num_candidates=8):
    """Videonun son birkaç karesini analiz edip en kaliteli olanı seçer.

    Son kareler bazen bozuk/bulanık olabiliyor (halüsinasyon, blur).
    Bu fonksiyon son N kareyi çıkarıp Laplacian varyansı ile
    en keskin (en az bulanık) olanı seçer.

    Args:
        video_path: Video dosyası yolu
        output_path: Seçilen karenin kaydedileceği yol
        num_candidates: Analiz edilecek son kare sayısı (varsayılan: 8)

    Returns:
        True başarılı, False başarısız (fallback olarak son kare kullanılır)
    """
    import tempfile

    # Video süresini ve FPS'i al
    cmd = [
        FFPROBE, "-v", "error",
        "-show_entries", "stream=r_frame_rate,duration",
        "-select_streams", "v:0",
        "-of", "default=noprint_wrappers=1",
        str(video_path)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    duration = 0
    fps = 24
    for line in result.stdout.strip().split('\n'):
        if line.startswith('duration='):
            try:
                duration = float(line.split('=')[1])
            except ValueError:
                pass
        elif line.startswith('r_frame_rate='):
            fr = line.split('=')[1]
            try:
                if '/' in fr:
                    num, den = fr.split('/')
                    fps = float(num) / float(den)
                else:
                    fps = float(fr)
            except ValueError:
                pass

    if duration <= 0:
        print(f"  [BEST FRAME] Video süresi alınamadı, fallback: son kare")
        extract_last_frame(video_path, output_path)
        return False

    # Son N kareyi çıkar
    skip_last = 2
    frame_interval = 1.0 / fps
    candidates_dir = tempfile.mkdtemp(prefix="best_frame_")

    candidate_files = []
    for i in range(num_candidates):
        frame_offset = skip_last + num_candidates - 1 - i
        seek_time = max(0, duration - (frame_offset * frame_interval) - 0.01)

        candidate_path = os.path.join(candidates_dir, f"candidate_{i:02d}.jpg")
        cmd = [
            FFMPEG, "-y",
            "-ss", str(seek_time),
            "-i", str(video_path),
            "-frames:v", "1",
            "-q:v", "1",
            candidate_path
        ]
        r = subprocess.run(cmd, capture_output=True)
        if r.returncode == 0 and os.path.exists(candidate_path) and os.path.getsize(candidate_path) > 0:
            candidate_files.append(candidate_path)

    if not candidate_files:
        print(f"  [BEST FRAME] Aday kare çıkarılamadı, fallback: son kare")
        shutil.rmtree(candidates_dir, ignore_errors=True)
        extract_last_frame(video_path, output_path)
        return False

    # Laplacian varyansı yaklaşımı: komşu piksel farkları
    best_score = -1
    best_file = candidate_files[-1]

    for cf in candidate_files:
        try:
            cmd = [FFMPEG, "-y", "-i", cf, "-pix_fmt", "gray", "-f", "rawvideo", "pipe:1"]
            r = subprocess.run(cmd, capture_output=True)
            if r.returncode != 0 or len(r.stdout) < 100:
                continue

            raw = r.stdout
            total_diff = 0
            count = 0
            for j in range(0, len(raw) - 1):
                diff = abs(raw[j] - raw[j + 1])
                total_diff += diff * diff
                count += 1

            score = total_diff / max(count, 1)

            if score > best_score:
                best_score = score
                best_file = cf
        except Exception:
            continue

    shutil.copy2(best_file, str(output_path))
    shutil.rmtree(candidates_dir, ignore_errors=True)

    print(f"  [BEST FRAME] {len(candidate_files)} aday analiz edildi, en keskin kare seçildi (skor: {best_score:.0f})")
    return True


def face_swap_image(source_face_path, target_image_path, output_path):
    """FaceFusion ile bir resimdeki yüzü değiştirir."""
    if not FACEFUSION_VENV_PYTHON.exists():
        print(f"  HATA: FaceFusion kurulu değil!")
        return False

    ff_script = FACEFUSION_DIR / "facefusion" / "facefusion.py"
    if not ff_script.exists():
        ff_script = FACEFUSION_DIR / "facefusion" / "run.py"
    if not ff_script.exists():
        print(f"  HATA: FaceFusion script bulunamadı!")
        return False

    source_abs = str(Path(source_face_path).resolve())
    target_abs = str(Path(target_image_path).resolve())
    output_abs = str(Path(output_path).resolve())

    env = os.environ.copy()
    nvidia_pkgs = FACEFUSION_DIR / "venv" / "Lib" / "site-packages" / "nvidia"
    extra_paths = [str(FACEFUSION_DIR / "venv" / "Scripts")]
    if nvidia_pkgs.exists():
        for d in nvidia_pkgs.iterdir():
            if d.is_dir():
                for sub in ("bin", "lib"):
                    sd = d / sub
                    if sd.exists():
                        extra_paths.append(str(sd))
    cuda_path = r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\bin"
    if os.path.exists(cuda_path):
        extra_paths.append(cuda_path)
    env["PATH"] = ";".join(extra_paths) + ";" + env.get("PATH", "")

    cmd = [
        str(FACEFUSION_VENV_PYTHON),
        str(ff_script),
        "headless-run",
        "--source-paths", source_abs,
        "--target-path", target_abs,
        "--output-path", output_abs,
        "--processors", "face_swapper",
        "--execution-providers", "cuda",
        "--face-selector-mode", "one",
        "--face-detector-score", "0.5",
        "--output-image-quality", "95"
    ]

    print(f"  [FACE SWAP] Uygulanıyor...")
    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True, timeout=120,
            cwd=str(FACEFUSION_DIR / "facefusion"), env=env
        )
        if Path(output_abs).exists():
            print(f"  [FACE SWAP] Başarılı!")
            return True
        else:
            print(f"  [FACE SWAP] HATA: Çıkış dosyası oluşmadı!")
            return False
    except subprocess.TimeoutExpired:
        print(f"  [FACE SWAP] HATA: Zaman aşımı!")
        return False
    except Exception as e:
        print(f"  [FACE SWAP] HATA: {e}")
        return False


def concatenate_videos(video_list, output_path):
    """Videoları birleştirir."""
    list_file = output_path.parent / "concat_list.txt"
    with open(list_file, "w", encoding="utf-8") as f:
        for v in video_list:
            escaped = str(v).replace("'", "'\\''")
            f.write(f"file '{escaped}'\n")

    cmd = [
        FFMPEG, "-y",
        "-f", "concat",
        "-safe", "0",
        "-i", str(list_file),
        "-c", "copy",
        str(output_path)
    ]
    subprocess.run(cmd, capture_output=True, check=True)
    list_file.unlink(missing_ok=True)
    print(f"  Videolar birleştirildi: {output_path.name}")


def add_audio_to_video(video_path, audio_path, output_path):
    """Videoya orijinal tam sesi ekler."""
    cmd = [
        FFMPEG, "-y",
        "-i", str(video_path),
        "-i", str(audio_path),
        "-c:v", "copy",
        "-c:a", "aac",
        "-b:a", "192k",
        "-map", "0:v:0",
        "-map", "1:a:0",
        "-shortest",
        str(output_path)
    ]
    subprocess.run(cmd, capture_output=True, check=True)
    print(f"  Orijinal ses eklendi: {output_path.name}")


def duration_to_frames(duration_sec, fps):
    """Süreyi frame sayısına çevirir (WanGP frame bazlı çalışır)."""
    frames = int(duration_sec * fps)
    # WanGP genelde 4n+1 frame bekler (81, 121, 161, 201, 241...)
    # En yakın geçerli değere yuvarla
    frames = max(17, frames)
    remainder = (frames - 1) % 4
    if remainder != 0:
        frames = frames + (4 - remainder)
    return frames


def generate_all_segments(manifest_path, wangp_root, tasks_dir, segments_dir, frames_dir,
                          face_swap_source, timeout=3600):
    """WanGP Python API ile tum segmentleri TEK SESSION'da uretir.

    Model 1 kere yuklenir. Her segment uretildikten sonra son kare
    cikarilir ve sonraki segmentin image_start'i guncellenir.

    Args:
        manifest_path: Tum segment task'larinin listesini iceren JSON
        wangp_root: WanGP ana dizini
        tasks_dir: Her segmentin output dizinlerinin bulundugu ust klasor
        segments_dir: Final segment videolarinin kopyalanacagi klasor
        frames_dir: Cikarilan karelerin kaydedilecegi klasor
        face_swap_source: Yuz degistirme icin kaynak gorsel (None = yok)
        timeout: Toplam max bekleme suresi (saniye)

    Returns:
        list: Basarili segment video yollari listesi
    """
    # Runner script'i yaz
    runner_script = tasks_dir / "_wangp_batch_runner.py"
    runner_code = r'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""WanGP Batch Runner - tek session'da tum segmentleri uretir.

Kullanim:
    python _wangp_batch_runner.py <wangp_root> <manifest_json>

Model BIR KERE yuklenir, tum segmentler sirayla uretilir.
Her segment icin run_task() cagirilir (senkron, basit).
"""
import sys, os, json, time, glob, subprocess, shutil, traceback, gc
from pathlib import Path

# ── CUDA OOM FIX: Bellek parcalanmasini onle ──
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"

FFMPEG = "ffmpeg"
FFPROBE = "ffprobe"

def find_biggest_mp4(directory):
    """Dizindeki en buyuk mp4 dosyasini bul."""
    mp4s = glob.glob(os.path.join(str(directory), "*.mp4"))
    if not mp4s:
        # Alt dizinlere de bak (WanGP bazen alt klasore kaydeder)
        mp4s = glob.glob(os.path.join(str(directory), "**", "*.mp4"), recursive=True)
    if not mp4s:
        return None
    mp4s.sort(key=os.path.getsize, reverse=True)
    return mp4s[0]

def extract_frame(video_path, output_path, skip_last=4):
    """Videonun son karesini cikar (son N kare atlanir). Fallback olarak kullanilir."""
    cmd = [FFPROBE, "-v", "error", "-show_entries", "stream=r_frame_rate,duration",
           "-select_streams", "v:0", "-of", "default=noprint_wrappers=1", str(video_path)]
    r = subprocess.run(cmd, capture_output=True, text=True)
    dur, fps = 0, 24
    for line in r.stdout.strip().split("\n"):
        if line.startswith("duration="): dur = float(line.split("=")[1])
        elif line.startswith("r_frame_rate="):
            fr = line.split("=")[1]
            fps = float(fr.split("/")[0]) / float(fr.split("/")[1]) if "/" in fr else float(fr)
    seek = max(0, dur - skip_last / fps - 0.05)
    cmd = [FFMPEG, "-y", "-ss", str(seek), "-i", str(video_path),
           "-frames:v", "1", "-q:v", "1", str(output_path)]
    subprocess.run(cmd, capture_output=True, check=True)

def extract_best_frame(video_path, output_path, num_candidates=8):
    """Son birkaç kareyi analiz edip en keskin olanini sec."""
    import tempfile
    cmd = [FFPROBE, "-v", "error", "-show_entries", "stream=r_frame_rate,duration",
           "-select_streams", "v:0", "-of", "default=noprint_wrappers=1", str(video_path)]
    r = subprocess.run(cmd, capture_output=True, text=True)
    dur, fps = 0, 24
    for line in r.stdout.strip().split("\n"):
        if line.startswith("duration="):
            try: dur = float(line.split("=")[1])
            except: pass
        elif line.startswith("r_frame_rate="):
            fr = line.split("=")[1]
            try: fps = float(fr.split("/")[0]) / float(fr.split("/")[1]) if "/" in fr else float(fr)
            except: pass
    if dur <= 0:
        extract_frame(video_path, output_path)
        return
    skip_last = 2
    fi = 1.0 / fps
    cdir = tempfile.mkdtemp(prefix="bf_")
    cands = []
    for i in range(num_candidates):
        off = skip_last + num_candidates - 1 - i
        st = max(0, dur - off * fi - 0.01)
        cp = os.path.join(cdir, f"c_{i:02d}.jpg")
        r2 = subprocess.run([FFMPEG, "-y", "-ss", str(st), "-i", str(video_path),
                            "-frames:v", "1", "-q:v", "1", cp], capture_output=True)
        if r2.returncode == 0 and os.path.exists(cp) and os.path.getsize(cp) > 0:
            cands.append(cp)
    if not cands:
        shutil.rmtree(cdir, ignore_errors=True)
        extract_frame(video_path, output_path)
        return
    best_s, best_f = -1, cands[-1]
    for cf in cands:
        try:
            r3 = subprocess.run([FFMPEG, "-y", "-i", cf, "-pix_fmt", "gray",
                                "-f", "rawvideo", "pipe:1"], capture_output=True)
            if r3.returncode != 0 or len(r3.stdout) < 100: continue
            raw = r3.stdout
            td, cnt = 0, 0
            for j in range(len(raw)-1):
                d = abs(raw[j]-raw[j+1]); td += d*d; cnt += 1
            sc = td / max(cnt, 1)
            if sc > best_s: best_s, best_f = sc, cf
        except: continue
    shutil.copy2(best_f, str(output_path))
    shutil.rmtree(cdir, ignore_errors=True)
    print(f"  [BEST FRAME] {len(cands)} aday -> en keskin secildi (skor: {best_s:.0f})")

def main():
    if len(sys.argv) < 3:
        print("Kullanim: _wangp_batch_runner.py <wangp_root> <manifest_json>")
        sys.exit(1)

    wangp_root = sys.argv[1]
    manifest_path = sys.argv[2]

    # WanGP'yi import edebilmek icin path ekle
    if wangp_root not in sys.path:
        sys.path.insert(0, wangp_root)
    os.chdir(wangp_root)

    with open(manifest_path, "r", encoding="utf-8") as f:
        manifest = json.load(f)

    tasks = manifest["tasks"]
    frames_dir = manifest["frames_dir"]
    segments_dir = manifest["segments_dir"]
    num_tasks = len(tasks)

    print(f"\n{'='*60}")
    print(f"[RUNNER] {num_tasks} segment uretilecek (TEK SESSION)")
    print(f"[RUNNER] Model 1 kere yuklenecek, RAM'de kalacak")
    print(f"{'='*60}")

    # WanGP API'yi import et
    try:
        from shared.api import WanGPSession
    except ImportError as e:
        print(f"[RUNNER] HATA: WanGP API import edilemedi: {e}")
        print(f"[RUNNER] wangp_root: {wangp_root}")
        print(f"[RUNNER] sys.path: {sys.path[:3]}")
        sys.exit(1)

    # TEK SESSION AC - model 1 kere yuklenir
    # output_dir olarak ilk segmentin dizinini ver
    first_output_dir = tasks[0]["output_dir"]
    os.makedirs(first_output_dir, exist_ok=True)

    # ── CUDA OOM FIX: Video uretiminden once GPU bellegini temizle ──
    # Audio separation modeli GPU'da bellek parcalanmasina neden olabilir
    try:
        import torch
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
            torch.cuda.synchronize()
            gc.collect()
            free_mem = torch.cuda.mem_get_info()[0] / 1024**3
            total_mem = torch.cuda.mem_get_info()[1] / 1024**3
            print(f"[RUNNER] GPU bellek: {free_mem:.1f}/{total_mem:.1f} GiB bos (cache temizlendi)")
    except Exception as e:
        print(f"[RUNNER] GPU bellek temizleme uyarisi: {e}")

    print(f"[RUNNER] WanGPSession aciliyor...")
    print(f"[RUNNER] Output: {first_output_dir}")

    try:
        session = WanGPSession(
            root=wangp_root,
            output_dir=first_output_dir,
            console_output=True,
        )
    except Exception as e:
        print(f"[RUNNER] HATA: Session acilamadi: {e}")
        traceback.print_exc()
        sys.exit(1)

    print(f"[RUNNER] Session acildi, model yuklendi!\n")

    results = []
    total_start = time.time()

    for idx, task_info in enumerate(tasks):
        seg_output_dir = task_info["output_dir"]
        task_params = dict(task_info["params"])  # kopya al
        task_params["repeat_generation"] = 1

        # Output dizinini guncelle
        os.makedirs(seg_output_dir, exist_ok=True)
        try:
            session._output_dir = Path(seg_output_dir)
        except Exception:
            pass  # eger _output_dir yoksa sorun degil, WanGP kendi dizinine kaydeder

        print(f"\n{'='*60}")
        print(f"[SEGMENT {idx+1}/{num_tasks}]")
        print(f"  Output : {seg_output_dir}")
        print(f"  Image  : {task_params.get('image_start', 'N/A')}")
        print(f"  Audio  : {os.path.basename(task_params.get('audio_source', 'N/A'))}")
        if task_params.get('audio_guide'):
            print(f"  LipSync: ACIK")
        print(f"{'='*60}")

        # Mevcut mp4'leri kaydet (sonra yenileri bulmak icin)
        existing_mp4 = set(glob.glob(os.path.join(seg_output_dir, "*.mp4")))
        # WanGP bazen farkli dizine de kaydedebilir - session output_dir'i de kontrol et
        wangp_save_dir = seg_output_dir
        existing_mp4_save = set(glob.glob(os.path.join(str(first_output_dir), "*.mp4")))

        seg_start = time.time()

        # ── CUDA OOM FIX: Her segment oncesi GPU cache temizle ──
        try:
            import torch
            if torch.cuda.is_available():
                torch.cuda.empty_cache()
                gc.collect()
        except Exception:
            pass

        try:
            # run_task = submit_task + result bekle (senkron, basit)
            print(f"  [*] Video uretiliyor (run_task)...")
            result = session.run_task(task_params)
            seg_elapsed = time.time() - seg_start

            print(f"  [*] Task tamamlandi ({seg_elapsed:.1f}s)")
            print(f"  [*] Basarili: {result.success}")
            print(f"  [*] Dosyalar: {len(result.generated_files) if result.generated_files else 0}")
            if result.errors:
                for err in result.errors:
                    print(f"  [HATA] {err}")

            # Video bul - 4 kademeli arama
            video_path = None

            # Yontem 1: API sonucu (generated_files)
            if result.success and result.generated_files:
                for f in result.generated_files:
                    fstr = str(f)
                    if fstr.endswith(".mp4") and os.path.exists(fstr):
                        video_path = fstr
                        print(f"  [BULUNDU] API sonucu: {os.path.basename(fstr)}")
                        break

            # Yontem 2: Segment output dizininde yeni mp4
            if not video_path:
                current_mp4 = set(glob.glob(os.path.join(seg_output_dir, "*.mp4")))
                new_mp4 = current_mp4 - existing_mp4
                if new_mp4:
                    video_path = sorted(new_mp4, key=os.path.getsize, reverse=True)[0]
                    print(f"  [BULUNDU] Yeni mp4: {os.path.basename(video_path)}")

            # Yontem 3: Session'in ilk output dizininde yeni mp4
            if not video_path and str(first_output_dir) != seg_output_dir:
                current_mp4_save = set(glob.glob(os.path.join(str(first_output_dir), "*.mp4")))
                new_mp4_save = current_mp4_save - existing_mp4_save
                if new_mp4_save:
                    video_path = sorted(new_mp4_save, key=os.path.getsize, reverse=True)[0]
                    print(f"  [BULUNDU] Session dir: {os.path.basename(video_path)}")

            # Yontem 4: En buyuk mp4 (segment dir veya ilk dir)
            if not video_path:
                video_path = find_biggest_mp4(seg_output_dir)
                if not video_path and str(first_output_dir) != seg_output_dir:
                    video_path = find_biggest_mp4(first_output_dir)
                if video_path:
                    print(f"  [BULUNDU] Fallback: {os.path.basename(video_path)}")

            if video_path:
                size_mb = os.path.getsize(video_path) / 1024 / 1024
                print(f"  VIDEO: {os.path.basename(video_path)} ({size_mb:.1f}MB, {seg_elapsed:.1f}s)")

                # Segment klasorune kopyala
                seg_video = os.path.join(segments_dir, f"segment_{idx:03d}.mp4")
                shutil.copy2(video_path, seg_video)
                print(f"  -> {seg_video}")

                # Sonraki segment icin en iyi kareyi cikar
                if idx < num_tasks - 1:
                    frame_path = os.path.join(frames_dir, f"frame_{idx:03d}.jpg")
                    try:
                        extract_best_frame(video_path, frame_path)
                        tasks[idx + 1]["params"]["image_start"] = frame_path
                        print(f"  En iyi kare cikarildi -> sonraki segment kullanacak")
                    except Exception as e:
                        print(f"  UYARI: Son kare cikarilamadi: {e}")

                results.append({"index": idx, "success": True, "video": seg_video, "elapsed": seg_elapsed})
            else:
                print(f"  HATA: Video bulunamadi!")
                print(f"  Aranan dizinler: {seg_output_dir}")
                if str(first_output_dir) != seg_output_dir:
                    print(f"                   {first_output_dir}")
                # Dizindeki dosyalari listele (debug)
                all_files = glob.glob(os.path.join(seg_output_dir, "*"))
                print(f"  Dizindeki dosyalar ({len(all_files)}): {[os.path.basename(f) for f in all_files[:10]]}")
                results.append({"index": idx, "success": False, "elapsed": seg_elapsed})

        except Exception as e:
            seg_elapsed = time.time() - seg_start
            print(f"  EXCEPTION: {e}")
            traceback.print_exc()
            # Yine de video aramaya calis
            fallback = find_biggest_mp4(seg_output_dir)
            if not fallback and str(first_output_dir) != seg_output_dir:
                fallback = find_biggest_mp4(first_output_dir)
            if fallback:
                seg_video = os.path.join(segments_dir, f"segment_{idx:03d}.mp4")
                shutil.copy2(fallback, seg_video)
                print(f"  Fallback video bulundu: {os.path.basename(fallback)}")
                results.append({"index": idx, "success": True, "video": seg_video, "elapsed": seg_elapsed})
            else:
                results.append({"index": idx, "success": False, "elapsed": seg_elapsed})

    # Session kapat
    print(f"\n[RUNNER] Session kapatiliyor...")
    try:
        session.close()
    except Exception:
        pass

    total_elapsed = time.time() - total_start
    success_count = sum(1 for r in results if r["success"])
    print(f"\n{'='*60}")
    print(f"[RUNNER] TAMAMLANDI: {success_count}/{num_tasks} basarili ({total_elapsed:.1f}s)")
    print(f"{'='*60}")

    # Sonuclari kaydet
    result_path = manifest_path.replace(".json", "_results.json")
    with open(result_path, "w", encoding="utf-8") as f:
        json.dump({"results": results, "total_elapsed": total_elapsed}, f, indent=2)

    print(f"[RUNNER] Sonuclar: {result_path}")

if __name__ == "__main__":
    main()
'''
    with open(runner_script, "w", encoding="utf-8") as f:
        f.write(runner_code)

    # Manifest zaten caller (main) tarafindan yazilmis olmali
    # Burada sadece runner'i calistiriyoruz

    # WanGP Python ile runner'i calistir
    python_exe = str(WANGP_PYTHON)
    if not Path(python_exe).exists():
        python_exe = sys.executable

    print(f"\n[*] WanGP Batch Runner baslatiliyor (tek session)...")
    print(f"    Model 1 kere yuklenecek, tum segmentler sirayla uretilecek\n")

    # ── CUDA OOM FIX: expandable_segments ortam degiskeni ──
    runner_env = os.environ.copy()
    runner_env["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"

    try:
        proc = subprocess.run(
            [python_exe, str(runner_script), str(wangp_root), str(manifest_path)],
            timeout=timeout,
            capture_output=False,
            cwd=str(wangp_root),
            env=runner_env
        )
    except subprocess.TimeoutExpired:
        print(f"  HATA: Toplam zaman asimi ({timeout}s)!")
    except Exception as e:
        print(f"  HATA: {e}")

    # Sonuclari oku
    results_path = str(manifest_path).replace(".json", "_results.json")
    video_segments = []
    if Path(results_path).exists():
        with open(results_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        for r in data.get("results", []):
            if r.get("success") and r.get("video") and Path(r["video"]).exists():
                video_segments.append(Path(r["video"]))
    else:
        # Fallback: segments dizininde mp4 ara
        import glob
        for mp4 in sorted(glob.glob(str(segments_dir / "segment_*.mp4"))):
            video_segments.append(Path(mp4))

    return video_segments


# ============================================================================
# ANA PIPELINE
# ============================================================================

def main():
    parser = argparse.ArgumentParser(
        description="WanGP Müzik Video Üretici",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Örnek Kullanımlar:
  python wangp_video_uret.py sarki.mp3 gorsel.jpg
  python wangp_video_uret.py sarki.mp3 gorsel.jpg --model i2v_720p --resolution 1280x720
  python wangp_video_uret.py sarki.mp3 gorsel.jpg --model ltx2_22B --steps 40 --guidance 3.0
  python wangp_video_uret.py sarki.mp3 gorsel.jpg --lora "my_lora.safetensors" --lora-weight 0.8
  python wangp_video_uret.py sarki.mp3 gorsel.jpg --face-swap yuz.jpg

Modelleri Listele:
  python wangp_video_uret.py --list-models
  python wangp_video_uret.py --list-loras
        """
    )

    # Pozisyonel argümanlar
    parser.add_argument("audio", nargs="?", help="Giriş ses dosyası (mp3, wav, flac, vb.)")
    parser.add_argument("image", nargs="?", help="Giriş görseli (jpg, png)")

    # Model ve temel ayarlar
    model_group = parser.add_argument_group("Model Ayarları")
    model_group.add_argument("--model", "-m", default=DEFAULT_MODEL,
                            help=f"Video üretim modeli (varsayılan: {DEFAULT_MODEL})")
    model_group.add_argument("--resolution", "-r", default=DEFAULT_RESOLUTION,
                            help=f"Çözünürlük WIDTHxHEIGHT formatında (varsayılan: {DEFAULT_RESOLUTION})")
    model_group.add_argument("--video-length", type=int, default=DEFAULT_VIDEO_LENGTH,
                            help=f"Segment başına frame sayısı (varsayılan: {DEFAULT_VIDEO_LENGTH}). 4n+1 olmalı (81,121,161,...)")
    model_group.add_argument("--fps", type=int, default=DEFAULT_FPS,
                            help=f"Video FPS (varsayılan: {DEFAULT_FPS})")

    # Prompt ayarları
    prompt_group = parser.add_argument_group("Prompt Ayarları")
    prompt_group.add_argument("--prompt", "-p", default=DEFAULT_PROMPT,
                             help="Video üretim prompt'u")
    prompt_group.add_argument("--negative-prompt", "--neg", default=DEFAULT_NEGATIVE_PROMPT,
                             help="Negatif prompt (istenmeyen öğeler)")
    prompt_group.add_argument("--prompt-enhancer", type=int, default=1, choices=[0, 1],
                             help="Prompt geliştirici (0=kapalı, 1=açık)")
    prompt_group.add_argument("--auto-prompt", action="store_true",
                             help="Ses enerjisi analizine göre her segmente otomatik farklı prompt oluştur. "
                                  "Sessiz bölümlerde sakin, enerjik bölümlerde dinamik prompt kullanılır.")

    # Inference ayarları
    inf_group = parser.add_argument_group("Inference Ayarları")
    inf_group.add_argument("--steps", "-n", type=int, default=DEFAULT_STEPS,
                          help=f"Inference adım sayısı (varsayılan: {DEFAULT_STEPS})")
    inf_group.add_argument("--guidance-scale", "-g", type=float, default=DEFAULT_GUIDANCE_SCALE,
                          help=f"Guidance scale / CFG (varsayılan: {DEFAULT_GUIDANCE_SCALE})")
    inf_group.add_argument("--guidance2-scale", type=float, default=None,
                          help="İkinci faz guidance scale (opsiyonel)")
    inf_group.add_argument("--guidance3-scale", type=float, default=None,
                          help="Üçüncü faz guidance scale (opsiyonel)")
    inf_group.add_argument("--guidance-phases", type=int, default=None, choices=[1, 2, 3],
                          help="Guidance faz sayısı (1-3)")
    inf_group.add_argument("--embedded-guidance", type=float, default=None,
                          help="Embedded guidance scale (modele bağlı)")
    inf_group.add_argument("--alt-guidance", type=float, default=None,
                          help="Alternatif guidance scale")
    inf_group.add_argument("--alt-scale", type=float, default=None,
                          help="Alternatif scale (0.0-1.0)")
    inf_group.add_argument("--flow-shift", type=float, default=DEFAULT_FLOW_SHIFT,
                          help=f"Flow shift (varsayılan: {DEFAULT_FLOW_SHIFT})")
    inf_group.add_argument("--solver", default=DEFAULT_SOLVER,
                          help=f"Sampling algoritması (varsayılan: {DEFAULT_SOLVER}). Seçenekler: euler, unipc")
    inf_group.add_argument("--seed", type=int, default=DEFAULT_SEED,
                          help=f"Seed (-1=rastgele, varsayılan: {DEFAULT_SEED})")
    inf_group.add_argument("--denoising-strength", type=float, default=None,
                          help="Denoising gücü (0.0-1.0)")

    # Audio guidance ve lip-sync
    audio_group = parser.add_argument_group("Audio Guidance ve Lip-Sync Ayarları")
    audio_group.add_argument("--audio-guidance-scale", type=float, default=None,
                            help="Audio guidance scale (LTX2 varsayılan: 7.0)")
    audio_group.add_argument("--audio-scale", type=float, default=None,
                            help="Audio scale (0.0-1.0)")
    audio_group.add_argument("--audio-prompt-type", default=None,
                            help="Audio prompt tipi bayrakları: A=tek kişi lip-sync, V=vokal ayır, N=ses normalize, X=iki kişi otomatik, B=iki ayrı ses")
    audio_group.add_argument("--no-lip-sync", action="store_true",
                            help="Lip-sync'i devre dışı bırak (sesi sadece son videoya ekle, görüntüyü etkilemesin)")
    audio_group.add_argument("--no-vocal-isolation", action="store_true",
                            help="Vokal izolasyonu yapma (V bayrağını kaldır)")

    # LoRA ayarları
    lora_group = parser.add_argument_group("LoRA Ayarları")
    lora_group.add_argument("--lora", action="append", default=None,
                           help="LoRA dosyası (birden fazla --lora ile eklenebilir)")
    lora_group.add_argument("--lora-weight", default="1.0",
                           help="LoRA ağırlıkları (boşlukla ayrılmış, örn: '0.8 1.0')")

    # Sliding window (uzun video)
    sw_group = parser.add_argument_group("Sliding Window Ayarları (Uzun Video)")
    sw_group.add_argument("--sliding-window-size", type=int, default=DEFAULT_SLIDING_WINDOW_SIZE,
                         help="Sliding window frame boyutu (0=kapalı)")
    sw_group.add_argument("--sliding-window-overlap", type=int, default=DEFAULT_SLIDING_WINDOW_OVERLAP,
                         help="Sliding window overlap")
    sw_group.add_argument("--sliding-window-color-correction", type=float, default=0.0,
                         help="Renk düzeltme gücü (0.0-1.0)")
    sw_group.add_argument("--sliding-window-overlap-noise", type=int, default=0,
                         help="Overlap noise (0-150)")

    # Gelişmiş ayarlar
    adv_group = parser.add_argument_group("Gelişmiş Ayarlar")
    adv_group.add_argument("--perturbation", type=int, default=0, choices=[0, 1, 2, 3, 4],
                          help="Perturbation modu (0=kapalı)")
    adv_group.add_argument("--perturbation-layers", type=int, nargs="+", default=None,
                          help="Perturbation katmanları (örn: 28)")
    adv_group.add_argument("--perturbation-start", type=int, default=0,
                          help="Perturbation başlangıç yüzdesi (0-100)")
    adv_group.add_argument("--perturbation-end", type=int, default=100,
                          help="Perturbation bitiş yüzdesi (0-100)")
    adv_group.add_argument("--apg", type=int, default=0,
                          help="Adaptive Prompt Guidance modu")
    adv_group.add_argument("--cfg-star", type=int, default=0,
                          help="CFG* scheduling modu")
    adv_group.add_argument("--nag-scale", type=float, default=None,
                          help="Negative Attention Guidance scale")
    adv_group.add_argument("--nag-tau", type=float, default=None,
                          help="NAG tau parametresi")
    adv_group.add_argument("--nag-alpha", type=float, default=None,
                          help="NAG alpha parametresi")
    adv_group.add_argument("--self-refiner", type=int, default=0, choices=[0, 1, 2],
                          help="Self-refiner (0=kapalı, 1=P1-Norm, 2=P2-Norm)")
    adv_group.add_argument("--temporal-upsampling", default=None,
                          help="Temporal upsampling metodu")
    adv_group.add_argument("--spatial-upsampling", default=None,
                          help="Spatial upsampling metodu")
    adv_group.add_argument("--film-grain", type=float, default=0.0,
                          help="Film grain yoğunluğu (0.0-1.0)")
    adv_group.add_argument("--control-net-weight", type=float, default=None,
                          help="Control net ağırlığı (0.0-2.0)")
    adv_group.add_argument("--motion-amplitude", type=float, default=None,
                          help="Hareket genliği (1.0-1.4)")
    adv_group.add_argument("--image-end", default=None,
                          help="Bitiş görseli yolu (destekleyen modeller için)")

    # Segment ve çıkış ayarları
    out_group = parser.add_argument_group("Segment ve Çıkış Ayarları")
    out_group.add_argument("--segment-duration", "-s", type=float, default=DEFAULT_SEGMENT_DURATION,
                          help=f"Segment süresi saniye (varsayılan: {DEFAULT_SEGMENT_DURATION})")
    out_group.add_argument("--output", "-o", default=None,
                          help="Çıkış dosyası yolu")
    out_group.add_argument("--repeat", type=int, default=1,
                          help="Her segment için tekrar sayısı (en iyisini seç)")
    out_group.add_argument("--timeout", type=int, default=600,
                          help="Segment başına maksimum bekleme süresi (saniye)")

    # Face swap
    face_group = parser.add_argument_group("Face Swap Ayarları")
    face_group.add_argument("--face-swap", "-f", default=None,
                           help="Face swap için kaynak yüz resmi")

    # Bilgi komutları
    info_group = parser.add_argument_group("Bilgi Komutları")
    info_group.add_argument("--list-models", action="store_true",
                           help="Kullanılabilir modelleri listele")
    info_group.add_argument("--list-loras", action="store_true",
                           help="Kullanılabilir LoRA'ları listele")

    args = parser.parse_args()

    # Bilgi komutları
    if args.list_models:
        models = get_available_models()
        print("=" * 70)
        print("  KULLANILABILIR MODELLER")
        print("=" * 70)
        for model_id, info in models.items():
            print(f"  {model_id:<40} {info['name']}")
        print(f"\n  Toplam: {len(models)} model")
        print("=" * 70)
        return

    if args.list_loras:
        loras = get_available_loras()
        print("=" * 70)
        print("  KULLANILABILIR LoRA'LAR")
        print("=" * 70)
        for l in loras:
            print(f"  {l}")
        if not loras:
            print("  (LoRA bulunamadı)")
        print(f"\n  Toplam: {len(loras)} LoRA")
        print("=" * 70)
        return

    # Girdi dosyaları kontrolü
    if not args.audio or not args.image:
        parser.error("Ses dosyası ve görsel gerekli! Kullanım: wangp_video_uret.py <ses> <görsel>")

    audio_path = Path(args.audio).resolve()
    image_path = Path(args.image).resolve()

    if not audio_path.exists():
        print(f"HATA: Ses dosyası bulunamadı: {audio_path}")
        sys.exit(1)
    if not image_path.exists():
        print(f"HATA: Görsel dosyası bulunamadı: {image_path}")
        sys.exit(1)

    face_swap_source = None
    if args.face_swap:
        face_swap_source = Path(args.face_swap).resolve()
        if not face_swap_source.exists():
            print(f"HATA: Face swap resmi bulunamadı: {face_swap_source}")
            sys.exit(1)

    # End image kontrolü
    image_end_path = None
    if args.image_end:
        image_end_path = Path(args.image_end).resolve()
        if not image_end_path.exists():
            print(f"HATA: Bitiş görseli bulunamadı: {image_end_path}")
            sys.exit(1)

    # Segment süresi ve frame hesaplama
    # --segment-duration her zaman ana kaynak, frame sayisi bundan hesaplanir
    segment_dur = args.segment_duration
    frames_per_segment = duration_to_frames(segment_dur, args.fps)

    # Eger kullanici ayrica --video-length verdiyse ve varsayilandan farkliysa,
    # onu kullan (ileri kullanim icin)
    if args.video_length != DEFAULT_VIDEO_LENGTH:
        frames_per_segment = args.video_length

    segment_dur_actual = frames_per_segment / args.fps

    # Çalışma dizinleri
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    work_dir = Path(__file__).parent / "output" / f"wangp_video_{timestamp}"
    work_dir.mkdir(parents=True, exist_ok=True)
    segments_dir = work_dir / "segments"
    segments_dir.mkdir(exist_ok=True)
    frames_dir = work_dir / "frames"
    frames_dir.mkdir(exist_ok=True)
    audio_segments_dir = work_dir / "audio_segments"
    audio_segments_dir.mkdir(exist_ok=True)
    tasks_dir = work_dir / "tasks"
    tasks_dir.mkdir(exist_ok=True)

    # Çıkış dosyası
    if args.output:
        output_path = Path(args.output).resolve()
    else:
        stem = audio_path.stem
        output_path = work_dir / f"{stem}_wangp_video.mp4"

    # Başlık
    print("=" * 65)
    print("  WANGP MÜZİK VİDEO ÜRETİCİ")
    print("=" * 65)
    print(f"  Ses      : {audio_path}")
    print(f"  Görsel   : {image_path}")
    print(f"  Model    : {args.model}")
    print(f"  Çözünürlük: {args.resolution}")
    print(f"  Prompt   : {args.prompt[:60]}...")
    print(f"  Neg.Prompt: {args.negative_prompt[:50]}...")
    print(f"  Steps    : {args.steps}")
    print(f"  Guidance : {args.guidance_scale}")
    print(f"  Flow Shift: {args.flow_shift}")
    print(f"  Solver   : {args.solver}")
    print(f"  Seed     : {args.seed}")
    print(f"  FPS      : {args.fps}")
    print(f"  Frames   : {frames_per_segment} ({segment_dur_actual:.1f}s)")
    print(f"  Segment  : {segment_dur}s")
    if args.lora:
        print(f"  LoRA     : {', '.join(args.lora)}")
        print(f"  LoRA Wt  : {args.lora_weight}")
    if args.sliding_window_size > 0:
        print(f"  Sl.Window: {args.sliding_window_size} (overlap: {args.sliding_window_overlap})")
    if face_swap_source:
        print(f"  FaceSwap : {face_swap_source}")
    if image_end_path:
        print(f"  EndImage : {image_end_path}")
    print(f"  Çıkış    : {output_path}")
    print("=" * 65)

    # ── 1. Ses Süresini Al ve Segmentlere Böl ──
    total_duration = get_audio_duration(audio_path)
    print(f"\n[*] Toplam ses süresi: {total_duration:.1f}s")

    audio_segments = split_audio_into_segments(
        audio_path, audio_segments_dir, segment_dur, total_duration
    )

    num_segments = len(audio_segments)
    print(f"[*] {num_segments} segment üretilecek")

    # ── 2. İlk Görseli Hazırla ──
    input_image = frames_dir / f"input_000.jpg"
    shutil.copy2(image_path, input_image)

    if face_swap_source:
        print("\n[*] İlk görsele face swap uygulanıyor...")
        swapped_input = frames_dir / f"input_000_swapped.jpg"
        if face_swap_image(face_swap_source, input_image, swapped_input):
            input_image = swapped_input

    # ── 3. Tüm Segment Task'larını Hazırla ve Manifest Oluştur ──
    start_time = time.time()

    # Lip-sync desteği kontrolü
    model_supports_lipsync = args.model.startswith("ltx2") or args.model in [
        "multitalk", "vace_multitalk_14B", "infinitetalk",
        "magi_human", "longcat_avatar", "hunyuan_1_5_i2v_avatar",
    ]

    if not args.no_lip_sync and model_supports_lipsync:
        print(f"\n  [LIP-SYNC] ACIK - {args.model} destekliyor")
    elif not args.no_lip_sync and not model_supports_lipsync:
        print(f"\n  [LIP-SYNC] KAPALI - {args.model} modeli lip-sync desteklemiyor!")
        print(f"             Lip-sync icin LTX2 modeli kullanin (profil 1-3)")

    manifest_tasks = []

    for i, (seg_audio, seg_start, seg_end, seg_actual_dur) in enumerate(audio_segments):

        # Task ayarlarını oluştur
        task_params = {
            "model_type": args.model,
            "prompt": args.prompt,
            "negative_prompt": args.negative_prompt,
            "resolution": args.resolution,
            "video_length": frames_per_segment,
            "num_inference_steps": args.steps,
            "guidance_scale": args.guidance_scale,
            "flow_shift": args.flow_shift,
            "sample_solver": args.solver,
            "seed": args.seed,
            "prompt_enhancer": args.prompt_enhancer,
            "image_start": str(input_image),  # ilk segment icin; sonrakiler runner tarafindan guncellenir
            "audio_source": str(seg_audio),
            "image_prompt_type": "S",  # Start image
            "repeat_generation": 1,  # Tekrar yok, 1 video uret
        }

        # Lip-sync ayarlari
        if not args.no_lip_sync and model_supports_lipsync:
            task_params["audio_guide"] = str(seg_audio)
            apt_flags = "A"
            if not args.no_vocal_isolation:
                apt_flags += "V"
            apt_flags += "N"
            task_params["audio_prompt_type"] = apt_flags

        # Kullanicinin manuel audio_prompt_type ayari
        if args.audio_prompt_type is not None:
            task_params["audio_prompt_type"] = args.audio_prompt_type

        # Opsiyonel parametreler
        if args.guidance2_scale is not None:
            task_params["guidance2_scale"] = args.guidance2_scale
        if args.guidance3_scale is not None:
            task_params["guidance3_scale"] = args.guidance3_scale
        if args.guidance_phases is not None:
            task_params["guidance_phases"] = args.guidance_phases
        if args.embedded_guidance is not None:
            task_params["embedded_guidance_scale"] = args.embedded_guidance
        if args.alt_guidance is not None:
            task_params["alt_guidance_scale"] = args.alt_guidance
        if args.alt_scale is not None:
            task_params["alt_scale"] = args.alt_scale
        if args.denoising_strength is not None:
            task_params["denoising_strength"] = args.denoising_strength
        if args.audio_guidance_scale is not None:
            task_params["audio_guidance_scale"] = args.audio_guidance_scale
        if args.audio_scale is not None:
            task_params["audio_scale"] = args.audio_scale
        if args.model.startswith("ltx2") and "distilled" not in args.model and args.audio_guidance_scale is None:
            task_params["audio_guidance_scale"] = 7.0
        if args.control_net_weight is not None:
            task_params["control_net_weight"] = args.control_net_weight
        if args.motion_amplitude is not None:
            task_params["motion_amplitude"] = args.motion_amplitude
        if args.nag_scale is not None:
            task_params["NAG_scale"] = args.nag_scale
        if args.nag_tau is not None:
            task_params["NAG_tau"] = args.nag_tau
        if args.nag_alpha is not None:
            task_params["NAG_alpha"] = args.nag_alpha

        # End image (sadece son segment)
        if image_end_path and i == num_segments - 1:
            task_params["image_end"] = str(image_end_path)
            task_params["image_prompt_type"] = "SE"

        # LoRA
        if args.lora:
            loras_dir = WANGP_ROOT / "loras"
            lora_paths = []
            for l in args.lora:
                lp = loras_dir / l
                if lp.exists():
                    lora_paths.append(str(lp))
                elif Path(l).exists():
                    lora_paths.append(str(Path(l).resolve()))
                else:
                    print(f"  UYARI: LoRA bulunamadı: {l}")
            if lora_paths:
                task_params["activated_loras"] = lora_paths
                task_params["loras_multipliers"] = args.lora_weight

        # Sliding window
        if args.sliding_window_size > 0:
            task_params["sliding_window_size"] = args.sliding_window_size
            task_params["sliding_window_overlap"] = args.sliding_window_overlap
            task_params["sliding_window_color_correction_strength"] = args.sliding_window_color_correction
            task_params["sliding_window_overlap_noise"] = args.sliding_window_overlap_noise

        # Perturbation
        if args.perturbation > 0:
            task_params["perturbation_switch"] = args.perturbation
            task_params["perturbation_start_perc"] = args.perturbation_start
            task_params["perturbation_end_perc"] = args.perturbation_end
            if args.perturbation_layers:
                task_params["perturbation_layers"] = args.perturbation_layers

        # APG / CFG*
        if args.apg > 0:
            task_params["apg_switch"] = args.apg
        if args.cfg_star > 0:
            task_params["cfg_star_switch"] = args.cfg_star

        # Self-refiner
        if args.self_refiner > 0:
            task_params["self_refiner_setting"] = args.self_refiner

        # Post-processing
        if args.temporal_upsampling:
            task_params["temporal_upsampling"] = args.temporal_upsampling
        if args.spatial_upsampling:
            task_params["spatial_upsampling"] = args.spatial_upsampling
        if args.film_grain > 0:
            task_params["film_grain_intensity"] = args.film_grain

        # FPS override
        if args.fps != DEFAULT_FPS:
            task_params["force_fps"] = args.fps

        # Output dizini
        video_output_dir = tasks_dir / f"seg_{i:03d}"
        video_output_dir.mkdir(exist_ok=True)

        manifest_tasks.append({
            "output_dir": str(video_output_dir),
            "params": task_params,
        })

        print(f"  Task {i+1}/{num_segments}: {seg_start:.1f}s-{seg_end:.1f}s -> {video_output_dir.name}")

    # Manifest JSON'u yaz
    manifest_path = tasks_dir / "manifest.json"
    manifest_data = {
        "tasks": manifest_tasks,
        "frames_dir": str(frames_dir),
        "segments_dir": str(segments_dir),
    }
    with open(manifest_path, "w", encoding="utf-8") as f:
        json.dump(manifest_data, f, indent=2, ensure_ascii=False)

    print(f"\n[*] Manifest yazildi: {manifest_path}")
    print(f"[*] {num_segments} segment TEK SESSION'da uretilecek (model 1 kere yuklenir)\n")

    # Tum segmentleri tek session'da uret
    total_timeout = args.timeout * num_segments + 300  # ekstra 5dk tampon
    video_segments = generate_all_segments(
        manifest_path=manifest_path,
        wangp_root=WANGP_ROOT,
        tasks_dir=tasks_dir,
        segments_dir=segments_dir,
        frames_dir=frames_dir,
        face_swap_source=face_swap_source,
        timeout=total_timeout,
    )

    # Face swap (runner son kareleri cikarip zincirler, ama face swap burada yapilir)
    if face_swap_source and video_segments:
        print("\n[*] Face swap segment videolarina uygulanacak...")
        # Face swap sadece son karelere uygulanir (runner'da zaten yapiliyor)
        # Ama eger istenirse segment videolarina da uygulanabilir (opsiyonel)

    if not video_segments:
        print("\nHATA: Hiç video segmenti üretilemedi!")
        sys.exit(1)

    # ── 4. Videoları Birleştir ──
    print(f"\n{'=' * 60}")
    print("[*] Videolar birleştiriliyor...")
    concat_video = work_dir / "concat_raw.mp4"

    if len(video_segments) == 1:
        shutil.copy2(video_segments[0], concat_video)
    else:
        concatenate_videos(video_segments, concat_video)

    # ── 5. Orijinal Tam Sesi Ekle ──
    print("[*] Orijinal şarkı ekleniyor...")
    add_audio_to_video(concat_video, audio_path, output_path)

    # ── 6. Tamamlandı ──
    total_time = time.time() - start_time
    file_size = output_path.stat().st_size / (1024 * 1024)

    print(f"\n{'=' * 65}")
    print(f"  TAMAMLANDI!")
    print(f"{'=' * 65}")
    print(f"  Çıkış     : {output_path}")
    print(f"  Boyut     : {file_size:.1f} MB")
    print(f"  Segmentler: {len(video_segments)}")
    print(f"  Toplam süre: {total_time:.0f} saniye ({total_time/60:.1f} dakika)")
    print(f"  Seg. başına: {total_time/len(video_segments):.1f}s ortalama")
    print(f"{'=' * 65}")

    return str(output_path)


if __name__ == "__main__":
    main()
