# FikoYT Nodes Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build `ComfyUI-FikoYT-Nodes` — a standalone ComfyUI custom-node package that mirrors every MVD render workflow (image / video / audio / character) with full parameter exposure, auto-download, and standard-type composability.

**Architecture:** Single package under `ComfyUI/custom_nodes/ComfyUI-FikoYT-Nodes/`. Six categories (image, video, audio, character, util, web). Core shared layer (`core/`) provides `hf_download`, `get_models_dir`, custom types (`FIKOYT_CHARACTER`, `FIKOYT_LORA_STACK`), and curated registry. Each node = its own file. Single `__init__.py` aggregates `NODE_CLASS_MAPPINGS`. Code is **rewritten from MVD references** (not copied) — sadeleştirme + parametre genişletme.

**Tech Stack:** Python 3.11+ (ComfyUI embedded), PyTorch, diffusers, transformers, huggingface_hub, librosa/pydub, ffmpeg-python. Uses already-installed MVD dependencies (insightface, onnxruntime, einops, smthemex SongGen node, ComfyUI-GGUF).

**Constraint (CLAUDE.md):** Sade ilerle. Minimum dosya/satır. Dead code yok. Tek dosya → tek sorumluluk.

**Notes for executor:**
- Repo **git değil**. Commit step'leri opsiyonel; bunun yerine her dosyayı yazdıktan sonra `python -c "import ast; ast.parse(open('<file>', encoding='utf-8').read())"` ile syntax check yap.
- Test stratejisi: pytest **gerekmiyor**. Her node için ComfyUI restart sonrası menüde görünme + 1 canlı render smoke testi yeterli.
- MVD referans path'leri: `D:/Comfy/App/ComfyUI_windows_portable/ComfyUI/custom_nodes/ComfyUI-MusicVideoDirector/`.
- Hedef path: `D:/Comfy/App/ComfyUI_windows_portable/ComfyUI/custom_nodes/ComfyUI-FikoYT-Nodes/`.

---

## Phase F1 — Paket İskeleti (SIRALI, EN ÖNCE)

Bu sprint tek başına çalışan minimum paketi yaratır. F2-F5 paralel sprint'lerinin tümü F1 tamamlandıktan sonra başlar.

### Task F1.1: pyproject.toml + requirements

**Files:**
- Create: `ComfyUI-FikoYT-Nodes/pyproject.toml`
- Create: `ComfyUI-FikoYT-Nodes/requirements.txt`
- Create: `ComfyUI-FikoYT-Nodes/README.md`

- [ ] **Step 1: pyproject.toml yaz**

```toml
[project]
name = "comfyui-fikoyt-nodes"
version = "0.1.0"
description = "FikoYT custom nodes for ComfyUI — image / video / audio / character — full parameter exposure"
authors = [{name = "FikoYT", email = "fikri007@gmail.com"}]
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.11"

[project.urls]
Repository = "https://github.com/fikoyt/ComfyUI-FikoYT-Nodes"

[tool.comfy]
PublisherId = "fikoyt"
DisplayName = "FikoYT Nodes"
Icon = ""
```

- [ ] **Step 2: requirements.txt yaz (minimum, MVD bağımlılıklarına güven)**

```
huggingface_hub>=0.24
pydub>=0.25
ffmpeg-python>=0.2
```

- [ ] **Step 3: README.md yaz**

İçerik: paket amacı, kategori listesi, kurulum (`git clone` veya ComfyUI Manager), 3 örnek workflow (F6'da JSON eklenir).

- [ ] **Step 4: Syntax check**

```bash
python -c "import tomllib; tomllib.load(open('ComfyUI-FikoYT-Nodes/pyproject.toml','rb'))"
```
Expected: hata yok.

---

### Task F1.2: core/ katmanı (paylaşılan yardımcılar)

**Files:**
- Create: `ComfyUI-FikoYT-Nodes/core/__init__.py`
- Create: `ComfyUI-FikoYT-Nodes/core/auto_download.py`
- Create: `ComfyUI-FikoYT-Nodes/core/types.py`
- Create: `ComfyUI-FikoYT-Nodes/core/registry.py`
- Create: `ComfyUI-FikoYT-Nodes/core/helpers.py`
- Create: `ComfyUI-FikoYT-Nodes/core/registry_data/loras.json`
- Create: `ComfyUI-FikoYT-Nodes/core/registry_data/models.json`

- [ ] **Step 1: `core/auto_download.py`**

MVD referans: `ComfyUI-MusicVideoDirector/nodes/common.py:32-65` (`hf_download`) + `_legacy.py` PuLID download blokları.

```python
"""HF auto-download utilities: resume + SHA check + progress callback."""
import os
import hashlib
from typing import Optional, Callable

try:
    from huggingface_hub import hf_hub_download
    _HAS_HF = True
except ImportError:
    _HAS_HF = False
    import urllib.request


def get_models_dir(subdir: Optional[str] = None) -> str:
    try:
        import folder_paths
        base = folder_paths.models_dir
    except Exception:
        here = os.path.dirname(os.path.abspath(__file__))
        base = os.path.abspath(os.path.join(here, "..", "..", "..", "..", "models"))
    if subdir:
        base = os.path.join(base, subdir)
    os.makedirs(base, exist_ok=True)
    return base


def _sha256(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def ensure_model(
    repo_id: str,
    filename: str,
    subdir: str,
    sha256: Optional[str] = None,
    revision: str = "main",
    progress_cb: Optional[Callable[[int, int], None]] = None,
) -> str:
    """Download model if missing. Return absolute path."""
    dest_dir = get_models_dir(subdir)
    dest_path = os.path.join(dest_dir, os.path.basename(filename))

    if os.path.isfile(dest_path) and os.path.getsize(dest_path) > 1024:
        if sha256 and _sha256(dest_path) != sha256:
            os.remove(dest_path)
        else:
            return dest_path

    if _HAS_HF:
        local = hf_hub_download(
            repo_id=repo_id, filename=filename, revision=revision,
            local_dir=dest_dir, local_dir_use_symlinks=False, resume_download=True,
        )
        return local

    url = f"https://huggingface.co/{repo_id}/resolve/{revision}/{filename}"
    urllib.request.urlretrieve(url, dest_path)
    return dest_path
```

- [ ] **Step 2: `core/types.py`**

```python
"""Custom types for FikoYT nodes — opsiyonel, standart tiplerle bypass edilebilir."""

FIKOYT_CHARACTER = "FIKOYT_CHARACTER"
FIKOYT_LORA_STACK = "FIKOYT_LORA_STACK"


def make_character(
    name: str,
    face_emb_path: str = "",
    lora_paths: list = None,
    ref_image_path: str = "",
    base_prompt: str = "",
    base_neg: str = "",
) -> dict:
    return {
        "name": name,
        "face_emb_path": face_emb_path,
        "lora_paths": lora_paths or [],
        "ref_image_path": ref_image_path,
        "base_prompt": base_prompt,
        "base_neg": base_neg,
    }


def make_lora_stack(items: list = None) -> list:
    """items: list of {name, strength_model, strength_clip}."""
    return list(items or [])
```

- [ ] **Step 3: `core/helpers.py`**

```python
"""Tensor / aspect / seed helpers."""
import random


ASPECT_PRESETS = {
    "1:1 Square (1024)":   (1024, 1024),
    "16:9 Landscape (HD)": (1280, 720),
    "9:16 Portrait (HD)":  (720, 1280),
    "4:5 Portrait":        (1024, 1280),
    "3:2 Photo":           (1216, 832),
    "21:9 Cinema":         (1536, 640),
}


def resolve_aspect(preset: str, default=(1024, 1024)):
    return ASPECT_PRESETS.get(preset, default)


def normalize_seed(seed: int) -> int:
    if seed < 0:
        return random.randint(0, 2**32 - 1)
    return int(seed) % (2**32)
```

- [ ] **Step 4: `core/registry.py`**

```python
"""Curated model/LoRA registry — JSON tabanlı."""
import json
import os

_HERE = os.path.dirname(os.path.abspath(__file__))


def _load(name: str) -> dict:
    path = os.path.join(_HERE, "registry_data", f"{name}.json")
    if not os.path.isfile(path):
        return {}
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def list_loras(category: str = None) -> list:
    data = _load("loras")
    items = data.get("items", [])
    if category:
        items = [x for x in items if x.get("category") == category]
    return items


def list_models(category: str = None) -> list:
    data = _load("models")
    items = data.get("items", [])
    if category:
        items = [x for x in items if x.get("category") == category]
    return items


def get_lora_names(category: str = None) -> list:
    return [x["name"] for x in list_loras(category)]
```

- [ ] **Step 5: `core/registry_data/loras.json` ve `models.json`**

MVD'deki curated LoRA listesinden taşı. **Referans dosyalar:**
- `ComfyUI-MusicVideoDirector/nodes/lora_registry_endpoints.py` — `CURATED_LORAS` sözlüğü
- `ComfyUI-MusicVideoDirector/nodes/models_registry_endpoints.py` — `MODELS_REGISTRY` sözlüğü

Format:
```json
{
  "items": [
    {"name": "Flux Realism v2", "category": "flux", "repo": "user/repo", "filename": "flux_realism.safetensors", "subdir": "loras", "default_strength": 0.85}
  ]
}
```

İlk sürüm için **en az 10 entry** (her kategoriden — flux, music, ltx, qwen, wan). Eksiksiz registry F6'da tamamlanır.

- [ ] **Step 6: `core/__init__.py`**

```python
from .auto_download import ensure_model, get_models_dir
from .types import FIKOYT_CHARACTER, FIKOYT_LORA_STACK, make_character, make_lora_stack
from .helpers import ASPECT_PRESETS, resolve_aspect, normalize_seed
from .registry import list_loras, list_models, get_lora_names

__all__ = [
    "ensure_model", "get_models_dir",
    "FIKOYT_CHARACTER", "FIKOYT_LORA_STACK", "make_character", "make_lora_stack",
    "ASPECT_PRESETS", "resolve_aspect", "normalize_seed",
    "list_loras", "list_models", "get_lora_names",
]
```

- [ ] **Step 7: Syntax check her dosya için**

```bash
for f in ComfyUI-FikoYT-Nodes/core/*.py; do python -c "import ast; ast.parse(open('$f',encoding='utf-8').read())"; done
```
Expected: hata yok.

---

### Task F1.3: İlk node — Flux Loader + Sampler (smoke test)

**Files:**
- Create: `ComfyUI-FikoYT-Nodes/image/__init__.py`
- Create: `ComfyUI-FikoYT-Nodes/image/flux_loader.py`
- Create: `ComfyUI-FikoYT-Nodes/image/flux_sampler.py`

- [ ] **Step 1: `image/flux_loader.py`**

MVD referans: `nodes/visual.py` Flux loader bloğu.

```python
"""FikoYT Flux Loader — Flux dev/schnell/kontext + auto-download + LoRA stack input."""
import os
import torch

try:
    import folder_paths
    import comfy.sd
    import comfy.utils
except ImportError:
    folder_paths = None

from ..core import ensure_model, get_models_dir


FLUX_PRESETS = {
    "flux1-dev (fp8)":     {"repo": "Comfy-Org/flux1-dev", "file": "flux1-dev-fp8.safetensors"},
    "flux1-schnell (fp8)": {"repo": "Comfy-Org/flux1-schnell", "file": "flux1-schnell-fp8.safetensors"},
    "flux1-kontext-dev":   {"repo": "black-forest-labs/FLUX.1-Kontext-dev", "file": "flux1-kontext-dev.safetensors"},
}


class FikoYTFluxLoader:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "preset": (list(FLUX_PRESETS.keys()), {"default": "flux1-dev (fp8)"}),
                "auto_download": ("BOOLEAN", {"default": True}),
                "precision": (["fp16", "bf16", "fp8_e4m3fn"], {"default": "fp8_e4m3fn"}),
            }
        }

    RETURN_TYPES = ("MODEL", "CLIP", "VAE")
    RETURN_NAMES = ("MODEL", "CLIP", "VAE")
    FUNCTION = "load"
    CATEGORY = "FikoYT/image"

    def load(self, preset, auto_download, precision):
        meta = FLUX_PRESETS[preset]
        ckpt_path = os.path.join(get_models_dir("checkpoints"), meta["file"])
        if not os.path.isfile(ckpt_path) and auto_download:
            ckpt_path = ensure_model(meta["repo"], meta["file"], "checkpoints")
        if not os.path.isfile(ckpt_path):
            raise FileNotFoundError(f"Checkpoint yok: {ckpt_path} (auto_download=False)")

        out = comfy.sd.load_checkpoint_guess_config(
            ckpt_path, output_vae=True, output_clip=True,
            embedding_directory=folder_paths.get_folder_paths("embeddings"),
        )
        return (out[0], out[1], out[2])


NODE_CLASS_MAPPINGS = {"FikoYTFluxLoader": FikoYTFluxLoader}
NODE_DISPLAY_NAME_MAPPINGS = {"FikoYTFluxLoader": "FikoYT • Flux Loader"}
```

- [ ] **Step 2: `image/flux_sampler.py`**

```python
"""FikoYT Flux Sampler — KSampler wrapper, Flux defaults, lora_stack support."""
import torch

try:
    import comfy.sample
    import comfy.samplers
    import comfy.utils
    from nodes import common_ksampler
except ImportError:
    common_ksampler = None

from ..core import FIKOYT_LORA_STACK, normalize_seed


class FikoYTFluxSampler:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("MODEL",),
                "positive": ("CONDITIONING",),
                "negative": ("CONDITIONING",),
                "latent_image": ("LATENT",),
                "seed": ("INT", {"default": -1, "min": -1, "max": 2**32 - 1}),
                "steps": ("INT", {"default": 20, "min": 1, "max": 200}),
                "cfg": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 30.0, "step": 0.1}),
                "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler"}),
                "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "simple"}),
                "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
            }
        }

    RETURN_TYPES = ("LATENT",)
    FUNCTION = "sample"
    CATEGORY = "FikoYT/image"

    def sample(self, model, positive, negative, latent_image, seed, steps, cfg, sampler_name, scheduler, denoise):
        seed = normalize_seed(seed)
        return common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise)


NODE_CLASS_MAPPINGS = {"FikoYTFluxSampler": FikoYTFluxSampler}
NODE_DISPLAY_NAME_MAPPINGS = {"FikoYTFluxSampler": "FikoYT • Flux Sampler"}
```

- [ ] **Step 3: `image/__init__.py`**

```python
from .flux_loader import NODE_CLASS_MAPPINGS as _M1, NODE_DISPLAY_NAME_MAPPINGS as _D1
from .flux_sampler import NODE_CLASS_MAPPINGS as _M2, NODE_DISPLAY_NAME_MAPPINGS as _D2

NODE_CLASS_MAPPINGS = {**_M1, **_M2}
NODE_DISPLAY_NAME_MAPPINGS = {**_D1, **_D2}
```

---

### Task F1.4: Top-level `__init__.py` (NODE_CLASS_MAPPINGS aggregator)

**Files:**
- Create: `ComfyUI-FikoYT-Nodes/__init__.py`

- [ ] **Step 1: `__init__.py`**

```python
"""ComfyUI-FikoYT-Nodes — FikoYT custom nodes for image/video/audio/character."""
import importlib
import logging

log = logging.getLogger("FikoYT")

NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./web"

_SUBPACKAGES = ["image", "video", "audio", "character", "util"]

for pkg in _SUBPACKAGES:
    try:
        mod = importlib.import_module(f".{pkg}", package=__name__)
        NODE_CLASS_MAPPINGS.update(getattr(mod, "NODE_CLASS_MAPPINGS", {}))
        NODE_DISPLAY_NAME_MAPPINGS.update(getattr(mod, "NODE_DISPLAY_NAME_MAPPINGS", {}))
    except Exception as e:
        log.warning(f"FikoYT subpackage {pkg} yüklenemedi: {e}")

__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]

log.info(f"FikoYT Nodes loaded: {len(NODE_CLASS_MAPPINGS)} nodes")
```

- [ ] **Step 2: Boş `web/` klasörü + placeholder**

```bash
mkdir -p ComfyUI-FikoYT-Nodes/web/js
echo "// FikoYT JS extensions placeholder" > ComfyUI-FikoYT-Nodes/web/js/placeholder.js
```

- [ ] **Step 3: F2-F5 alt paket iskeletleri (boş __init__.py)**

```bash
for pkg in video audio character util; do
  mkdir -p "ComfyUI-FikoYT-Nodes/$pkg"
  cat > "ComfyUI-FikoYT-Nodes/$pkg/__init__.py" <<'EOF'
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
EOF
done
```

- [ ] **Step 4: Syntax check tüm Python dosyaları**

```bash
find ComfyUI-FikoYT-Nodes -name "*.py" -exec python -c "import ast; ast.parse(open('{}',encoding='utf-8').read())" \;
```
Expected: hata yok.

- [ ] **Step 5: ComfyUI restart + smoke test**

Kullanıcıya bildir: "ComfyUI'yi yeniden başlatın (`run_nvidia_gpu.bat`). Sonra menüde `FikoYT/image/Flux Loader` ve `FikoYT/image/Flux Sampler` görünmeli. Basit bir Flux T2I workflow: Loader → CLIP Text Encode (pozitif/negatif) → Empty Latent → Sampler → VAE Decode → Save Image. Render başarılıysa F1 tamam."

---

## Phase F2 — Image Katmanı (PARALEL, F1 sonrası)

Her node ayrı dosya, ortak pattern: `INPUT_TYPES`, `RETURN_TYPES`, `FUNCTION`, `CATEGORY="FikoYT/image"`, `NODE_CLASS_MAPPINGS`. Sonra `image/__init__.py`'a kaydet.

### Task F2.1: Qwen Image Loader + Multi-Angle

**Files:** Create `image/qwen_loader.py`, `image/qwen_multi_angle.py`

MVD referans: `ComfyUI-MusicVideoDirector/nodes/endpoints.py` Qwen blokları + `pro-studios/image-extra.js` Qwen ayarları.

- [ ] **Step 1: Qwen Loader** — repo: `Qwen/Qwen-Image`, parameters: precision, vram_mode
- [ ] **Step 2: Qwen Multi-Angle node** — input: IMAGE + angle preset (front/side/back/3q), output: IMAGE
- [ ] **Step 3: image/__init__.py'a ekle**
- [ ] **Step 4: Syntax check**

### Task F2.2: SDXL Loader

**Files:** Create `image/sdxl_loader.py`

- [ ] **Step 1:** Preset listesi (Juggernaut XL, RealVisXL, etc.), auto-download, precision
- [ ] **Step 2:** image/__init__.py'a ekle + syntax check

### Task F2.3: PuLID v2 Face Apply

**Files:** Create `image/pulid_v2.py`

MVD referans: `nodes/endpoints.py:1584-1700` (`check_pulid_v2` + `install_pulid_v2`) + ComfyUI_PuLID_Flux_ll node.

- [ ] **Step 1:** Antelopev2 + EVA-CLIP + PuLID-Flux model auto-download
- [ ] **Step 2:** Node: input MODEL + IMAGE (face ref) + weight (FLOAT 0-2) + start_at (FLOAT 0-1), output MODEL
- [ ] **Step 3:** image/__init__.py'a ekle + syntax check

### Task F2.4: InstantID Face Apply

**Files:** Create `image/instantid.py`

MVD referans: `nodes/endpoints.py:221-343` InstantID install bloğu.

- [ ] **Step 1:** InstantX/InstantID + antelopev2 auto-download
- [ ] **Step 2:** Node: MODEL + IMAGE + weight + start_at + end_at, output MODEL + CONDITIONING

### Task F2.5: IPAdapter v2 Style

**Files:** Create `image/ipadapter_v2.py`

MVD referans: `nodes/endpoints.py:343-514` IPAdapter v2 bloğu.

- [ ] **Step 1:** IPAdapter v2 + CLIP Vision auto-download
- [ ] **Step 2:** Node: MODEL + IMAGE (style ref) + weight + noise

### Task F2.6: DWPose Extract

**Files:** Create `image/dwpose_extract.py`

MVD referans: `nodes/endpoints.py:514-720` DWPose install + comfyui_controlnet_aux node.

- [ ] **Step 1:** dw-ll_ucoco_384 + yolox_l onnx auto-download
- [ ] **Step 2:** Node: IMAGE → keypoint image (IMAGE), include face/hands toggle

### Task F2.7: ControlNet Union Apply

**Files:** Create `image/cn_union.py`

MVD referans: `nodes/endpoints.py:716-770` CN Union install.

- [ ] **Step 1:** CN Union (SDXL + Flux) auto-download
- [ ] **Step 2:** Node: CONDITIONING + IMAGE (control) + strength + start_percent + end_percent + control_type (canny/depth/pose/...)

### Task F2.8: SAM2 Mask

**Files:** Create `image/sam2_mask.py`

MVD referans: `ComfyUI-segment-anything-2` node + MVD SAM endpoint.

- [ ] **Step 1:** sam2_hiera_large auto-download
- [ ] **Step 2:** Node: IMAGE + point coords (STRING) veya box → MASK + IMAGE

### Task F2.9: Flux Kontext Edit

**Files:** Create `image/kontext_edit.py`

MVD referans: `nodes/endpoints.py:1959-2160` (`flux_kontext`) + `kontext_chain_render`.

- [ ] **Step 1:** Flux Kontext model (Task F2.1'deki preset ile aynı), T5 + CLIP-L
- [ ] **Step 2:** Node: MODEL + CLIP + VAE + IMAGE (kaynak) + edit_prompt (STRING) + steps + cfg + denoise → IMAGE

### Task F2.10: Style Transfer

**Files:** Create `image/style_transfer.py`

MVD referans: `nodes/endpoints.py:1264-1300` (`character/{cid}/style_transfer`).

- [ ] **Step 1:** Node: IMAGE (kaynak) + IMAGE (style ref) + strength + model_type (IPAdapter/StyleAlign), output IMAGE

---

## Phase F3 — Video Katmanı (PARALEL, F1 sonrası)

Tüm node'lar `CATEGORY="FikoYT/video"`. Ortak pattern: T2V → MODEL+CONDITIONING → LATENT, I2V → MODEL+IMAGE → LATENT.

### Task F3.1: Wan T2V/I2V

**Files:** Create `video/wan_t2v.py`, `video/wan_i2v.py`

MVD referans: `wan/__init__.py`, `wan/s2v_helpers.py`, `wan/endpoints.py`.

- [ ] **Step 1:** Wan 2.2 14B/7B model preset + auto-download (Wan-AI/Wan2.2 repo)
- [ ] **Step 2:** T2V node: text → 81-frame video, parameters: steps, cfg, sampler, scheduler, fps, length, width, height, seed
- [ ] **Step 3:** I2V node: IMAGE → video, ek input motion_strength

### Task F3.2: Wan S2V (Sound-to-Video)

**Files:** Create `video/wan_s2v.py`

MVD referans: `wan/s2v_node.py`, `wan/s2v_helpers.py`.

- [ ] **Step 1:** Wan S2V model + wav2vec2 auto-download
- [ ] **Step 2:** Node: MODEL + AUDIO + IMAGE (ref face) + text_prompt + audio_start_sec + steps + cfg → IMAGE batch (video frames)

### Task F3.3: LTX Distilled

**Files:** Create `video/ltx_loader.py`, `video/ltx_distilled.py`

MVD referans: `ltx/loader.py`, `ltx/video_gen.py`, `ltx/lora_helpers.py`, `ltx/endpoints.py`.

- [ ] **Step 1:** LTX-Video-2B Distilled checkpoint + T5 XXL + LTX VAE auto-download
- [ ] **Step 2:** Loader node: precision (fp16/fp8/bf16), vram_mode, lora_stack input
- [ ] **Step 3:** Generate node: positive + negative + width + height + length (frames) + fps + steps + cfg + stg_scale + image (opsiyonel I2V) → IMAGE batch

### Task F3.4: LTX Interpolate + Upscale

**Files:** Create `video/ltx_interpolate.py`, `video/ltx_upscale.py`

MVD referans: `ltx/endpoints.py:1892-1956`.

- [ ] **Step 1:** Interpolate: IMAGE batch (input frames) → IMAGE batch (multiplied)
- [ ] **Step 2:** Upscale: IMAGE batch + scale_factor + denoise → IMAGE batch

### Task F3.5: HunyuanVideo

**Files:** Create `video/hunyuan_video.py`

MVD referans: ComfyUI-HunyuanVideoWrapper node + MVD Hunyuan studio.

- [ ] **Step 1:** Hunyuan Video model + LLaVA-LLaMA + CLIP-L auto-download
- [ ] **Step 2:** Node: text + length + fps + steps + cfg → IMAGE batch

### Task F3.6: InfiniteTalk

**Files:** Create `video/infinitetalk.py`

MVD referans: `wan/infinitetalk_helpers.py`.

- [ ] **Step 1:** InfiniteTalk + wav2vec auto-download
- [ ] **Step 2:** Node: IMAGE (face) + AUDIO + text_prompt + duration_sec → IMAGE batch

### Task F3.7: HuMo

**Files:** Create `video/humo_generate.py`

MVD referans: `ltx/humo_helpers.py`, `ltx/endpoints.py:1739-1900`.

- [ ] **Step 1:** HuMo model + wav2vec auto-download
- [ ] **Step 2:** Node: IMAGE + AUDIO + text → IMAGE batch

### Task F3.8: Storyboard Concat + Video Grid

**Files:** Create `video/storyboard_concat.py`, `video/video_grid.py`

MVD referans: `ltx/endpoints.py:1521-1605` (`combine_all`) + Storyboard studio.

- [ ] **Step 1:** Storyboard Concat: list of IMAGE batches → single IMAGE batch (crossfade opsiyonel, duration param)
- [ ] **Step 2:** Video Grid: list of IMAGE batches → grid IMAGE (2x2, 3x3, etc.)

---

## Phase F4 — Audio Katmanı (PARALEL, F1 sonrası)

Tüm node'lar `CATEGORY="FikoYT/audio"`. I/O: AUDIO (ComfyUI standart: `{"waveform": Tensor, "sample_rate": int}`).

### Task F4.1: SongGen v2 (LeVo)

**Files:** Create `audio/songgen_loader.py`, `audio/songgen_v2.py`

MVD referans: `music_gen/endpoints.py:1397-1700` + smthemex ComfyUI_SongGeneration node.

- [ ] **Step 1:** Loader: PT (12.9G) veya GGUF Q8 (7.6G) seçimi + auxiliary models auto-download
- [ ] **Step 2:** Generate node: lyrics (STRING multiline) + style_prompt + duration_sec + temperature + top_k + cfg + seed + gguf_mode (bool) → AUDIO

### Task F4.2: F5-TTS Klonlama

**Files:** Create `audio/f5_tts.py`

MVD referans: `music_gen/endpoints.py:3401-3473` (`tts/generate`) + F5-TTS bloğu.

- [ ] **Step 1:** F5-TTS model + vocoder auto-download
- [ ] **Step 2:** Node: text (STRING) + reference_audio (AUDIO) + reference_text (STRING) + speed (FLOAT 0.5-2.0) + max_chunk_sec → AUDIO
- [ ] **Step 3:** MP3 → WAV otomatik dönüştürme (pydub)

### Task F4.3: Edge TTS

**Files:** Create `audio/edge_tts.py`

MVD referans: `music_gen/endpoints.py:3401+` Edge TTS bloğu.

- [ ] **Step 1:** Voice listesi (20 ses) hardcoded enum
- [ ] **Step 2:** Node: text + voice + rate (-50% +50%) + pitch (-50Hz +50Hz) → AUDIO

### Task F4.4: Demucs Vokal Ayrıştırma

**Files:** Create `audio/demucs_split.py`

MVD referans: `music_gen/endpoints.py:2829-2937` Demucs.

- [ ] **Step 1:** htdemucs / mdx_extra auto-download
- [ ] **Step 2:** Node: AUDIO + model_name → AUDIO (vocals), AUDIO (instrumental), AUDIO (drums), AUDIO (bass)

### Task F4.5: Whisper STT

**Files:** Create `audio/whisper_stt.py`

MVD referans: Lyrics Storyboard Whisper bloğu (sprint_w32 memory).

- [ ] **Step 1:** Whisper large-v3 auto-download
- [ ] **Step 2:** Node: AUDIO + language → STRING (transcript) + STRING (SRT JSON segmentleri)

### Task F4.6: Audio Trim + Concat

**Files:** Create `audio/audio_trim.py`, `audio/audio_concat.py`

MVD referans: sprint_w30 audio trimmer.

- [ ] **Step 1:** Trim: AUDIO + start_sec + end_sec → AUDIO (ffmpeg slice)
- [ ] **Step 2:** Concat: list of AUDIO + crossfade_sec → AUDIO

### Task F4.7: Audio Master

**Files:** Create `audio/master_audio.py`

MVD referans: `music_gen/endpoints.py:2720-2829` (`master_audio` + presets).

- [ ] **Step 1:** Mastering presets (bright/warm/cinematic/podcast) hardcoded
- [ ] **Step 2:** Node: AUDIO + preset + target_lufs + true_peak → AUDIO (ffmpeg EQ + compressor + limiter chain)

---

## Phase F5 — Character + Util (PARALEL, F1 sonrası)

### Task F5.1: Character Bind + Unpack

**Files:** Create `character/__init__.py`, `character/bind.py`, `character/unpack.py`

- [ ] **Step 1:** Bind node: name + face_image (IMAGE optional) + lora_stack (FIKOYT_LORA_STACK optional) + ref_image + base_prompt + base_neg → FIKOYT_CHARACTER
- [ ] **Step 2:** Unpack node: FIKOYT_CHARACTER → IMAGE (ref), STRING (prompt), STRING (neg), FIKOYT_LORA_STACK

### Task F5.2: LoRA Stack Apply

**Files:** Create `character/lora_stack_apply.py`

- [ ] **Step 1:** Node: MODEL + CLIP + FIKOYT_LORA_STACK → MODEL + CLIP (her LoRA sırayla uygulanır)

### Task F5.3: Face Swap Reactor

**Files:** Create `character/face_swap_reactor.py`

MVD referans: `nodes/endpoints.py:1299-1315` (`check_reactor`).

- [ ] **Step 1:** Reactor models auto-download (inswapper_128 + GFPGAN)
- [ ] **Step 2:** Node: IMAGE (target) + IMAGE (face source) + face_restore (bool) + face_index → IMAGE

### Task F5.4: Util — Aspect Preset

**Files:** Create `util/__init__.py`, `util/aspect_preset.py`

- [ ] **Step 1:** Node: preset (dropdown ASPECT_PRESETS) → INT (width), INT (height), LATENT (empty)

### Task F5.5: Util — Seed Control

**Files:** Create `util/seed_control.py`

- [ ] **Step 1:** Node: mode (fixed/random/incremental) + seed → INT (seed)

### Task F5.6: Util — Prompt Builder

**Files:** Create `util/prompt_builder.py`

- [ ] **Step 1:** Node: template (STRING multiline with `{character}` `{scene}` `{style}` placeholders) + character (FIKOYT_CHARACTER optional) + scene + style → STRING

### Task F5.7: Util — LoRA Picker

**Files:** Create `util/lora_picker.py`, `web/js/lora_picker.js`

- [ ] **Step 1:** Node: registry'den dropdown (kategoriye göre) + strength_model + strength_clip → FIKOYT_LORA_STACK (tek item)
- [ ] **Step 2:** Stack Append node: FIKOYT_LORA_STACK + FIKOYT_LORA_STACK → FIKOYT_LORA_STACK (birleştirir)

### Task F5.8: Util — File Picker

**Files:** Create `util/file_picker.py`, `web/js/file_picker.js`

MVD referans: `web/js/mvd/panel-core.js` `fmvdPickFile` global + sprint_w24 file picker pattern.

- [ ] **Step 1:** Node: file_type (image/audio/video) + display_label → STRING (path)
- [ ] **Step 2:** JS extension: tile büyük + scroll + z-index 2147400000 (MVD pattern)

### Task F5.9: Util — Gallery Save

**Files:** Create `util/gallery_save.py`

- [ ] **Step 1:** Node: IMAGE + project_name + filename_prefix → STRING (saved path) — `ComfyUI/output/fikoyt/{project_name}/` altına yazar

---

## Phase F6 — Yayın (SIRALI, EN SON)

### Task F6.1: README + örnek workflow JSON'lar

**Files:**
- Modify: `README.md`
- Create: `examples/flux_t2i.json`
- Create: `examples/wan_s2v.json`
- Create: `examples/songgen_v2.json`

- [ ] **Step 1:** 3 örnek workflow JSON (ComfyUI'den export, FikoYT node'larıyla)
- [ ] **Step 2:** README'ye kategori listesi + node sayısı + örnek screenshot referansları

### Task F6.2: Curated registry tam

**Files:**
- Modify: `core/registry_data/loras.json` (58 entry)
- Modify: `core/registry_data/models.json` (32 entry)

- [ ] **Step 1:** MVD'deki `CURATED_LORAS` ve `MODELS_REGISTRY`'den tam transfer
- [ ] **Step 2:** Syntax: `python -c "import json; json.load(open('...'))"` check

### Task F6.3: Full system smoke test

- [ ] **Step 1:** ComfyUI restart, menüde 60+ node listesi sayımı (`NODE_CLASS_MAPPINGS` len)
- [ ] **Step 2:** 3 örnek workflow JSON'u yükle + her birini render et
- [ ] **Step 3:** Başarı kriteri (spec §10) maddelerini tek tek tikle

### Task F6.4: Registry yayın hazırlık (opsiyonel)

- [ ] **Step 1:** `comfy-cli` ile registry publish denemesi (kullanıcı isterse)

---

## Self-Review Notları

- **Spec coverage**: Tüm spec §1-§10 maddeleri F1-F6'da temsil ediliyor (F1=iskelet, F2-F5=parite, F6=yayın).
- **Placeholder check**: F2-F5 task'larında her node için MVD referans path'i + parametreler verildi. "TBD" yok.
- **Type consistency**: `FIKOYT_CHARACTER` ve `FIKOYT_LORA_STACK` her kullanıldığı yerde aynı (string sabitler `core/types.py`'da tanımlı).
- **Critical path**: F1 → (F2 ∥ F3 ∥ F4 ∥ F5) → F6. F2-F5 birbirinden bağımsız.
