#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Müzik Video Üretici - LTX Desktop Backend API
===============================================
Şarkı + görsel girişi → otomatik uzun müzik videosu üretir.

Pipeline:
1. Şarkıyı segmentlere böl (ffmpeg) — her segment için ayrı ses dosyası
2. Her segment için LTX Desktop API ile video üret (LTX 2.3 Fast model)
   - imagePath: giriş görseli (ilk segment) veya önceki videonun son karesi
   - audioPath: o segmente karşılık gelen ses parçası (dudak senkronizasyonu)
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 muzik_video_uret.py <sarki.mp3> <gorsel.jpg> [--prompt "açıklama"] [--resolution 540p]
    
    veya sürükle-bırak ile Muzik-Video-Uret.bat kullanın.

Çözünürlüğe göre max segment süreleri:
    540p  → max 20 saniye
    720p  → max 10 saniye
    1080p → max 5 saniye
"""

import os
import sys
import json
import time
import shutil
import ctypes
import struct
import argparse
import subprocess
import urllib.request
import urllib.error
from pathlib import Path
from datetime import datetime

# ============================================================================
# AYARLAR
# ============================================================================
LTX_API_URL = None  # Otomatik bulunacak (port her başlatmada değişebilir)
LTX_OUTPUT_DIR = Path(os.environ.get("LOCALAPPDATA", "")) / "LTXDesktop" / "outputs"

# WanGP ayarları (InfiniteTalk modu için)
WANGP_ROOT = Path(__file__).parent / "LTX-Desktop-WanGP" / "LTX-Desktop-WanGP" / "Wan2GP"
WANGP_PYTHON = WANGP_ROOT / "env_venv" / "Scripts" / "python.exe"
SEGMENT_DURATION = 20      # 540p'de 20 saniyeye kadar destekleniyor
VIDEO_MODEL = "fast"       # "fast" = LTX 2.3 Fast (hızlı), "pro" = yüksek kalite ama yavaş
DEFAULT_RESOLUTION = "540p" # "512p", "540p", "720p", "1080p"
DEFAULT_ASPECT = "16:9"    # "16:9" veya "9:16"
DEFAULT_FPS = "24"
DEFAULT_GUIDANCE_SCALE = 3.5  # 1.0-15.0 arası — düşük=serbest, yüksek=prompt'a bağlı
DEFAULT_STEPS = 30            # 10-50 arası — az=hızlı/düşük kalite, çok=yavaş/yüksek kalite
POLL_INTERVAL = 1          # API durumu sorgu aralığı (saniye)
MAX_WAIT_TIME = 600        # Tek bir video için max bekleme (saniye)

FFMPEG = "ffmpeg"
FFPROBE = "ffprobe"

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

# Çözünürlüğe göre max segment süresi (saniye)
MAX_DURATION_BY_RESOLUTION = {
    "512p": 20,
    "540p": 20,
    "720p": 10,
    "1080p": 5,
}

# LTX API'nin kabul ettiği süre değerleri (saniye)
ALLOWED_DURATIONS = {5, 6, 8, 10, 20}

def get_max_segment_duration(resolution):
    """Çözünürlüğe göre izin verilen max segment süresini döndürür."""
    return MAX_DURATION_BY_RESOLUTION.get(resolution, 10)

def snap_to_allowed_duration(desired_dur, resolution):
    """İstenen süreyi LTX'in kabul ettiği en yakın süreye yuvarlar."""
    max_dur = get_max_segment_duration(resolution)
    allowed = sorted([d for d in ALLOWED_DURATIONS if d <= max_dur])
    if not allowed:
        return 5
    best = allowed[0]
    for a in allowed:
        if a <= desired_dur:
            best = a
    return best


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

# Ses enerji seviyelerine göre prompt şablonları
# Her şablon kullanıcının ana prompt'una eklenir (prefix/suffix olarak)
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.

    ffprobe ile RMS loudness değerlerini ölçer ve segmentleri
    sessizlik/düşük/orta/yüksek/klimaks olarak sınıflandırır.

    Args:
        audio_path: Ses dosyası yolu
        segment_boundaries: [(start, end), ...] segment sınırları

    Returns:
        list: Her segment için enerji seviyesi ("silence"/"low"/"medium"/"high"/"climax")
    """
    # Her segment için RMS enerji ölç
    rms_values = []

    for seg_start, seg_end in segment_boundaries:
        seg_dur = seg_end - seg_start
        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)
            # RMS değerlerini topla
            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

            if values:
                # Ortalama RMS (dB cinsinden, negatif: -inf=sessiz, 0=max)
                avg_rms = sum(values) / len(values)
                rms_values.append(avg_rms)
            else:
                rms_values.append(-60.0)  # Sessiz varsay
        except Exception:
            rms_values.append(-60.0)

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

    # RMS değerlerini normalize et ve sınıflandır
    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:
            # 0-1 arası normalize
            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.

    Args:
        base_prompt: Kullanıcının verdiği ana prompt
        energy_level: "silence", "low", "medium", "high", "climax"

    Returns:
        str: Zenginleştirilmiş prompt
    """
    template = ENERGY_PROMPT_TEMPLATES.get(energy_level, ENERGY_PROMPT_TEMPLATES["medium"])
    return template["prefix"] + base_prompt + template["suffix"]


# ============================================================================
# PORT ve BAĞLANTI BULMA
# ============================================================================

def find_ltx_backend_port():
    """LTX Desktop backend'inin hangi portta çalıştığını otomatik bulur.
    
    ltx2_server.py sürecinin dinlediği portu netstat/PowerShell ile tespit eder.
    Port her LTX Desktop başlatmada değişebilir (57835, 62106, vb.)
    """
    print("[*] LTX Desktop backend portu aranıyor...")
    
    # Yöntem 1: wmic + netstat ile ltx2_server.py sürecinin portunu bul
    try:
        # ltx2_server.py çalıştıran python PID'lerini bul
        result = subprocess.run(
            ['wmic', 'process', 'where',
             "CommandLine like '%ltx2_server.py%'",
             'get', 'ProcessId', '/format:list'],
            capture_output=True, text=True, timeout=10
        )
        
        pids = []
        for line in result.stdout.strip().split('\n'):
            line = line.strip()
            if line.startswith('ProcessId='):
                try:
                    pids.append(int(line.split('=')[1]))
                except ValueError:
                    pass
        
        if not pids:
            print("  HATA: ltx2_server.py süreci bulunamadı!")
            print("  LTX Desktop açık olduğundan emin olun.")
            sys.exit(1)
        
        # Her PID için dinlenen portu bul (netstat ile)
        for pid in pids:
            result = subprocess.run(
                ['netstat', '-ano'],
                capture_output=True, text=True, timeout=10
            )
            for line in result.stdout.split('\n'):
                line = line.strip()
                if f'{pid}' in line and 'LISTENING' in line:
                    parts = line.split()
                    if len(parts) >= 4:
                        addr = parts[1]  # 127.0.0.1:62106 veya 0.0.0.0:62106
                        if ':' in addr:
                            port_str = addr.rsplit(':', 1)[-1]
                            try:
                                port = int(port_str)
                                if port > 1024:  # Geçerli port
                                    url = f"http://127.0.0.1:{port}"
                                    print(f"  Port bulundu: {port} (PID {pid})")
                                    return url
                            except ValueError:
                                pass
    except Exception as e:
        print(f"  Port arama hatası: {e}")
    
    # Yöntem 2: Bilinen portları dene
    print("  Bilinen portlar deneniyor...")
    for port in [57835, 62106, 8000, 8080]:
        try:
            req = urllib.request.Request(f"http://127.0.0.1:{port}/api/generation/progress")
            urllib.request.urlopen(req, timeout=2)
            url = f"http://127.0.0.1:{port}"
            print(f"  Port bulundu (tarama): {port}")
            return url
        except urllib.error.HTTPError as e:
            if e.code == 401:  # Auth gerekli = sunucu çalışıyor!
                url = f"http://127.0.0.1:{port}"
                print(f"  Port bulundu (tarama): {port}")
                return url
        except Exception:
            continue
    
    print("  HATA: LTX Desktop backend'i bulunamadı!")
    print("  LTX Desktop'u açın ve tekrar deneyin.")
    sys.exit(1)


# ============================================================================
# TOKEN BULMA
# ============================================================================

def get_token_from_process_env():
    """LTX backend Python sürecinin environment değişkenlerinden token okur."""
    try:
        import psutil
        for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
            try:
                cmdline = " ".join(proc.info.get('cmdline') or [])
                if 'ltx2_server.py' in cmdline:
                    env = proc.environ()
                    token = env.get('LTX_AUTH_TOKEN', '')
                    if token:
                        print(f"  [TOKEN] psutil ile bulundu (PID {proc.pid})")
                        return token
            except (psutil.AccessDenied, psutil.NoSuchProcess):
                continue
    except ImportError:
        pass
    
    try:
        return _read_token_via_winapi()
    except Exception as e:
        print(f"  [TOKEN] Windows API ile okunamadı: {e}")
    
    return None


def _read_token_via_winapi():
    """Windows API ile çalışan LTX backend sürecinin env değişkenini okur."""
    kernel32 = ctypes.windll.kernel32
    ntdll = ctypes.windll.ntdll
    
    result = subprocess.run(
        ['wmic', 'process', 'where', 
         "CommandLine like '%ltx2_server.py%'",
         'get', 'ProcessId', '/format:list'],
        capture_output=True, text=True, timeout=10
    )
    
    pids = []
    for line in result.stdout.strip().split('\n'):
        line = line.strip()
        if line.startswith('ProcessId='):
            try:
                pids.append(int(line.split('=')[1]))
            except ValueError:
                pass
    
    if not pids:
        return None
    
    PROCESS_QUERY_INFORMATION = 0x0400
    PROCESS_VM_READ = 0x0010
    
    class PBI(ctypes.Structure):
        _fields_ = [
            ("Reserved1", ctypes.c_void_p),
            ("PebBaseAddress", ctypes.c_void_p),
            ("Reserved2", ctypes.c_void_p * 2),
            ("UniqueProcessId", ctypes.c_void_p),
            ("Reserved3", ctypes.c_void_p),
        ]
    
    for pid in pids:
        handle = kernel32.OpenProcess(
            PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid
        )
        if not handle:
            continue
        
        try:
            pbi = PBI()
            ret_len = ctypes.c_ulong()
            status = ntdll.NtQueryInformationProcess(
                handle, 0, ctypes.byref(pbi), ctypes.sizeof(pbi), ctypes.byref(ret_len)
            )
            if status != 0:
                continue
            
            peb = (ctypes.c_byte * 0x400)()
            read = ctypes.c_size_t()
            if not kernel32.ReadProcessMemory(
                handle, pbi.PebBaseAddress, peb, 0x400, ctypes.byref(read)
            ):
                continue
            
            pp_addr = struct.unpack_from("<Q", bytes(peb), 0x20)[0]
            
            pp = (ctypes.c_byte * 0x400)()
            if not kernel32.ReadProcessMemory(
                handle, pp_addr, pp, 0x400, ctypes.byref(read)
            ):
                continue
            
            env_addr = struct.unpack_from("<Q", bytes(pp), 0x80)[0]
            
            env_buf = (ctypes.c_byte * 65536)()
            if not kernel32.ReadProcessMemory(
                handle, env_addr, env_buf, 65536, ctypes.byref(read)
            ):
                continue
            
            env_data = bytes(env_buf)[:read.value]
            env_str = env_data.decode("utf-16-le", errors="ignore")
            for var in env_str.split("\0"):
                if var.startswith("LTX_AUTH_TOKEN="):
                    token = var[len("LTX_AUTH_TOKEN="):]
                    print(f"  [TOKEN] Windows API ile bulundu (PID {pid})")
                    return token
        finally:
            kernel32.CloseHandle(handle)
    
    return None


def discover_auth_token(api_url):
    """LTX Desktop auth token'ını otomatik bul."""
    print("[*] LTX Desktop auth token aranıyor...")
    
    # 1. Auth'suz dene
    try:
        req = urllib.request.Request(f"{api_url}/api/generation/progress")
        resp = urllib.request.urlopen(req, timeout=5)
        print("  [TOKEN] Auth gerekmiyor!")
        return None
    except urllib.error.HTTPError as e:
        if e.code == 401:
            print("  [TOKEN] Auth gerekli, token aranıyor...")
        else:
            raise
    except urllib.error.URLError:
        print("  HATA: LTX Desktop'a bağlanılamıyor!")
        sys.exit(1)
    
    # 2. Ortam değişkeninden
    env_token = os.environ.get("LTX_AUTH_TOKEN", "")
    if env_token:
        print("  [TOKEN] Ortam değişkeninden bulundu")
        return env_token
    
    # 3. token dosyasından (ve geçerliliğini kontrol et)
    token_file = Path(__file__).parent / "ltx_token.txt"
    if token_file.exists():
        token = token_file.read_text().strip()
        if token:
            try:
                req = urllib.request.Request(
                    f"{api_url}/api/generation/progress",
                    headers={"Authorization": f"Bearer {token}"}
                )
                urllib.request.urlopen(req, timeout=5)
                print(f"  [TOKEN] ltx_token.txt dosyasından bulundu")
                return token
            except urllib.error.HTTPError:
                print(f"  [TOKEN] ltx_token.txt'deki token geçersiz (LTX yeniden başlatılmış)")
            except urllib.error.URLError:
                pass
    
    # 4. Çalışan süreçten
    token = get_token_from_process_env()
    if token:
        token_file.write_text(token)
        return token
    
    # 5. Manuel
    print("\n" + "=" * 60)
    print("AUTH TOKEN BULUNAMADI!")
    print("=" * 60)
    print("LTX Desktop'ta DevTools > Network sekmesini açın,")
    print("bir video üretin, ve istekteki 'authorization' header'ından")
    print("Bearer token'ı kopyalayıp buraya yapıştırın.")
    print("=" * 60)
    token = input("Token: ").strip()
    if token.startswith("Bearer "):
        token = token[7:]
    if token:
        token_file.write_text(token)
        return token
    
    print("HATA: Token girilmedi!")
    sys.exit(1)


# ============================================================================
# LTX API
# ============================================================================

def ltx_api_request(api_url, endpoint, method="GET", data=None, token=None):
    """LTX Desktop backend API'sine istek gönderir."""
    url = f"{api_url}{endpoint}"
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    
    if data is not None:
        body = json.dumps(data).encode("utf-8")
        req = urllib.request.Request(url, data=body, headers=headers, method=method)
    else:
        req = urllib.request.Request(url, headers=headers, method=method)
    
    resp = urllib.request.urlopen(req, timeout=MAX_WAIT_TIME)
    return json.loads(resp.read().decode())


def check_ltx_status(api_url, token):
    """LTX Desktop backend durumunu kontrol eder."""
    try:
        return ltx_api_request(api_url, "/api/generation/progress", token=token)
    except Exception:
        return None


def wait_for_idle(api_url, token, max_wait=120):
    """Eğer LTX meşgulse tamamlanmasını bekle."""
    print("[*] LTX Desktop durumu kontrol ediliyor...")
    start = time.time()
    while time.time() - start < max_wait:
        status = check_ltx_status(api_url, token)
        if status is None:
            print("  HATA: LTX Desktop'a bağlanılamıyor! Açık olduğundan emin olun.")
            sys.exit(1)
        
        if status["status"] in ("idle", "complete", "error", "cancelled"):
            print(f"  LTX Desktop hazır (durum: {status['status']})")
            return True
        
        phase = status.get("phase", "?")
        progress = status.get("progress", 0)
        print(f"  Bekleniyor... [{phase}] %{progress}")
        time.sleep(2)
    
    print("  UYARI: LTX Desktop hâlâ meşgul, yine de devam ediliyor...")
    return False


def generate_video(api_url, prompt, image_path, audio_path, token, 
                   resolution=DEFAULT_RESOLUTION, aspect=DEFAULT_ASPECT, 
                   duration="20", guidance_scale=DEFAULT_GUIDANCE_SCALE,
                   steps=DEFAULT_STEPS):
    """LTX Desktop API ile tek bir video segmenti üretir."""
    
    image_abs = str(Path(image_path).resolve())
    audio_abs = str(Path(audio_path).resolve())
    
    payload = {
        "prompt": prompt,
        "model": VIDEO_MODEL,
        "duration": duration,
        "resolution": resolution,
        "fps": DEFAULT_FPS,
        "audio": "true",
        "cameraMotion": "none",
        "aspectRatio": aspect,
        "negativePrompt": "blurry, low quality, distorted face, extra fingers, multiple people, bright daylight, cartoon, animated",
        "imagePath": image_abs,
        "audioPath": audio_abs,
        "guidanceScale": guidance_scale,
        "numInferenceSteps": steps
    }
    
    print(f"  API isteği gönderiliyor...")
    print(f"    Model: {VIDEO_MODEL} | Süre: {duration}s | Çözünürlük: {resolution}")
    print(f"    Guidance Scale: {guidance_scale} | Steps: {steps}")
    print(f"    Görsel: {image_abs}")
    print(f"    Ses: {audio_abs}")
    
    try:
        result = ltx_api_request(api_url, "/api/generate", method="POST", data=payload, token=token)
    except urllib.error.HTTPError as e:
        error_body = e.read().decode()
        print(f"  HATA: API hatası {e.code}: {error_body}")
        return None
    except Exception as e:
        print(f"  HATA: API bağlantı hatası: {e}")
        return None
    
    status = result.get("status", "")
    video_path = result.get("video_path", "")
    
    if status == "complete" and video_path:
        print(f"  Video üretildi: {video_path}")
        return video_path
    else:
        print(f"  HATA: Video üretilemedi. Durum: {status}")
        print(f"  Yanıt: {json.dumps(result, indent=2)}")
        return None


# ============================================================================
# FFMPEG YARDIMCI FONKSİYONLARI
# ============================================================================

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.
    
    Returns:
        List of (segment_audio_path, start_time, end_time, actual_duration)
    """
    segments = []
    num_segments = int(total_duration // segment_dur)
    remainder = total_duration % segment_dur
    # FİX: Fazla segment oluşturma — remainder >= 5.0 ise yeni segment aç
    # Çünkü 5s altı audio WanGP'nin vokal ayırıcısını crash ettiriyor
    if remainder >= 5.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}.mp3"
        
        cmd = [
            FFMPEG, "-y",
            "-i", str(audio_path),
            "-ss", str(seg_start),
            "-t", str(seg_actual_dur),
            "-acodec", "libmp3lame",
            "-b:a", "192k",
            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")
    
    return segments


def extract_last_frame(video_path, output_path):
    """Videonun son karesini çıkarır."""
    cmd = [
        FFPROBE, "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        str(video_path)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    duration = float(result.stdout.strip())
    
    seek_time = max(0, duration - 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}")


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 (her biri ayrı dosyaya)
    skip_last = 2  # En son 2 kareyi tamamen atla (genelde siyah/bozuk)
    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ı ile en keskin kareyi bul
    # (Yüksek varyans = daha keskin/detaylı, düşük = bulanık)
    best_score = -1
    best_file = candidate_files[-1]  # fallback: en son aday

    for cf in candidate_files:
        try:
            # PPM'e çevirip piksel verisi oku (harici kütüphane gerektirmez)
            ppm_path = cf + ".ppm"
            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
            # Basit kenar tespiti: komşu piksel farkları toplamı (Laplacian yaklaşımı)
            # Her piksel ile sağ ve alt komşusu arasındaki fark
            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

    # En iyi kareyi hedefe kopyala
    shutil.copy2(best_file, str(output_path))

    # Temizlik
    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.

    Args:
        source_face_path: Kaynak yüz resmi (kimin yüzü konacak)
        target_image_path: Hedef resim (yüzü değiştirilecek)
        output_path: Çıkış resmi yolu

    Returns:
        True başarılı, False başarısız
    """
    if not FACEFUSION_VENV_PYTHON.exists():
        print(f"  HATA: FaceFusion kurulu değil! ({FACEFUSION_VENV_PYTHON})")
        return False

    ff_script = FACEFUSION_SCRIPT
    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())

    # CUDA DLL'leri için PATH ayarla
    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():
                bin_dir = d / "bin"
                lib_dir = d / "lib"
                if bin_dir.exists():
                    extra_paths.append(str(bin_dir))
                if lib_dir.exists():
                    extra_paths.append(str(lib_dir))
    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] Kaynak yüz: {source_abs}")
    print(f"  [FACE SWAP] Hedef resim: {target_abs}")
    print(f"  [FACE SWAP] Çıkış: {output_abs}")

    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ı!")
            if result.stderr:
                print(f"  stderr: {result.stderr[:500]}")
            return False
    except subprocess.TimeoutExpired:
        print(f"  [FACE SWAP] HATA: Zaman aşımı (120s)!")
        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}")


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}")


# ============================================================================
# INFINITETALK PIPELINE (WanGP ÜZERİNDEN TEK SEFERLİK UZUN VİDEO)
# ============================================================================

def run_infinitetalk_pipeline(audio_path, image_path, prompt, output_path, work_dir,
                               guidance_scale=5.0, steps=30, fps=16):
    """InfiniteTalk modeli ile segmentsiz tek seferde uzun video üretir.

    InfiniteTalk, sliding window ile uzun video üretir — segmentlere bölme
    ihtiyacı ortadan kalkar, geçiş sorunları yok olur.

    WanGP Python API'si kullanılır (model: infinitetalk).

    Args:
        audio_path: Ses dosyası yolu
        image_path: Giriş görseli
        prompt: Video prompt'u
        output_path: Final çıkış dosyası
        work_dir: Çalışma dizini
        guidance_scale: CFG değeri
        steps: Inference adım sayısı
        fps: Video FPS (InfiniteTalk varsayılan 16)

    Returns:
        str: Çıkış dosyası yolu veya None
    """
    total_duration = get_audio_duration(audio_path)
    total_frames = int(total_duration * fps)
    # 4n+1 formatına yuvarla (WanGP gereksinimleri)
    remainder = (total_frames - 1) % 4
    if remainder != 0:
        total_frames = total_frames + (4 - remainder)
    total_frames = max(81, total_frames)

    print(f"\n[INFINITETALK] Tek seferde uzun video üretimi")
    print(f"  Toplam süre: {total_duration:.1f}s")
    print(f"  Toplam frame: {total_frames} ({total_frames/fps:.1f}s @ {fps}fps)")
    print(f"  Sliding window: 81 frame")
    print(f"  Model: infinitetalk (Wan2.1 InfiniteTalk Single 480p 14B)")

    # WanGP runner script'i oluştur
    runner_script = work_dir / "_infinitetalk_runner.py"
    runner_code = f'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""InfiniteTalk Runner - tek seferde uzun video."""
import sys, os, json, time, glob, shutil
from pathlib import Path

# CUDA bellek parçalanmasını önle (OOM fix)
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"

wangp_root = r"{str(WANGP_ROOT)}"
if wangp_root not in sys.path:
    sys.path.insert(0, wangp_root)
os.chdir(wangp_root)

output_dir = r"{str(work_dir)}"
os.makedirs(output_dir, exist_ok=True)

print("[INFINITETALK] WanGP API import ediliyor...")
try:
    from shared.api import WanGPSession
except ImportError as e:
    print(f"HATA: WanGP API import edilemedi: {{e}}")
    sys.exit(1)

# ── CUDA OOM FIX: Video uretiminden once GPU bellegini temizle ──
import gc
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"[INFINITETALK] GPU bellek: {{free_mem:.1f}}/{{total_mem:.1f}} GiB bos (cache temizlendi)")
except Exception as e:
    print(f"[INFINITETALK] GPU bellek temizleme uyarisi: {{e}}")

print("[INFINITETALK] Session aciliyor (model yukleniyor)...")
session = WanGPSession(
    root=wangp_root,
    output_dir=output_dir,
    console_output=True,
)

task_params = {{
    "model_type": "infinitetalk",
    "prompt": {json.dumps(prompt)},
    "negative_prompt": "blurry, low quality, distorted face, deformed, extra fingers, multiple people, cartoon, animated",
    "resolution": "832x480",
    "video_length": {total_frames},
    "num_inference_steps": {steps},
    "guidance_scale": {guidance_scale},
    "flow_shift": 7.0,
    "sample_solver": "unipc",
    "seed": -1,
    "prompt_enhancer": 1,
    "image_start": r"{str(image_path)}",
    "image_prompt_type": "S",
    "audio_source": r"{str(audio_path)}",
    "audio_guide": r"{str(audio_path)}",
    "audio_prompt_type": "AVN",
    "sliding_window_size": 81,
    "sliding_window_overlap": 1,
    "sliding_window_color_correction_strength": 0.0,
    "sliding_window_overlap_noise": 0,
    "repeat_generation": 1,
}}

print("[INFINITETALK] Video uretimi basliyor...")
start = time.time()
result = session.run_task(task_params)
elapsed = time.time() - start

print(f"[INFINITETALK] Tamamlandi ({{elapsed:.1f}}s)")
print(f"  Basarili: {{result.success}}")

video_path = None
if result.success and result.generated_files:
    for f in result.generated_files:
        if str(f).endswith(".mp4") and os.path.exists(str(f)):
            video_path = str(f)
            break

if not video_path:
    # Fallback: dizinde en buyuk mp4
    mp4s = glob.glob(os.path.join(output_dir, "**", "*.mp4"), recursive=True)
    if mp4s:
        mp4s.sort(key=os.path.getsize, reverse=True)
        video_path = mp4s[0]

if video_path:
    result_json = os.path.join(output_dir, "_infinitetalk_result.json")
    with open(result_json, "w") as f:
        json.dump({{"video": video_path, "elapsed": elapsed, "success": True}}, f)
    print(f"[INFINITETALK] Video: {{video_path}}")
else:
    result_json = os.path.join(output_dir, "_infinitetalk_result.json")
    with open(result_json, "w") as f:
        json.dump({{"success": False, "elapsed": elapsed}}, f)
    print("[INFINITETALK] HATA: Video bulunamadi!")

try:
    session.close()
except:
    pass
'''
    with open(runner_script, "w", encoding="utf-8") as f:
        f.write(runner_code)

    # WanGP Python ile çalıştır
    python_exe = str(WANGP_PYTHON)
    if not WANGP_PYTHON.exists():
        print(f"  HATA: WanGP Python bulunamadı: {WANGP_PYTHON}")
        print(f"  InfiniteTalk modu WanGP kurulumu gerektirir.")
        return None

    print(f"\n[INFINITETALK] Runner başlatılıyor...")
    timeout = int(total_duration * 60) + 600  # Her saniye video için ~60s üretim + 10dk tampon

    # ── 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)],
            timeout=timeout,
            capture_output=False,
            cwd=str(WANGP_ROOT),
            env=runner_env
        )
    except subprocess.TimeoutExpired:
        print(f"  HATA: Zaman aşımı ({timeout}s)!")
        return None
    except Exception as e:
        print(f"  HATA: {e}")
        return None

    # Sonucu oku
    result_json = work_dir / "_infinitetalk_result.json"
    if result_json.exists():
        with open(result_json, "r") as f:
            data = json.load(f)
        if data.get("success") and data.get("video"):
            raw_video = Path(data["video"])
            if raw_video.exists():
                # Orijinal tam sesi ekle
                print(f"\n[INFINITETALK] Orijinal ses ekleniyor...")
                add_audio_to_video(raw_video, audio_path, output_path)
                return str(output_path)

    print("[INFINITETALK] HATA: Video üretilemedi!")
    return None


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

def main():
    parser = argparse.ArgumentParser(description="Müzik Video Üretici - LTX Desktop API")
    parser.add_argument("audio", help="Giriş ses dosyası (mp3, wav, flac, vb.)")
    parser.add_argument("image", help="Giriş görseli (jpg, png)")
    parser.add_argument("--prompt", "-p", default="cinematic slow motion, beautiful scenery, atmospheric lighting, high quality 4k",
                        help="Video üretim prompt'u")
    parser.add_argument("--resolution", "-r", default=DEFAULT_RESOLUTION,
                        choices=["512p", "540p", "720p", "1080p"],
                        help="Video çözünürlüğü (540p=max 20s, 720p=max 10s, 1080p=max 5s)")
    parser.add_argument("--aspect", "-a", default=DEFAULT_ASPECT,
                        choices=["16:9", "9:16"],
                        help="En-boy oranı")
    parser.add_argument("--output", "-o", default=None,
                        help="Çıkış dosyası yolu")
    parser.add_argument("--token", "-t", default=None,
                        help="LTX Desktop auth token (otomatik bulunmazsa)")
    parser.add_argument("--segment-duration", "-s", type=int, default=SEGMENT_DURATION,
                        help="Segment süresi (saniye, varsayılan: 20)")
    parser.add_argument("--port", type=int, default=None,
                        help="LTX Desktop backend portu (otomatik bulunmazsa)")
    parser.add_argument("--guidance-scale", "-g", type=float, default=DEFAULT_GUIDANCE_SCALE,
                        help=f"Guidance scale / CFG (varsayılan: {DEFAULT_GUIDANCE_SCALE}). Düşük=serbest üretim, Yüksek=prompt'a bağlı")
    parser.add_argument("--steps", "-n", type=int, default=DEFAULT_STEPS,
                        help=f"Inference adım sayısı (varsayılan: {DEFAULT_STEPS}). Az=hızlı, Çok=kaliteli")
    parser.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.")
    parser.add_argument("--infinitetalk", action="store_true",
                        help="InfiniteTalk modu: Segmentlere bölmeden WanGP InfiniteTalk modeli ile "
                             "tek seferde uzun video üretir. Sliding window sayesinde geçiş sorunları olmaz. "
                             "WanGP kurulumu gerektirir. Bu mod LTX Desktop yerine WanGP API kullanır.")
    parser.add_argument("--face-swap", "-f", default=None,
                        help="Face swap için kaynak yüz resmi. Belirtilirse her segmentin son karesine face swap uygulanır ve sonraki segmente o yüzle devam edilir.")
    
    args = parser.parse_args()
    
    audio_path = Path(args.audio).resolve()
    image_path = Path(args.image).resolve()
    
    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 kaynak resmi bulunamadı: {face_swap_source}")
            sys.exit(1)
        if not FACEFUSION_VENV_PYTHON.exists():
            print(f"HATA: FaceFusion kurulu değil! Önce FaceFusion-Kur.bat çalıştırın.")
            sys.exit(1)

    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)
    
    # Çözünürlüğe göre max segment süresini belirle
    max_seg = get_max_segment_duration(args.resolution)
    segment_dur = min(args.segment_duration, max_seg)
    segment_dur = snap_to_allowed_duration(segment_dur, args.resolution)
    
    if args.segment_duration > max_seg:
        print(f"NOT: {args.resolution} çözünürlükte max segment süresi {max_seg}s, {segment_dur}s olarak ayarlandı.")
    
    # Çıkış klasörü
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    work_dir = Path(__file__).parent / "output" / f"muzik_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)
    
    # Çıkış dosyası
    if args.output:
        output_path = Path(args.output).resolve()
    else:
        stem = audio_path.stem
        output_path = work_dir / f"{stem}_muzik_video.mp4"
    
    print("=" * 60)
    print("  MÜZİK VİDEO ÜRETİCİ - LTX Desktop API")
    print("=" * 60)
    print(f"  Ses: {audio_path}")
    print(f"  Görsel: {image_path}")
    print(f"  Prompt: {args.prompt[:60]}...")
    print(f"  Çözünürlük: {args.resolution} | Oran: {args.aspect}")
    print(f"  Model: {VIDEO_MODEL}")
    print(f"  Guidance Scale: {args.guidance_scale} | Steps: {args.steps}")
    print(f"  Segment süresi: {segment_dur}s (max {max_seg}s @ {args.resolution})")
    print(f"  Auto-Prompt: {'AÇIK' if args.auto_prompt else 'Kapalı'}")
    print(f"  InfiniteTalk: {'AÇIK (WanGP sliding window)' if args.infinitetalk else 'Kapalı'}")
    print(f"  Face Swap: {'AÇIK - ' + str(face_swap_source) if face_swap_source else 'Kapalı'}")
    print(f"  Çıkış: {output_path}")
    print("=" * 60)

    # ── INFINITETALK MODU ──
    if args.infinitetalk:
        print("\n" + "=" * 60)
        print("  INFINITETALK MODU - Segmentsiz tek seferde uzun video")
        print("=" * 60)
        result = run_infinitetalk_pipeline(
            audio_path=audio_path,
            image_path=image_path,
            prompt=args.prompt,
            output_path=output_path,
            work_dir=work_dir,
            guidance_scale=args.guidance_scale,
            steps=args.steps,
        )
        if result:
            file_size = output_path.stat().st_size / (1024 * 1024)
            print(f"\n{'=' * 60}")
            print(f"  TAMAMLANDI! (InfiniteTalk)")
            print(f"{'=' * 60}")
            print(f"  Çıkış: {output_path}")
            print(f"  Boyut: {file_size:.1f} MB")
            print(f"{'=' * 60}")
            return str(output_path)
        else:
            print("\nHATA: InfiniteTalk ile video üretilemedi!")
            sys.exit(1)

    # ── 1. LTX Desktop Backend Portu ──
    if args.port:
        api_url = f"http://127.0.0.1:{args.port}"
        print(f"\n[*] Manuel port: {args.port}")
    else:
        api_url = find_ltx_backend_port()
    
    print(f"[*] LTX Backend: {api_url}")
    
    # ── 2. Auth Token ──
    if args.token:
        token = args.token
    else:
        token = discover_auth_token(api_url)
    
    if token:
        token_file = Path(__file__).parent / "ltx_token.txt"
        token_file.write_text(token)
    
    # ── 3. LTX Desktop Hazır mı? ──
    wait_for_idle(api_url, token)
    
    # ── 4. 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 ({segment_dur}s)")
    
    # ── 5. Görseli Çalışma Dizinine Kopyala (ve opsiyonel face swap) ──
    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
            print("  [FACE SWAP] İlk görsel face swap'landı!")
        else:
            print("  [FACE SWAP] İlk görselde face swap başarısız, orijinal kullanılacak.")

    # ── 6. Ses Enerjisi Analizi (opsiyonel) ──
    energy_levels = None
    if args.auto_prompt:
        print("\n[*] Ses enerjisi analiz ediliyor (auto-prompt)...")
        seg_boundaries = [(s, e) for (_, s, e, _) in audio_segments]
        energy_levels = analyze_audio_energy(audio_path, seg_boundaries)
        for i, (lvl, (_, s, e, _)) in enumerate(zip(energy_levels, audio_segments)):
            print(f"  Segment {i+1}: {s:.1f}s-{e:.1f}s → {lvl.upper()}")

    # ── 7. Video Segmentlerini Üret ──
    video_segments = []
    current_image = input_image

    start_time = time.time()
    
    for i, (seg_audio, seg_start, seg_end, seg_actual_dur) in enumerate(audio_segments):
        
        actual_dur = snap_to_allowed_duration(int(seg_actual_dur), args.resolution)
        actual_dur = max(5, actual_dur)
        
        print(f"\n{'─' * 55}")
        print(f"[SEGMENT {i+1}/{num_segments}] {seg_start:.1f}s - {seg_end:.1f}s (üretim: {actual_dur}s)")
        print(f"{'─' * 55}")
        
        wait_for_idle(api_url, token)

        # Prompt seçimi: auto-prompt açıksa enerji seviyesine göre
        segment_prompt = args.prompt
        if energy_levels and i < len(energy_levels):
            segment_prompt = build_dynamic_prompt(args.prompt, energy_levels[i])
            print(f"  [AUTO-PROMPT] Seviye: {energy_levels[i].upper()} → Prompt güncellenmiş")

        video_path = generate_video(
            api_url=api_url,
            prompt=segment_prompt,
            image_path=current_image,
            audio_path=seg_audio,
            token=token,
            resolution=args.resolution,
            aspect=args.aspect,
            duration=str(actual_dur),
            guidance_scale=args.guidance_scale,
            steps=args.steps
        )
        
        if video_path is None:
            print(f"  HATA: Segment {i+1} üretilemedi! Atlıyorum...")
            continue
        
        seg_video = segments_dir / f"segment_{i:03d}.mp4"
        shutil.copy2(video_path, seg_video)
        video_segments.append(seg_video)
        
        elapsed = time.time() - start_time
        avg_per_seg = elapsed / (i + 1)
        remaining = avg_per_seg * (num_segments - i - 1)
        print(f"  Süre: {elapsed:.0f}s | Ort: {avg_per_seg:.1f}s/seg | Kalan: ~{remaining:.0f}s")
        
        if i < num_segments - 1:
            next_image = frames_dir / f"frame_{i:03d}.jpg"
            try:
                # Son birkaç kareyi analiz edip en kaliteli olanı seç
                if not extract_best_frame(seg_video, next_image):
                    # Fallback zaten extract_best_frame içinde yapılıyor
                    pass

                # Face swap: son karedeki yüzü değiştir
                if face_swap_source:
                    swapped_image = frames_dir / f"frame_{i:03d}_swapped.jpg"
                    if face_swap_image(face_swap_source, next_image, swapped_image):
                        current_image = swapped_image
                        print(f"  [FACE SWAP] Sonraki segment face swap'lı kare ile devam edecek")
                    else:
                        print(f"  [FACE SWAP] Başarısız, orijinal son kare kullanılacak")
                        current_image = next_image
                else:
                    current_image = next_image
            except Exception as e:
                print(f"  UYARI: Son kare çıkarılamadı: {e}")
    
    if not video_segments:
        print("\nHATA: Hiç video segmenti üretilemedi!")
        sys.exit(1)
    
    # ── 7. Videoları Birleştir ──
    print(f"\n{'=' * 55}")
    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)
    
    # ── 8. Orijinal Tam Sesi Ekle ──
    print("[*] Orijinal şarkı ekleniyor...")
    add_audio_to_video(concat_video, audio_path, output_path)
    
    # ── 9. Tamamlandı ──
    total_time = time.time() - start_time
    file_size = output_path.stat().st_size / (1024 * 1024)
    
    print(f"\n{'=' * 60}")
    print(f"  TAMAMLANDI!")
    print(f"{'=' * 60}")
    print(f"  Çıkış: {output_path}")
    print(f"  Boyut: {file_size:.1f} MB")
    print(f"  Segment sayısı: {len(video_segments)}")
    print(f"  Toplam süre: {total_time:.0f} saniye ({total_time/60:.1f} dakika)")
    print(f"  Segment başına ortalama: {total_time/len(video_segments):.1f}s")
    print(f"{'=' * 60}")
    
    return str(output_path)


if __name__ == "__main__":
    main()
