"""FikoYT MSR Compose — gelişmiş çok-özne referans birleştirici (LiconMSR superset).

LTX 2.3 MSR için referans kareleri üretir. LiconMSR'den farkları:
- 10 özne slotu (4 değil) — pratikte sınırsız özne.
- Her özne girişi BATCH olabilir → tek karakterin çoklu açısı (önden/yandan/arkadan/
  yakın çekim) tek slota verilebilir. Karakter turnaround sayfası mantığı.
- frame_count serbest INT (sabit 17/25/33/41 değil); görsel sayısı fazlaysa otomatik
  büyür, her referans en az 1 kare alır.
- Arka plan + tüm özneler sabit uzunluktaki referans videoya dağıtılır → IC-LoRA
  arayüzü değişmez, MSR LoRA mekanizması bozulmaz (güvenli).
Çıkış (IMAGE) doğrudan LTXAddVideoICLoRAGuide.image girişine bağlanır (LiconMSR drop-in).
"""

import torch
import torch.nn.functional as F

_MAX = 10


class FikoYTMSRCompose:
    @classmethod
    def INPUT_TYPES(cls):
        opt = {
            f"ozne_{i}": ("IMAGE", {
                "tooltip": f"Özne {i} — tek görsel veya batch (çoklu açı). Boşsa atlanır."
            })
            for i in range(1, _MAX + 1)
        }
        return {
            "required": {
                "width": ("INT", {"default": 1280, "min": 32, "max": 8192, "step": 32}),
                "height": ("INT", {"default": 720, "min": 32, "max": 8192, "step": 32}),
                "frame_count": ("INT", {
                    "default": 41, "min": 5, "max": 513, "step": 1,
                    "tooltip": "Referans video uzunluğu. Tüm görseller buraya dağıtılır; "
                               "görsel çoksa otomatik büyür."
                }),
                "background": ("IMAGE", {"tooltip": "Sahne/ortam görseli — zorunlu, sona yerleşir."}),
            },
            "optional": {
                **opt,
                "cerceve": (["oto", "kirp", "sigdir", "doldur"], {
                    "default": "oto",
                    "tooltip": "oto=akilli distorsiyonsuz (oran yakin→kirp, farkli→sigdir), "
                               "sigdir=orani koru + kenar doldur, doldur=video oranina ger, kirp=doldur+kirp",
                }),
            },
        }

    RETURN_TYPES = ("IMAGE", "INT")
    RETURN_NAMES = ("reference_frames", "frame_count")
    FUNCTION = "compose"
    CATEGORY = "FikoYT/video"

    @staticmethod
    def _fit(img_hwc, W, H, mode="oto"):
        ih, iw = int(img_hwc.shape[0]), int(img_hwc.shape[1])
        t = img_hwc.permute(2, 0, 1).unsqueeze(0).float()      # [1,C,ih,iw]
        if mode == "oto":
            # DISTORSIYON YOK: oran hedefe yakinsa kirp (az kayip), cok farkliysa sigdir (hepsi durur)
            r = (iw / ih) / (W / H)
            mode = "kirp" if 0.8 <= r <= 1.25 else "sigdir"
        if mode == "doldur":                                    # video oranina GER (eski)
            t = F.interpolate(t, size=(H, W), mode="bilinear", align_corners=False)
        elif mode == "kirp":                                    # doldur + ortadan kirp
            s = max(W / iw, H / ih)
            nw, nh = max(1, round(iw * s)), max(1, round(ih * s))
            t = F.interpolate(t, size=(nh, nw), mode="bilinear", align_corners=False)
            x0, y0 = (nw - W) // 2, (nh - H) // 2
            t = t[:, :, y0:y0 + H, x0:x0 + W]
        else:                                                   # sigdir: orani koru + kenar doldur
            s = min(W / iw, H / ih)
            nw, nh = max(1, round(iw * s)), max(1, round(ih * s))
            t = F.interpolate(t, size=(nh, nw), mode="bilinear", align_corners=False)
            pl, pt = (W - nw) // 2, (H - nh) // 2
            pr, pb = W - nw - pl, H - nh - pt
            t = F.pad(t, (pl, pr, pt, pb), mode="replicate")    # kenar pikselini uzat (siyah bar yok)
        return t.squeeze(0).permute(1, 2, 0).clamp(0, 1)        # [H,W,C]

    def compose(self, width, height, frame_count, background, **kwargs):
        mode = kwargs.get("cerceve", "kirp")
        refs = []
        for i in range(1, _MAX + 1):
            img = kwargs.get(f"ozne_{i}")
            if img is None:
                continue
            for b in range(img.shape[0]):                       # batch = çoklu açı
                refs.append(self._fit(img[b], width, height, mode))
        for b in range(background.shape[0]):                    # arka plan (batch destekli), sona
            refs.append(self._fit(background[b], width, height, mode))
        if not refs:
            raise ValueError("FikoYTMSRCompose: en az arka plan görseli gerekli")

        n = len(refs)
        fc = max(int(frame_count), n)                           # her referans en az 1 kare
        base, rem = fc // n, fc % n
        frames = []
        for idx, im in enumerate(refs):
            frames.extend([im] * (base + (1 if idx < rem else 0)))
        out = torch.stack(frames, dim=0)                        # [F,H,W,C]
        return (out, out.shape[0])


import base64
import io
import json
import re
import urllib.request


def _img_to_b64(img_bhwc):
    """ComfyUI IMAGE tensörünün ilk karesini PNG base64'e çevirir."""
    import numpy as np
    from PIL import Image
    arr = (img_bhwc[0].detach().cpu().numpy() * 255.0).clip(0, 255).astype("uint8")  # [H,W,C]
    im = Image.fromarray(arr)
    buf = io.BytesIO()
    im.save(buf, format="PNG")
    return base64.b64encode(buf.getvalue()).decode("ascii")


def _ollama_caption(url, model, instruction, b64, timeout=180):
    body = json.dumps({
        "model": model, "prompt": instruction, "images": [b64],
        "stream": False, "options": {"temperature": 0.0},
    }).encode("utf-8")
    req = urllib.request.Request(
        url.rstrip("/") + "/api/generate", data=body,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=timeout) as r:
        data = json.loads(r.read().decode("utf-8"))
    return (data.get("response") or "").strip().replace("\n", " ")


def _ollama_up(url):
    try:
        urllib.request.urlopen(url.rstrip("/") + "/api/tags", timeout=4).read()
        return True
    except Exception:
        return False


def _ollama_text(url, model, system, user, examples=None, temperature=0.0, timeout=120):
    """Görselsiz metin sohbeti (çeviri) — /api/chat. examples: (user, assistant) tek-örnek listesi."""
    msgs = [{"role": "system", "content": system}]
    for eu, ea in (examples or []):
        msgs.append({"role": "user", "content": eu})
        msgs.append({"role": "assistant", "content": ea})
    msgs.append({"role": "user", "content": user})
    body = json.dumps({
        "model": model, "stream": False, "options": {"temperature": temperature},
        "messages": msgs,
    }).encode("utf-8")
    req = urllib.request.Request(url.rstrip("/") + "/api/chat", data=body,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        data = json.loads(r.read().decode("utf-8"))
    return (data.get("message", {}).get("content") or "").strip()


_TR_CHARS = set("ğşıİĞŞ")   # Türkçe'ye özgü güçlü sinyal (ç/ö/ü zayıf, kullanılmaz)


def _force_en(url, model, text):
    """Caption Türkçe çıktıysa İngilizce'ye çevir (llava bazen Türkçe cevaplıyor)."""
    if not text or not any(c in _TR_CHARS for c in text):
        return text
    try:
        en = _ollama_text(url, model,
                          "Translate to natural English. Output ONLY the English translation, "
                          "nothing else, no quotes, no preamble.", text, temperature=0.0)
        return en or text
    except Exception:
        return text


_CLEAN_LEAD = re.compile(
    r'^\s*(the\s+)?(image|picture|photo|scene)\s+'
    r'(shows?|features?|depicts?|displays?|presents?|showcases?|contains?|is|portrays?)\s+'
    r'(of\s+)?(a\s+|an\s+)?(turnaround\s+|reference\s+|character\s+)*'
    r'(sheet\s+(of|featuring|showing|showcasing|with)\s+)?(a\s+|an\s+)?', re.I)
_CLEAN_ANGLES = re.compile(
    r'[\s,]*(from|in|shown\s+in|seen\s+from|depicted\s+from|viewed\s+from)\s+'
    r'(various|multiple|several|different|all|many|numerous)\s+'
    r'(angles?|views?|perspectives?|sides?)(\s+and\s+(perspectives?|views?|angles?|sides?))*', re.I)


def _clean_caption(s):
    """llava'nın 'The image shows... from various angles' kalıbını temizler (turnaround→çok kişi sanma)."""
    if not s:
        return s
    s = _CLEAN_LEAD.sub('', s.strip())
    s = _CLEAN_ANGLES.sub('', s)
    s = re.sub(r'\b(turnaround|reference sheet)\b', '', s, flags=re.I)
    s = re.sub(r'\bone single subject\b', 'a subject', s, flags=re.I)
    s = re.sub(r'\s{2,}', ' ', s).strip(' ,.')
    if s:
        s = s[0].upper() + s[1:]
        if not s.endswith('.'):
            s += '.'
    return s


def _ollama_models(url="http://localhost:11434"):
    """Kurulu Ollama modellerini listele (combo/dropdown için). Ollama yoksa fallback."""
    try:
        data = json.load(urllib.request.urlopen(url.rstrip("/") + "/api/tags", timeout=4))
        names = [m.get("name") for m in data.get("models", []) if m.get("name")]
        return names or ["llava:13b"]
    except Exception:
        return ["llava:13b"]


class FikoYTMSRStudio:
    """TEK NODE: referans kareleri (compose) + VLM destekli prompt + opsiyonel isim.

    Her özneyi BİR kez tanıtırsın (görseli `ozne_i`'ye bağla) — VLM (Ollama/llava)
    otomatik tarif eder, isim opsiyonel. Tariflenmiş özneler + arka plan + senin
    yazdığın `aksiyon` tek prompt'a dizilip CLIP ile encode edilir.

    HEPSİ OPSİYONEL: özne/arka plan vermezsen `reference_frames` boş döner ve sadece
    `aksiyon` prompt'u kalır → saf TEXT-TO-VIDEO. (Bu durumda ICLoRA guide grubunu
    bypass et.)

    Çıkışlar:
      reference_frames → LTXAddVideoICLoRAGuide.image
      conditioning     → LTXVConditioning.positive (CLIPTextEncode yerine)
      frame_count      → bilgi
      prompt           → birleşen metin (Show Text ile kontrol)
    """

    _OZNE_INS = ("This image is a TURNAROUND reference sheet showing ONE SINGLE subject from several angles "
                 "(front, side, back, close-up) — it is NOT multiple people or objects. "
                 "Describe ONLY the single subject itself, as if writing a character/object description for a "
                 "video generation prompt. Do NOT mention the image, the sheet, the turnaround, the angles, the "
                 "views, or that there are multiple of anything — write in the SINGULAR ('a man...', 'a red car...'). "
                 "Give rich, specific detail: exact clothing with colors and material, hair, facial features, and "
                 "ANY mask, headpiece, helmet, costume or distinctive accessory and markings — never omit an unusual "
                 "head or face covering. For an object, cover its type, color, shape and notable parts. "
                 "Reply in ENGLISH ONLY, 2 concise sentences. No preamble.")
    _BG_INS = ("Describe this location/scene for a video prompt: setting, key elements, time of day, lighting "
               "and mood. Reply in ENGLISH ONLY, ONE or TWO sentences, concise. No preamble, only the description.")

    @classmethod
    def INPUT_TYPES(cls):
        ozne = {
            f"ozne_{i}": ("FIKOYT_OZNE", {"tooltip": f"Özne/nesne {i} — 'FikoYT Özne' node'undan (resim+ad+açıklama tek pakette). Boşsa atlanır."})
            for i in range(1, _MAX + 1)
        }
        return {
            "required": {
                "clip": ("CLIP",),
                "aksiyon": ("STRING", {
                    "default": "The subject acts naturally with gentle motion; the camera slowly moves closer; cinematic realistic lighting.",
                    "multiline": True,
                    "tooltip": "Sahne / aksiyon / kamera — asıl yönetmen burası. İsim verdiysen burada kullan: 'Zekiye arabaya yaslanıyor'.",
                }),
                "width": ("INT", {"default": 1280, "min": 32, "max": 8192, "step": 32}),
                "height": ("INT", {"default": 720, "min": 32, "max": 8192, "step": 32}),
                "frame_count": ("INT", {"default": 41, "min": 5, "max": 513, "step": 1,
                                        "tooltip": "Referans şerit uzunluğu (video süresi DEĞİL). auto_frame kapalıyken kullanılır."}),
            },
            "optional": {
                "background": ("FIKOYT_OZNE", {"tooltip": "Mekan — 'FikoYT Özne' node'undan (resim+ad+açıklama). Opsiyonel, referans sonuna yazılır."}),
                **ozne,
                "otomatik_tarif": ("BOOLEAN", {"default": True,
                                               "tooltip": "Açık: VLM görselleri tarif eder. Kapalı/Ollama yoksa: sadece isim+açıklama + aksiyon."}),
                "auto_frame": ("BOOLEAN", {"default": True,
                                           "tooltip": "Açık: referans kare sayısını özne sayısından akıllıca hesaplar. Kapalı: frame_count kullanılır."}),
                "cerceve": (["oto", "kirp", "sigdir", "doldur"], {"default": "oto",
                            "tooltip": "oto=akıllı distorsiyonsuz (önerilen), kirp=doldur+kırp, sigdir=oranı koru+kenar doldur, doldur=ger."}),
                "ollama_model": (_ollama_models(), {"tooltip": "Kurulu Ollama vision modeli (otomatik listelenir)."}),
                "ollama_url": ("STRING", {"default": "http://localhost:11434"}),
                "prompt_birlestir": ("BOOLEAN", {"default": True,
                    "tooltip": "Açık: aksiyon + VLM özne tarifleri + mekan birleştirilir. "
                               "KAPALI = DİREKT MOD: girdiğin prompt aynen LTX'e gider — Ollama'ya HİÇ yollamaz, "
                               "çeviri/tarif yok (referans görseller yine çalışır)."}),
            },
        }

    RETURN_TYPES = ("IMAGE", "CONDITIONING", "INT", "STRING")
    RETURN_NAMES = ("reference_frames", "conditioning", "frame_count", "prompt")
    FUNCTION = "run"
    CATEGORY = "FikoYT/video"

    def run(self, clip, aksiyon, width, height, frame_count, background=None,
            otomatik_tarif=True, auto_frame=True, cerceve="kirp",
            ollama_model="llava:13b", ollama_url="http://localhost:11434",
            prompt_birlestir=True, **kwargs):
        fit = FikoYTMSRCompose._fit
        birlestir = bool(prompt_birlestir)
        vlm = birlestir and bool(otomatik_tarif) and _ollama_up(ollama_url)   # direkt modda Ollama'ya hiç gitme

        def _coz(o):
            # FIKOYT_OZNE paketi {image, ad, aciklama} VEYA düz IMAGE tensörü (geriye dönük)
            if isinstance(o, dict):
                return o.get("image"), (o.get("ad") or "").strip(), (o.get("aciklama") or "").strip()
            return o, "", ""

        # --- referans kareleri + özne tarifleri (tek geçiş) ---
        refs, ozneler = [], []
        for i in range(1, _MAX + 1):
            img, ad, ipucu = _coz(kwargs.get(f"ozne_{i}"))
            if img is None:
                continue
            for b in range(img.shape[0]):                       # batch = çoklu açı
                refs.append(fit(img[b], width, height, cerceve))
            if birlestir:                                       # direkt modda tarif/birleştirme yok
                cap = ""
                if vlm:
                    ins = self._OZNE_INS + (f" Focus on this hint: {ipucu}." if ipucu else "")
                    cap = _ollama_caption(ollama_url, ollama_model, ins, _img_to_b64(img))
                    cap = _clean_caption(_force_en(ollama_url, ollama_model, cap))  # EN'e zorla + 'image/açı' temizle
                tarif = cap or ipucu or ad                      # VLM yoksa ipucu/isme düş
                if ad and tarif and tarif != ad:
                    ozneler.append(f"{ad}: {tarif}")
                elif tarif:
                    ozneler.append(f"{len(ozneler) + 1}) {tarif}")

        mekan = ""
        bg_img, bg_ad, bg_ipucu = _coz(background)
        if bg_img is not None:
            for b in range(bg_img.shape[0]):
                refs.append(fit(bg_img[b], width, height, cerceve))
            if birlestir:
                if vlm:
                    ins = self._BG_INS + (f" Focus on this hint: {bg_ipucu}." if bg_ipucu else "")
                    mekan = _ollama_caption(ollama_url, ollama_model, ins, _img_to_b64(bg_img))
                    mekan = _clean_caption(_force_en(ollama_url, ollama_model, mekan))
                mekan = mekan or bg_ipucu or bg_ad

        # --- prompt (birleştir VEYA direkt aksiyon) + encode ---
        if birlestir:
            parcalar = []
            if aksiyon.strip():
                parcalar.append(aksiyon.strip())
            if ozneler:
                parcalar.append("Subjects in the scene:\n" + "\n".join(ozneler))
            if mekan:
                parcalar.append(f"Location: {mekan}")
            text = "\n\n".join(parcalar)
        else:
            text = aksiyon.strip()                              # DİREKT: girdiğin prompt aynen LTX'e
        tokens = clip.tokenize(text)
        if hasattr(clip, "encode_from_tokens_scheduled"):
            cond = clip.encode_from_tokens_scheduled(tokens)
        else:
            out = clip.encode_from_tokens(tokens, return_pooled=True)
            cond = [[out[0], {"pooled_output": out[1]}]]

        # --- referans şeridi (yoksa T2V: 1 kare placeholder, guide bypass'lı kalır) ---
        if not refs:
            return (torch.zeros(1, 64, 64, 3), cond, 0, text)
        n = len(refs)
        # auto_frame: her referansa ~8 kare, 17..121 arası; kapalı: kullanıcı frame_count'u
        fc = min(121, max(17, n * 8)) if bool(auto_frame) else max(int(frame_count), n)
        base, rem = fc // n, fc % n
        frames = []
        for idx, im in enumerate(refs):
            frames.extend([im] * (base + (1 if idx < rem else 0)))
        out = torch.stack(frames, dim=0)
        return (out, cond, out.shape[0], text)


class FikoYTOzne:
    """Tek node'da: resim seç (dialog/upload) + ad + açıklama → tek 'özne' paketi.

    Resimi ayrı LoadImage'ta seçip ismi başka yere yazma derdi biter. Çıkış (FIKOYT_OZNE)
    doğrudan 'FikoYT MSR Studio'nun ozne_/background girişine bağlanır. Açıklama yazarsan
    VLM o ipucuna odaklanır; boşsa serbest tarif eder. Ad'ı aksiyon metninde kullanırsın.
    Görsel yüklemesi ComfyUI LoadImage ile birebir aynıdır (upload + galeri).
    """

    @classmethod
    def INPUT_TYPES(cls):
        import os
        import folder_paths
        idir = folder_paths.get_input_directory()
        files = [f for f in os.listdir(idir) if os.path.isfile(os.path.join(idir, f))]
        try:
            files = folder_paths.filter_files_content_types(files, ["image"])
        except Exception:
            files = [f for f in files if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"))]
        return {
            "required": {
                "image": (sorted(files), {"image_upload": True, "tooltip": "Resmi yükle veya galeriden seç (dialog)."}),
            },
            "optional": {
                "ad": ("STRING", {"default": "", "tooltip": "Özne adı (Zekiye / kırmızı araba). Aksiyon metninde bu adı kullan."}),
                "aciklama": ("STRING", {"default": "", "multiline": True,
                                        "tooltip": "Açıklama ipucu — VLM buna odaklanarak tarif eder. Boşsa serbest tarif."}),
            },
        }

    RETURN_TYPES = ("FIKOYT_OZNE",)
    RETURN_NAMES = ("ozne",)
    FUNCTION = "yukle"
    CATEGORY = "FikoYT/video"

    def yukle(self, image, ad="", aciklama=""):
        from nodes import LoadImage
        img, _mask = LoadImage().load_image(image)
        return ({"image": img, "ad": (ad or "").strip(), "aciklama": (aciklama or "").strip()},)

    @classmethod
    def IS_CHANGED(cls, image, ad="", aciklama=""):
        from nodes import LoadImage
        try:
            return str(LoadImage.IS_CHANGED(image)) + "|" + (ad or "") + "|" + (aciklama or "")
        except Exception:
            return (image or "") + "|" + (ad or "") + "|" + (aciklama or "")

    @classmethod
    def VALIDATE_INPUTS(cls, image, ad="", aciklama=""):
        from nodes import LoadImage
        try:
            return LoadImage.VALIDATE_INPUTS(image)
        except Exception:
            return True


class FikoYTSahnePrompt:
    """Ayrı sahne/aksiyon prompt node'u + Türkçe→İngilizce (LTX'e uygun) Ollama çevirisi.

    Sahneyi Türkçe (ya da herhangi bir dilde) anlatırsın; Ollama metin modeli onu
    LTX text-to-video için tek, sinematik, kusursuz İngilizce prompt'a çevirir+iyileştirir
    (anlamı bozmadan, yeni özne uydurmadan). Çıkış (STRING) → MSR Studio 'aksiyon' girişine.
    Çeviri kapalıysa veya Ollama yoksa metni AYNEN geçirir.
    """

    _SYS = ("You convert a short video scene description into ONE cinematic English prompt for the LTX "
            "text-to-video model. RULES: "
            "1) Translate the action, camera, setting and mood into natural English. "
            "2) Tokens like <<KEEP0>> are PROTECTED dialogue: output each token exactly once, unchanged, with NO "
            "quotation marks around it, in the place where the character speaks. "
            "3) NEVER invent or write any dialogue yourself, NEVER put words inside quotation marks, NEVER add "
            "parenthetical translations or the phrase 'in Turkish'. "
            "4) Output ONLY the final prompt text — no preamble, no explanation.")
    # tek-örnek: doğru davranışı gösterir (uydurmayı/çift tırnağı engeller)
    _EX = [("Bir adam kapıyı açıp <<KEEP0>> diye bağırıyor, gece, sinematik.",
            "A man flings the door open and shouts <<KEEP0>>, at night with cinematic lighting as the camera holds on his face.")]

    # konuşma/şarkı sözü: tırnak İÇİ saklanır (çeviriye gitmez), geri koyarken tek tırnak eklenir
    _QUOTE = re.compile(r'"([^"]*)"|“([^”]*)”|‘([^’]*)’|«([^»]*)»')

    @classmethod
    def _protect(cls, text):
        keep = []

        def repl(m):
            ic = next((g for g in m.groups() if g is not None), "")
            keep.append(ic)
            return f"<<KEEP{len(keep) - 1}>>"
        return cls._QUOTE.sub(repl, text), keep

    @staticmethod
    def _restore(text, keep):
        eksik = []
        for i, k in enumerate(keep):
            tok = f"<<KEEP{i}>>"
            if tok in text:
                text = text.replace(tok, '"' + k + '"')
            else:
                eksik.append(k)               # model token'ı düşürdü
        if eksik:
            # GARANTİ: düşen sözü sona ekle (kelimeler asla kaybolmaz/çevrilmez)
            text = text.rstrip(" .") + ". She says: " + " ".join('"' + k + '"' for k in eksik) + "."
        # kazara çift tırnak temizliği
        for a, b in (('""', '"'), ("“”", '"')):
            text = text.replace(a, b)
        return text

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "metin": ("STRING", {"default": "", "multiline": True,
                                     "tooltip": "Sahneyi anlat (Türkçe olabilir). Ollama LTX'e uygun İngilizce'ye çevirir."}),
            },
            "optional": {
                "ceviri": ("BOOLEAN", {"default": True,
                                       "tooltip": "Açık: Ollama İngilizce'ye çevirir+iyileştirir. Kapalı/Ollama yoksa: aynen geçir."}),
                "ollama_model": (_ollama_models(), {"tooltip": "Çeviri için metin modeli (llama3.1 / gemma önerilir)."}),
                "ollama_url": ("STRING", {"default": "http://localhost:11434"}),
            },
        }

    RETURN_TYPES = ("STRING",)
    RETURN_NAMES = ("prompt",)
    FUNCTION = "build"
    CATEGORY = "FikoYT/video"

    def build(self, metin, ceviri=True, ollama_model="llama3.1:8b", ollama_url="http://localhost:11434"):
        t = (metin or "").strip()
        if not t:
            return ("",)
        if bool(ceviri) and _ollama_up(ollama_url):
            try:
                korumali, keep = self._protect(t)       # tırnak içi söz/şarkı çeviriye gitmez
                out = _ollama_text(ollama_url, ollama_model, self._SYS, korumali,
                                   examples=self._EX, temperature=0.0)
                out = self._restore(out, keep)           # orijinal sözleri aynen geri koy
                if out.strip():
                    return (out.strip(),)
            except Exception:
                pass
        return (t,)


class FikoYTPromptEncode:
    """Düzenlenmiş/komple metin + CLIP → CONDITIONING. (Düzenle node'undan sonra encode.)"""

    @classmethod
    def INPUT_TYPES(cls):
        return {"required": {
            "clip": ("CLIP",),
            "prompt": ("STRING", {"forceInput": True, "tooltip": "Encode edilecek (düzenlenmiş) prompt."}),
        }}

    RETURN_TYPES = ("CONDITIONING",)
    RETURN_NAMES = ("conditioning",)
    FUNCTION = "encode"
    CATEGORY = "FikoYT/video"

    def encode(self, clip, prompt):
        tokens = clip.tokenize(prompt or "")
        if hasattr(clip, "encode_from_tokens_scheduled"):
            cond = clip.encode_from_tokens_scheduled(tokens)
        else:
            out = clip.encode_from_tokens(tokens, return_pooled=True)
            cond = [[out[0], {"pooled_output": out[1]}]]
        return (cond,)


NODE_CLASS_MAPPINGS = {
    "FikoYTMSRCompose": FikoYTMSRCompose,
    "FikoYTMSRStudio": FikoYTMSRStudio,
    "FikoYTOzne": FikoYTOzne,
    "FikoYTSahnePrompt": FikoYTSahnePrompt,
    "FikoYTPromptEncode": FikoYTPromptEncode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
    "FikoYTMSRCompose": "FikoYT 🎭 MSR Compose (sınırsız özne)",
    "FikoYTMSRStudio": "FikoYT 🎬 MSR Studio (referans + VLM prompt, tek node)",
    "FikoYTOzne": "FikoYT 🧍 Özne (resim + ad + açıklama)",
    "FikoYTSahnePrompt": "FikoYT 📝 Sahne Prompt (Türkçe→İngilizce Ollama)",
    "FikoYTPromptEncode": "FikoYT 🔤 Prompt Encode (metin → conditioning)",
}
