{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# \u26a1 VOXFORJA GPU WORKER (Kaggle Cloud On-Demand Engine)\n",
        "### *Nvidia Tesla T4 x2 \u00b7 Faster-Whisper IA \u00b7 Coqui XTTS-v2 \u00b7 Auto-Apagado por Inactividad*\n",
        "\n",
        "Este kernel es activado autom\u00e1ticamente por la API de VoxForja cuando hay videos o locuciones en cola.\n",
        "Al finalizar los trabajos y permanecer 5 minutos inactivo, se apaga autom\u00e1ticamente para no consumir cuota."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import subprocess, sys, os\n",
        "import torch\n",
        "\n",
        "if torch.cuda.is_available():\n",
        "    gpu_name = torch.cuda.get_device_name(0)\n",
        "    gpu_vram = round(torch.cuda.get_device_properties(0).total_memory / (1024**3), 2)\n",
        "    print(f\"\ud83d\udd25 [GPU Conectada] {gpu_name} ({gpu_vram} GB VRAM)\\n\")\n",
        "else:\n",
        "    print(\"\u2139\ufe0f GPU dedicada no asignada en esta sesi\u00f3n. Operando en modo Alta Potencia CPU Multihilo.\\n\")\n",
        "\n",
        "print(\"\ud83d\udce6 [1/4] Instalando FFmpeg y Fuentes del Sistema...\")\n",
        "!apt-get update -qq > /dev/null 2>&1 && apt-get install -y -qq ffmpeg fonts-dejavu-core fonts-freefont-ttf fonts-liberation > /dev/null 2>&1\n",
        "\n",
        "print(\"\ud83d\udce6 [2/4] Instalando Motores de Voz y Video (EdgeTTS, gTTS, Websockets, OpenCV, GDown)...\")\n",
        "!pip install -q --no-warn-conflicts edge-tts gTTS \"websockets>=13.0\" nest_asyncio urllib3 requests opencv-python-headless gdown\n",
        "\n",
        "print(\"\ud83d\udce6 [3/4] Instalando Faster-Whisper IA y Librer\u00edas CUDA 12...\")\n",
        "!pip install -q --no-warn-conflicts faster-whisper nvidia-cublas-cu12 nvidia-cudnn-cu12\n",
        "\n",
        "print(\"\ud83d\udce6 [4/4] Instalando Motor de Clonaci\u00f3n Zero-Shot IA (Coqui XTTS-v2 & SoundFile)...\")\n",
        "try:\n",
        "    subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"coqui-tts==0.27.5\", \"transformers==4.57.5\", \"coqui-tts-trainer>=0.3.0,<0.4.0\", \"soundfile\", \"librosa\"], check=True)\n",
        "except Exception as e:\n",
        "    print(f\"\u26a0\ufe0f Instalaci\u00f3n de Coqui XTTS opcional advertencia: {e}\")\n",
        "\n",
        "print(\"\\n\ud83e\uddea Verificando herramientas en Python:\")\n",
        "ff_check = subprocess.run([\"ffmpeg\", \"-version\"], capture_output=True, text=True)\n",
        "if ff_check.returncode == 0:\n",
        "    print(\"  \u2705 [1/7] FFmpeg Video Engine: OPERATIVO\")\n",
        "else:\n",
        "    print(\"  \u274c [1/7] FFmpeg Video Engine: ERROR\")\n",
        "\n",
        "try:\n",
        "    import faster_whisper\n",
        "    print(\"  \u2705 [2/7] Faster-Whisper Subt\u00edtulos IA: OPERATIVO\")\n",
        "except Exception as e:\n",
        "    print(f\"  \u274c [2/7] Faster-Whisper: ERROR ({e})\")\n",
        "\n",
        "try:\n",
        "    import cv2\n",
        "    print(\"  \u2705 [3/7] OpenCV Smart Face & Layout Detector: OPERATIVO\")\n",
        "except Exception as e:\n",
        "    print(f\"  \u274c [3/7] OpenCV: ERROR ({e})\")\n",
        "\n",
        "try:\n",
        "    import gdown\n",
        "    print(\"  \u2705 [4/7] Google Drive Sync (gdown): OPERATIVO\")\n",
        "except Exception as e:\n",
        "    print(f\"  \u274c [4/7] Google Drive Sync: ERROR ({e})\")\n",
        "\n",
        "try:\n",
        "    import edge_tts\n",
        "    print(\"  \u2705 [5/7] Motor de Voz Neuronal (EdgeTTS): OPERATIVO\")\n",
        "except Exception as e:\n",
        "    print(f\"  \u274c [5/7] Motor de Voz Neuronal: ERROR ({e})\")\n",
        "\n",
        "try:\n",
        "    import websockets\n",
        "    print(\"  \u2705 [6/7] Puente de Comunicaci\u00f3n en Vivo: OPERATIVO\")\n",
        "except Exception as e:\n",
        "    print(f\"  \u274c [6/7] Websockets: ERROR ({e})\")\n",
        "\n",
        "try:\n",
        "    from TTS.api import TTS\n",
        "    print(\"  \u2705 [7/7] Motor de Clonaci\u00f3n Vocal Multiling\u00fce (Coqui XTTS-v2): OPERATIVO\")\n",
        "except Exception as e:\n",
        "    print(f\"  \u26a0\ufe0f [7/7] Motor de Clonaci\u00f3n Vocal (XTTS-v2): EN ESPERA ({e})\")\n",
        "\n",
        "print(\"\\n\ud83d\ude80 \u00a1ENTORNO INICIALIZADO! Conectando autom\u00e1ticamente al Master...\\n\")\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "#@title \ud83c\udf10 [2/2] Iniciar Worker Daemon con Motores 9:16 Vertical, Cinema 16:9 y Voice Studio\n",
        "import subprocess, sys, os, glob, ctypes, time, re, shutil, ssl, urllib3, warnings\n",
        "from pathlib import Path\n",
        "import numpy as np\n",
        "\n",
        "\n",
        "# Configurar librer\u00edas CUDA para CTranslate2 y Faster-Whisper\n",
        "def setup_cuda_libraries():\n",
        "    paths = [\"/usr/local/cuda/lib64\", \"/usr/local/cuda-12/lib64\", \"/usr/local/cuda-11/lib64\"]\n",
        "    for p in sys.path:\n",
        "        for lib_dir in glob.glob(os.path.join(p, \"nvidia\", \"*\", \"lib\")):\n",
        "            if os.path.exists(lib_dir):\n",
        "                paths.append(lib_dir)\n",
        "                for so in glob.glob(os.path.join(lib_dir, \"*.so*\")):\n",
        "                    try:\n",
        "                        ctypes.CDLL(so)\n",
        "                    except Exception:\n",
        "                        pass\n",
        "    for path in paths:\n",
        "        if os.path.exists(path) and path not in os.environ.get(\"LD_LIBRARY_PATH\", \"\"):\n",
        "            os.environ[\"LD_LIBRARY_PATH\"] = path + \":\" + os.environ.get(\"LD_LIBRARY_PATH\", \"\")\n",
        "\n",
        "setup_cuda_libraries()\n",
        "\n",
        "# Auto-instalar dependencias autom\u00e1ticamente si faltan de forma individual\n",
        "def ensure_dependencies():\n",
        "    packages = [\n",
        "        (\"faster_whisper\", \"faster-whisper\"),\n",
        "        (\"cv2\", \"opencv-python-headless\"),\n",
        "        (\"gdown\", \"gdown\"),\n",
        "        (\"gtts\", \"gTTS\"),\n",
        "        (\"edge_tts\", \"edge-tts\"),\n",
        "        (\"websockets\", \"websockets>=13.0\"),\n",
        "        (\"nest_asyncio\", \"nest_asyncio\")\n",
        "    ]\n",
        "    for mod_name, pkg_name in packages:\n",
        "        try:\n",
        "            __import__(mod_name)\n",
        "        except Exception:\n",
        "            try:\n",
        "                subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"--no-warn-conflicts\", pkg_name])\n",
        "            except Exception:\n",
        "                pass\n",
        "    try:\n",
        "        from TTS.api import TTS\n",
        "    except Exception:\n",
        "        try:\n",
        "            print(\"\ud83d\udce6 Auto-instalando Motor de Clonaci\u00f3n Vocal Multiling\u00fce (Coqui XTTS-v2 & SoundFile)...\", flush=True)\n",
        "            subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"--no-warn-conflicts\", \"coqui-tts==0.27.5\", \"transformers==4.57.5\", \"coqui-tts-trainer>=0.3.0,<0.4.0\", \"soundfile\", \"librosa\"])\n",
        "        except Exception as e_inst:\n",
        "            print(f\"\u2139\ufe0f Advertencia en instalaci\u00f3n de clonador: {e_inst}\", flush=True)\n",
        "\n",
        "    setup_cuda_libraries()\n",
        "\n",
        "ensure_dependencies()\n",
        "\n",
        "import asyncio, websockets, json, requests, torch, nest_asyncio\n",
        "import cv2\n",
        "\n",
        "warnings.filterwarnings(\"ignore\")\n",
        "urllib3.disable_warnings()\n",
        "nest_asyncio.apply()\n",
        "\n",
        "MASTER_WS_URL = \"wss://musicstudio.cyou/ws/worker?token=wentix_gpu_secret_token_2026\"\n",
        "MASTER_HTTP_URL = \"https://musicstudio.cyou\"\n",
        "\n",
        "DEVICE = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n",
        "GPU_NAME = torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\"\n",
        "GPU_VRAM = f\"{round(torch.cuda.get_device_properties(0).total_memory / (1024**3), 1)} GB\" if torch.cuda.is_available() else \"0 GB\"\n",
        "\n",
        "ssl_context = ssl.create_default_context()\n",
        "ssl_context.check_hostname = False\n",
        "ssl_context.verify_mode = ssl.CERT_NONE\n",
        "\n",
        "CURRENT_CANCELLED_JOBS = set()\n",
        "ACTIVE_RUNNING_JOBS = set()\n",
        "\n",
        "# --- 16 ESTILOS VIRALES CON BORDE NEGRO EXTRA GRUESO (8px) Y SOMBRA DE IMPACTO ---\n",
        "VIRAL_CAPTION_STYLES = [\n",
        "    {\"id\": \"hormozi_1w_yellow\", \"name\": \"Hormozi Classic (1 Palabra Amarillo Oro)\", \"chunk\": 1, \"size\": 78, \"c1\": \"&H00D7FF&\", \"c2\": \"&H00D7FF&\", \"bicolor\": False, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"hormozi_2w_bicolor\", \"name\": \"Hormozi 2.0 (2 Palabras Amarillo & Blanco)\", \"chunk\": 2, \"size\": 66, \"c1\": \"&H00D7FF&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"beast_1w_cyan\", \"name\": \"Beast Mode (1 Palabra Cyan Pop)\", \"chunk\": 1, \"size\": 80, \"c1\": \"&HFFFF00&\", \"c2\": \"&HFFFF00&\", \"bicolor\": False, \"bord\": 9, \"shad\": 4},\n",
        "    {\"id\": \"beast_1w_lime\", \"name\": \"Beast Glow (1 Palabra Verde Ne\u00f3n)\", \"chunk\": 1, \"size\": 80, \"c1\": \"&H14FF39&\", \"c2\": \"&H14FF39&\", \"bicolor\": False, \"bord\": 9, \"shad\": 4},\n",
        "    {\"id\": \"two_words_gold\", \"name\": \"Two Words Gold (2 Palabras Oro & Blanco)\", \"chunk\": 2, \"size\": 64, \"c1\": \"&H00C8FF&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"two_words_cyan\", \"name\": \"Two Words Cyan (2 Palabras Cyan & Blanco)\", \"chunk\": 2, \"size\": 64, \"c1\": \"&HFFFF00&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"fire_word_red\", \"name\": \"Fire Word (1 Palabra Rojo Fuego)\", \"chunk\": 1, \"size\": 82, \"c1\": \"&H0022FF&\", \"c2\": \"&H0022FF&\", \"bicolor\": False, \"bord\": 9, \"shad\": 5},\n",
        "    {\"id\": \"devin_1w_magenta\", \"name\": \"Devin Magenta (1 Palabra Hot Pink)\", \"chunk\": 1, \"size\": 78, \"c1\": \"&HF000FF&\", \"c2\": \"&HF000FF&\", \"bicolor\": False, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"iman_1w_white_titan\", \"name\": \"Iman Titan (1 Palabra Blanco Puro con Borde 9px)\", \"chunk\": 1, \"size\": 82, \"c1\": \"&HFFFFFF&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": False, \"bord\": 9, \"shad\": 5},\n",
        "    {\"id\": \"ali_2w_clean_white\", \"name\": \"Ali Clean (2 Palabras Blanco con Borde 8px)\", \"chunk\": 2, \"size\": 64, \"c1\": \"&HFFFFFF&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": False, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"two_words_pink\", \"name\": \"Two Words Pink (2 Palabras Rosa & Blanco)\", \"chunk\": 2, \"size\": 64, \"c1\": \"&HFF1493&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"cyber_duo_cyan_purple\", \"name\": \"Cyber Duo (2 Palabras Cyan & P\u00farpura)\", \"chunk\": 2, \"size\": 64, \"c1\": \"&HFFFF00&\", \"c2\": \"&HFF00AA&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"power_1w_orange\", \"name\": \"Power Orange (1 Palabra Naranja Intenso)\", \"chunk\": 1, \"size\": 80, \"c1\": \"&H0077FF&\", \"c2\": \"&H0077FF&\", \"bicolor\": False, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"emerald_1w_neon\", \"name\": \"Emerald Neon (1 Palabra Esmeralda Ne\u00f3n)\", \"chunk\": 1, \"size\": 80, \"c1\": \"&H55FF00&\", \"c2\": \"&H55FF00&\", \"bicolor\": False, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"two_words_lime_gold\", \"name\": \"Two Words Lime (2 Palabras Verde Lima & Blanco)\", \"chunk\": 2, \"size\": 64, \"c1\": \"&H14FF39&\", \"c2\": \"&HFFFFFF&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4},\n",
        "    {\"id\": \"pure_white_pop_2w\", \"name\": \"Pure Pop 2W (2 Palabras Blanco & Oro)\", \"chunk\": 2, \"size\": 66, \"c1\": \"&HFFFFFF&\", \"c2\": \"&H00D7FF&\", \"bicolor\": True, \"bord\": 8, \"shad\": 4}\n",
        "]\n",
        "\n",
        "# --- 6 ONDAS MODERNAS 9:16: 100% OPACIDAD S\u00d3LIDA, ALTO BRILLO NE\u00d3N ---\n",
        "MODERN_WAVEFORMS_916 = [\n",
        "    {\"name\": \"Micro-P\u00edldoras Spotify Canvas\", \"filter\": \"[1:a]showwaves=s=880x95:mode=p2p:colors=#00F5FF@1.0|#FFFFFF@0.9:scale=sqrt[wave]\", \"base_y\": 1420},\n",
        "    {\"name\": \"L\u00ednea L\u00e1ser Cyber Horizon\", \"filter\": \"[1:a]showwaves=s=920x80:mode=line:colors=#FF007F@1.0|#FFD700@0.9:scale=lin[wave]\", \"base_y\": 1420},\n",
        "    {\"name\": \"Puntos Flotantes LED Ne\u00f3n\", \"filter\": \"[1:a]showwaves=s=880x90:mode=point:colors=#FFD700@1.0|#FFFFFF@0.9:scale=lin[wave]\", \"base_y\": 1420},\n",
        "    {\"name\": \"Dual-Micro Sim\u00e9trico\", \"filter\": \"[1:a]showwaves=s=860x95:mode=p2p:colors=#A855F7@1.0|#00F5FF@0.9:scale=log[wave]\", \"base_y\": 1420},\n",
        "    {\"name\": \"Cinta Fluida Din\u00e1mica\", \"filter\": \"[1:a]showwaves=s=900x85:mode=line:colors=#00FF66@1.0|#FFFFFF@0.9:scale=cbrt[wave]\", \"base_y\": 1420},\n",
        "    {\"name\": \"Barras Ecualizador Glow\", \"filter\": \"[1:a]showfreqs=s=880x95:mode=bar:colors=#38BDF8@1.0|#FF007F@0.9:scale=sqrt[wave]\", \"base_y\": 1420}\n",
        "]\n",
        "\n",
        "# --- ONDAS PANOR\u00c1MICAS 16:9 (1400px ANCHO) ---\n",
        "PANORAMIC_WAVEFORMS_169 = {\n",
        "    \"spotify_horizontal\": {\"name\": \"Spotify Panor\u00e1mica\", \"filter\": \"showwaves=s=1400x90:mode=p2p:colors=#00F5FF@1.0|#FFFFFF@0.9:scale=sqrt[wave]\", \"y\": 940},\n",
        "    \"laser_cyber\": {\"name\": \"L\u00e1ser Cyber Horizon\", \"filter\": \"showwaves=s=1400x80:mode=line:colors=#FF007F@1.0|#FFD700@0.9:scale=lin[wave]\", \"y\": 945},\n",
        "    \"neon_bars\": {\"name\": \"Barras Ecualizador\", \"filter\": \"showfreqs=s=1400x90:mode=bar:colors=#38BDF8@1.0|#FF007F@0.9:scale=sqrt[wave]\", \"y\": 940},\n",
        "    \"dual_wave\": {\"name\": \"Dual-Micro Ne\u00f3n\", \"filter\": \"showwaves=s=1400x90:mode=p2p:colors=#A855F7@1.0|#00F5FF@0.9:scale=log[wave]\", \"y\": 940},\n",
        "    \"fluid_wave\": {\"name\": \"Cinta Fluida\", \"filter\": \"showwaves=s=1400x85:mode=line:colors=#00FF66@1.0|#FFFFFF@0.9:scale=cbrt[wave]\", \"y\": 942}\n",
        "}\n",
        "\n",
        "# --- \ud83c\udfaf MOTOR DE POSICIONAMIENTO VISUAL 9:16: SUBT\u00cdTULOS ANCLADOS ESTRICTAMENTE A LA CINTURA (Y \u2248 1020px) ---\n",
        "def analyze_smart_layout(image_path):\n",
        "    try:\n",
        "        img = cv2.imread(str(image_path))\n",
        "        if img is None:\n",
        "            return {\"detected\": False, \"wave_y\": 1420, \"sub_margin_v\": 900, \"layout\": \"DEFAULT_WAIST\"}\n",
        "        \n",
        "        h, w = img.shape[:2]\n",
        "        scale_y = 1920.0 / h\n",
        "        scale_x = 1080.0 / w\n",
        "        \n",
        "        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n",
        "        blurred = cv2.bilateralFilter(gray, 9, 75, 75)\n",
        "        edges = cv2.Canny(blurred, 40, 140)\n",
        "        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))\n",
        "        dilated = cv2.dilate(edges, kernel, iterations=2)\n",
        "        contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
        "        \n",
        "        candidate_cards = []\n",
        "        for cnt in contours:\n",
        "            x, y, cw, ch = cv2.boundingRect(cnt)\n",
        "            norm_x = int(x * scale_x)\n",
        "            norm_y = int(y * scale_y)\n",
        "            norm_w = int(cw * scale_x)\n",
        "            norm_h = int(ch * scale_y)\n",
        "            \n",
        "            if norm_w >= 450 and 100 <= norm_h <= 900:\n",
        "                aspect = norm_w / float(norm_h)\n",
        "                if 1.1 <= aspect <= 7.5:\n",
        "                    candidate_cards.append({\n",
        "                        \"ymin\": norm_y,\n",
        "                        \"ymax\": norm_y + norm_h,\n",
        "                        \"height\": norm_h,\n",
        "                        \"center_y\": norm_y + (norm_h // 2),\n",
        "                        \"width\": norm_w,\n",
        "                        \"area\": norm_w * norm_h\n",
        "                    })\n",
        "        \n",
        "        best_card = None\n",
        "        if candidate_cards:\n",
        "            candidate_cards.sort(key=lambda c: (c[\"center_y\"] > 700, c[\"area\"]), reverse=True)\n",
        "            best_card = candidate_cards[0]\n",
        "        \n",
        "        if best_card:\n",
        "            card_ymin = best_card[\"ymin\"]\n",
        "            card_ymax = best_card[\"ymax\"]\n",
        "            wave_y = min(1700, max(1350, card_ymax + 15))\n",
        "            target_sub_y = min(1040, max(980, card_ymin - 120))\n",
        "            sub_margin_v = 1920 - target_sub_y\n",
        "            layout = \"WAIST_ABOVE_PLAYER\"\n",
        "        else:\n",
        "            wave_y = 1420\n",
        "            target_sub_y = 1020\n",
        "            sub_margin_v = 900\n",
        "            layout = \"WAIST_STANDARD\"\n",
        "            \n",
        "        print(f\"\ud83c\udfaf [Smart Layout] {layout} -> Subt\u00edtulos en CINTURA: Y={target_sub_y}px (MarginV={sub_margin_v}) | Onda: Y={wave_y}px\", flush=True)\n",
        "        return {\n",
        "            \"detected\": True if best_card else False,\n",
        "            \"wave_y\": int(wave_y),\n",
        "            \"sub_margin_v\": int(sub_margin_v),\n",
        "            \"layout\": layout,\n",
        "            \"card\": best_card\n",
        "        }\n",
        "    except Exception as e:\n",
        "        print(f\"\u2139\ufe0f Smart Layout fallback: {e}\", flush=True)\n",
        "        return {\"detected\": False, \"wave_y\": 1420, \"sub_margin_v\": 900, \"layout\": \"DEFAULT_WAIST\"}\n",
        "\n",
        "# Normalizador de URLs\n",
        "def normalize_asset_url(url_or_path):\n",
        "    if not url_or_path:\n",
        "        return \"\"\n",
        "    u = str(url_or_path).strip()\n",
        "    if u.startswith(\"http://\") or u.startswith(\"https://\"):\n",
        "        return u\n",
        "    if u.startswith(\"/\"):\n",
        "        return f\"{MASTER_HTTP_URL}{u}\"\n",
        "    return f\"{MASTER_HTTP_URL}/{u}\"\n",
        "\n",
        "# Ordenamiento natural alfanum\u00e9rico (01.png, 02.png, ..., 10.png)\n",
        "def natural_sort_key(s):\n",
        "    return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\\d+)', str(s))]\n",
        "\n",
        "# Obtener duraci\u00f3n exacta de un archivo con ffprobe\n",
        "def get_media_duration(file_path):\n",
        "    try:\n",
        "        cmd = [\n",
        "            \"ffprobe\", \"-v\", \"error\",\n",
        "            \"-show_entries\", \"format=duration\",\n",
        "            \"-of\", \"default=noprint_wrappers=1:nokey=1\",\n",
        "            str(file_path)\n",
        "        ]\n",
        "        res = subprocess.run(cmd, capture_output=True, text=True, check=True)\n",
        "        return float(res.stdout.strip())\n",
        "    except Exception:\n",
        "        return 0.0\n",
        "\n",
        "# Descarga robusta con streaming y reintentos\n",
        "def download_file_robust(raw_url, dest_path, max_retries=3):\n",
        "    url = normalize_asset_url(raw_url)\n",
        "    if not url:\n",
        "        return False\n",
        "    for attempt in range(max_retries):\n",
        "        try:\n",
        "            with requests.get(url, verify=False, stream=True, timeout=(10, 60)) as r:\n",
        "                r.raise_for_status()\n",
        "                with open(dest_path, \"wb\") as f:\n",
        "                    for chunk in r.iter_content(chunk_size=65536):\n",
        "                        if chunk:\n",
        "                            f.write(chunk)\n",
        "            if dest_path.exists() and dest_path.stat().st_size > 0:\n",
        "                return True\n",
        "        except Exception as e:\n",
        "            print(f\"\u26a0\ufe0f Reintento descarga ({attempt+1}/{max_retries}) de {url}: {e}\", flush=True)\n",
        "            time.sleep(2)\n",
        "    return False\n",
        "\n",
        "# Subida robusta de video individual\n",
        "def upload_single_video_robust(job_id, video_path, is_final=False, max_retries=3):\n",
        "    v_path = Path(video_path)\n",
        "    if not v_path.exists() or v_path.stat().st_size == 0:\n",
        "        return False\n",
        "    for attempt in range(max_retries):\n",
        "        try:\n",
        "            with open(v_path, \"rb\") as f:\n",
        "                files = [(\"videos\", (v_path.name, f, \"video/mp4\"))]\n",
        "                data = {\n",
        "                    \"jobId\": job_id,\n",
        "                    \"isChunk\": \"true\",\n",
        "                    \"isFinal\": \"true\" if is_final else \"false\"\n",
        "                }\n",
        "                r = requests.post(\n",
        "                    f\"{MASTER_HTTP_URL}/api/worker/upload-result\",\n",
        "                    data=data,\n",
        "                    files=files,\n",
        "                    verify=False,\n",
        "                    timeout=(15, 180)\n",
        "                )\n",
        "                if r.status_code == 200:\n",
        "                    return True\n",
        "        except Exception as e:\n",
        "            print(f\"\u26a0\ufe0f Reintento subida ({attempt+1}/{max_retries}) de {v_path.name}: {e}\", flush=True)\n",
        "            time.sleep(3)\n",
        "    return False\n",
        "\n",
        "# Lazy-load Faster-Whisper\n",
        "whisper_model = None\n",
        "def get_whisper():\n",
        "    global whisper_model\n",
        "    if whisper_model is None:\n",
        "        setup_cuda_libraries()\n",
        "        try:\n",
        "            from faster_whisper import WhisperModel\n",
        "            print(\"\u26a1 Cargando Faster-Whisper IA (medium en GPU)...\", flush=True)\n",
        "            whisper_model = WhisperModel(\"medium\", device=\"cuda\", compute_type=\"float16\", download_root=\"/tmp/whisper_models\")\n",
        "        except Exception as e_gpu:\n",
        "            print(f\"\u2139\ufe0f Inicializando Faster-Whisper en CPU: {e_gpu}\", flush=True)\n",
        "            from faster_whisper import WhisperModel\n",
        "            whisper_model = WhisperModel(\"medium\", device=\"cpu\", compute_type=\"int8\", download_root=\"/tmp/whisper_models\")\n",
        "    return whisper_model\n",
        "\n",
        "# Lazy-load Motor de Clonaci\u00f3n Vocal Multiling\u00fce (Coqui XTTS-v2)\n",
        "voice_cloner_model = None\n",
        "\n",
        "def get_voice_cloner():\n",
        "    global voice_cloner_model\n",
        "    if voice_cloner_model is None:\n",
        "        print(\"\u26a1 Cargando Motor Coqui XTTS-v2 Multiling\u00fce (Espa\u00f1ol Latino/Castellano & 17 Idiomas) en GPU...\", flush=True)\n",
        "        import transformers\n",
        "        from importlib.metadata import version\n",
        "        if transformers.__version__ != \"4.57.5\" or version(\"coqui-tts\") != \"0.27.5\":\n",
        "            raise RuntimeError(\"Versiones XTTS incompatibles. Ejecuta el Paso 1 actualizado, reinicia la sesi\u00f3n de Colab y ejecuta el Paso 2.\")\n",
        "        print(f\"XTTS runtime: coqui-tts={version('coqui-tts')}, transformers={transformers.__version__}\", flush=True)\n",
        "        os.environ[\"COQUI_TOS_AGREED\"] = \"1\"\n",
        "        try:\n",
        "            from TTS.api import TTS\n",
        "            voice_cloner_model = TTS(\"tts_models/multilingual/multi-dataset/xtts_v2\").to(DEVICE)\n",
        "            print(\"\u2705 Motor Coqui XTTS-v2 listo en GPU.\", flush=True)\n",
        "        except Exception as e_xtts:\n",
        "            print(f\"\u274c Error cargando Coqui XTTS-v2 en GPU: {e_xtts}\", flush=True)\n",
        "            raise e_xtts\n",
        "    return voice_cloner_model\n",
        "\n",
        "def decode_audio_safe(audio_path):\n",
        "    cmd = [\n",
        "        \"ffmpeg\", \"-nostdin\", \"-threads\", \"0\", \"-i\", str(audio_path),\n",
        "        \"-f\", \"s16le\", \"-ac\", \"1\", \"-acodec\", \"pcm_s16le\", \"-ar\", \"16000\", \"-\"\n",
        "    ]\n",
        "    out = subprocess.run(cmd, capture_output=True, check=True).stdout\n",
        "    return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0\n",
        "\n",
        "# Smart Phrasing para 9:16\n",
        "def extract_smart_phrase_segments(audio_path, variants_count=10, requested_duration=None):\n",
        "    total_dur = get_media_duration(audio_path)\n",
        "    if total_dur <= 0: total_dur = 180.0\n",
        "    \n",
        "    if requested_duration and str(requested_duration).isdigit() and int(requested_duration) > 0 and int(requested_duration) != 30:\n",
        "        fixed_dur = int(requested_duration)\n",
        "        blocks = []\n",
        "        max_st = max(0, total_dur - fixed_dur)\n",
        "        for i in range(variants_count):\n",
        "            st = round(i * (max_st / max(1, variants_count - 1)), 1) if variants_count > 1 else 0.0\n",
        "            blocks.append({\"start\": st, \"duration\": fixed_dur, \"label\": f\"Fixed {fixed_dur}s [{st}s]\"})\n",
        "        return blocks\n",
        "        \n",
        "    print(\"\ud83c\udfbc [Smart Phrasing] Escaneando frases musicales y silencios con Whisper IA...\", flush=True)\n",
        "    model = get_whisper()\n",
        "    try:\n",
        "        audio_data = decode_audio_safe(audio_path)\n",
        "        segments, _ = model.transcribe(audio_data, word_timestamps=True, language=language)\n",
        "    except Exception:\n",
        "        segments, _ = model.transcribe(str(audio_path), word_timestamps=True, language=language)\n",
        "        \n",
        "    all_words = []\n",
        "    for seg in segments:\n",
        "        if hasattr(seg, 'words') and seg.words:\n",
        "            for w in seg.words:\n",
        "                if w.word.strip():\n",
        "                    all_words.append({\"word\": w.word.strip(), \"start\": w.start, \"end\": w.end})\n",
        "        else:\n",
        "            if seg.text.strip():\n",
        "                all_words.append({\"word\": seg.text.strip(), \"start\": seg.start, \"end\": seg.end})\n",
        "                \n",
        "    if not all_words:\n",
        "        blocks = []\n",
        "        max_st = max(0, total_dur - 30)\n",
        "        for i in range(variants_count):\n",
        "            st = round(i * (max_st / max(1, variants_count - 1)), 1) if variants_count > 1 else 0.0\n",
        "            blocks.append({\"start\": st, \"duration\": min(30.0, total_dur - st), \"label\": f\"Instrumental [{st}s]\"})\n",
        "        return blocks\n",
        "\n",
        "    phrases = []\n",
        "    curr = [all_words[0]]\n",
        "    for w in all_words[1:]:\n",
        "        pause = w[\"start\"] - curr[-1][\"end\"]\n",
        "        if pause >= 1.1:\n",
        "            phrases.append({\"start\": curr[0][\"start\"], \"end\": curr[-1][\"end\"], \"text\": \" \".join([x[\"word\"] for x in curr])})\n",
        "            curr = [w]\n",
        "        else:\n",
        "            curr.append(w)\n",
        "    if curr:\n",
        "        phrases.append({\"start\": curr[0][\"start\"], \"end\": curr[-1][\"end\"], \"text\": \" \".join([x[\"word\"] for x in curr])})\n",
        "\n",
        "    smart_blocks = []\n",
        "    idx = 0\n",
        "    while idx < len(phrases):\n",
        "        b_start = max(0.0, phrases[idx][\"start\"] - 0.25)\n",
        "        j = idx\n",
        "        b_end = phrases[j][\"end\"]\n",
        "        while j < len(phrases) - 1:\n",
        "            next_end = phrases[j+1][\"end\"]\n",
        "            cand_dur = next_end - b_start\n",
        "            if cand_dur <= 38.0:\n",
        "                j += 1\n",
        "                b_end = phrases[j][\"end\"]\n",
        "            else:\n",
        "                break\n",
        "        \n",
        "        final_end = min(total_dur, b_end + 0.35)\n",
        "        dur = round(final_end - b_start, 2)\n",
        "        if dur >= 15.0:\n",
        "            smart_blocks.append({\n",
        "                \"start\": round(b_start, 2),\n",
        "                \"duration\": dur,\n",
        "                \"label\": f\"Frase Musical [{b_start:.1f}s - {final_end:.1f}s]\"\n",
        "            })\n",
        "        idx = j + 1\n",
        "\n",
        "    if not smart_blocks:\n",
        "        smart_blocks.append({\"start\": 0.0, \"duration\": min(30.0, total_dur), \"label\": \"Track Intro\"})\n",
        "\n",
        "    result_blocks = []\n",
        "    for k in range(variants_count):\n",
        "        block = smart_blocks[k % len(smart_blocks)]\n",
        "        result_blocks.append(block)\n",
        "        \n",
        "    print(f\"\u2705 [Smart Phrasing] Generados {len(result_blocks)} fragmentos musicales con corte limpio.\", flush=True)\n",
        "    return result_blocks\n",
        "\n",
        "def extract_audio_segment(source_audio, output_segment, start_offset, duration=30):\n",
        "    fade_out_st = max(0.1, duration - 0.55)\n",
        "    audio_filter = f\"afade=t=in:ss=0:d=0.25,afade=t=out:st={fade_out_st:.2f}:d=0.55\"\n",
        "    cmd = [\n",
        "        \"ffmpeg\", \"-y\",\n",
        "        \"-ss\", str(start_offset),\n",
        "        \"-t\", str(duration),\n",
        "        \"-i\", str(source_audio),\n",
        "        \"-af\", audio_filter,\n",
        "        \"-c:a\", \"libmp3lame\", \"-b:a\", \"320k\",\n",
        "        str(output_segment)\n",
        "    ]\n",
        "    subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)\n",
        "    return os.path.exists(output_segment)\n",
        "\n",
        "def format_ass_time(seconds):\n",
        "    h = int(seconds // 3600)\n",
        "    m = int((seconds % 3600) // 60)\n",
        "    s = int(seconds % 60)\n",
        "    cs = int(round((seconds - int(seconds)) * 100))\n",
        "    if cs >= 100: cs = 99\n",
        "    return f\"{h:d}:{m:02d}:{s:02d}.{cs:02d}\"\n",
        "\n",
        "def create_default_gradient_cover(out_img_path, title=\"Wentix Voice\", subtitle=\"@wentixai\"):\n",
        "    try:\n",
        "        import numpy as np\n",
        "        import cv2\n",
        "        img = np.zeros((1920, 1080, 3), dtype=np.uint8)\n",
        "        for y in range(1920):\n",
        "            r = int(4 + (15 - 4) * (y / 1920))\n",
        "            g = int(8 + (25 - 8) * (y / 1920))\n",
        "            b = int(18 + (45 - 18) * (y / 1920))\n",
        "            img[y, :] = (b, g, r)\n",
        "        cv2.circle(img, (540, 750), 320, (60, 40, 15), -1)\n",
        "        cv2.imwrite(str(out_img_path), img)\n",
        "    except Exception:\n",
        "        cmd = [\n",
        "            \"ffmpeg\", \"-y\", \"-f\", \"lavfi\", \"-i\", \"color=c=0x090b14:s=1080x1920:d=1\",\n",
        "            \"-frames:v\", \"1\", str(out_img_path)\n",
        "        ]\n",
        "        subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
        "\n",
        "# Generar subt\u00edtulos ASS para 9:16 (1080x1920)\n",
        "def transcribe_audio_to_ass(audio_path, ass_path, style_idx=0, style_index=None, sub_margin_v=900, language=\"es\", **kwargs):\n",
        "    if style_index is not None:\n",
        "        style_idx = style_index\n",
        "    try:\n",
        "        model = get_whisper()\n",
        "        try:\n",
        "            audio_data = decode_audio_safe(audio_path)\n",
        "            segments, _ = model.transcribe(audio_data, word_timestamps=True, language=language)\n",
        "        except Exception:\n",
        "            segments, _ = model.transcribe(str(audio_path), word_timestamps=True, language=language)\n",
        "            \n",
        "        style = VIRAL_CAPTION_STYLES[style_idx % len(VIRAL_CAPTION_STYLES)]\n",
        "        chunk_size = style.get(\"chunk\", 1)\n",
        "        font_size = style.get(\"size\", 76)\n",
        "        bord_w = style.get(\"bord\", 8)\n",
        "        shad_w = style.get(\"shad\", 4)\n",
        "        \n",
        "        ass_lines = [\n",
        "            \"[Script Info]\",\n",
        "            \"ScriptType: v4.00+\",\n",
        "            \"PlayResX: 1080\",\n",
        "            \"PlayResY: 1920\",\n",
        "            \"\",\n",
        "            \"[V4+ Styles]\",\n",
        "            \"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\",\n",
        "            f\"Style: Default,DejaVu Sans,{font_size},&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,{bord_w},{shad_w},2,40,40,{sub_margin_v},1\",\n",
        "            \"\",\n",
        "            \"[Events]\",\n",
        "            \"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\"\n",
        "        ]\n",
        "        \n",
        "        for segment in segments:\n",
        "            words = list(segment.words)\n",
        "            if not words:\n",
        "                start = format_ass_time(segment.start)\n",
        "                end = format_ass_time(segment.end)\n",
        "                txt = segment.text.strip().upper()\n",
        "                styled_text = f\"{{\\\\b1\\\\fs{font_size}\\\\1c{style['c1']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{txt}\"\n",
        "                ass_lines.append(f\"Dialogue: 0,{start},{end},Default,,0,0,0,,{styled_text}\")\n",
        "                continue\n",
        "                \n",
        "            for i in range(0, len(words), chunk_size):\n",
        "                chunk = words[i:i + chunk_size]\n",
        "                chunk_start = format_ass_time(chunk[0].start)\n",
        "                chunk_end = format_ass_time(chunk[-1].end)\n",
        "                \n",
        "                if style.get(\"bicolor\") and len(chunk) > 1:\n",
        "                    w1 = chunk[0].word.strip().upper()\n",
        "                    w2 = \" \".join([w.word.strip().upper() for w in chunk[1:]])\n",
        "                    styled_text = f\"{{\\\\b1\\\\fs{font_size}\\\\1c{style['c1']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{w1} {{\\\\1c{style['c2']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{w2}\"\n",
        "                else:\n",
        "                    w_text = \" \".join([w.word.strip().upper() for w in chunk])\n",
        "                    styled_text = f\"{{\\\\b1\\\\fs{font_size}\\\\1c{style['c1']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{w_text}\"\n",
        "                \n",
        "                ass_lines.append(f\"Dialogue: 0,{chunk_start},{chunk_end},Default,,0,0,0,,{styled_text}\")\n",
        "                \n",
        "        with open(str(ass_path), \"w\", encoding=\"utf-8\") as f:\n",
        "            f.write(\"\\n\".join(ass_lines))\n",
        "        return True\n",
        "    except Exception as e:\n",
        "        print(f\"\u26a0\ufe0f Error generando subt\u00edtulos ASS: {e}\", flush=True)\n",
        "        return False\n",
        "\n",
        "# Generar subt\u00edtulos ASS para 16:9 Landscape (1920x1080)\n",
        "def transcribe_audio_to_ass_169(audio_path, ass_path, style_key=\"hormozi_1w_yellow\"):\n",
        "    try:\n",
        "        model = get_whisper()\n",
        "        try:\n",
        "            audio_data = decode_audio_safe(audio_path)\n",
        "            segments, _ = model.transcribe(audio_data, word_timestamps=True, language=language)\n",
        "        except Exception:\n",
        "            segments, _ = model.transcribe(str(audio_path), word_timestamps=True, language=language)\n",
        "            \n",
        "        style = next((s for s in VIRAL_CAPTION_STYLES if s[\"id\"] == style_key), VIRAL_CAPTION_STYLES[0])\n",
        "        chunk_size = style.get(\"chunk\", 1)\n",
        "        font_size = 54\n",
        "        bord_w = 8\n",
        "        shad_w = 4\n",
        "        margin_v = 240  # Posici\u00f3n Y \u2248 840px (arriba de la onda horizontal de 940px)\n",
        "        \n",
        "        ass_lines = [\n",
        "            \"[Script Info]\",\n",
        "            \"ScriptType: v4.00+\",\n",
        "            \"PlayResX: 1920\",\n",
        "            \"PlayResY: 1080\",\n",
        "            \"\",\n",
        "            \"[V4+ Styles]\",\n",
        "            \"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\",\n",
        "            f\"Style: Default,DejaVu Sans,{font_size},&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,{bord_w},{shad_w},2,60,60,{margin_v},1\",\n",
        "            \"\",\n",
        "            \"[Events]\",\n",
        "            \"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\"\n",
        "        ]\n",
        "        \n",
        "        for segment in segments:\n",
        "            words = list(segment.words)\n",
        "            if not words:\n",
        "                start = format_ass_time(segment.start)\n",
        "                end = format_ass_time(segment.end)\n",
        "                txt = segment.text.strip().upper()\n",
        "                styled_text = f\"{{\\\\b1\\\\fs{font_size}\\\\1c{style['c1']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{txt}\"\n",
        "                ass_lines.append(f\"Dialogue: 0,{start},{end},Default,,0,0,0,,{styled_text}\")\n",
        "                continue\n",
        "                \n",
        "            for i in range(0, len(words), chunk_size):\n",
        "                chunk = words[i:i + chunk_size]\n",
        "                chunk_start = format_ass_time(chunk[0].start)\n",
        "                chunk_end = format_ass_time(chunk[-1].end)\n",
        "                \n",
        "                if style.get(\"bicolor\") and len(chunk) > 1:\n",
        "                    w1 = chunk[0].word.strip().upper()\n",
        "                    w2 = \" \".join([w.word.strip().upper() for w in chunk[1:]])\n",
        "                    styled_text = f\"{{\\\\b1\\\\fs{font_size}\\\\1c{style['c1']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{w1} {{\\\\1c{style['c2']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{w2}\"\n",
        "                else:\n",
        "                    w_text = \" \".join([w.word.strip().upper() for w in chunk])\n",
        "                    styled_text = f\"{{\\\\b1\\\\fs{font_size}\\\\1c{style['c1']}\\\\3c&H000000&\\\\4c&H000000&\\\\bord{bord_w}\\\\shad{shad_w}}}{w_text}\"\n",
        "                \n",
        "                ass_lines.append(f\"Dialogue: 0,{chunk_start},{chunk_end},Default,,0,0,0,,{styled_text}\")\n",
        "                \n",
        "        with open(str(ass_path), \"w\", encoding=\"utf-8\") as f:\n",
        "            f.write(\"\\n\".join(ass_lines))\n",
        "        return True\n",
        "    except Exception as e:\n",
        "        print(f\"\u26a0\ufe0f Error generando subt\u00edtulos 16:9: {e}\", flush=True)\n",
        "        return False\n",
        "\n",
        "# =========================================================================\n",
        "# \ud83c\udfac MOTOR CINEMA 16:9 FULL HD (MULTI-IMAGEN ORDENADA + FUNDIDO SUAVE + WHISPER)\n",
        "# =========================================================================\n",
        "async def render_cinema_169_full(job, ws, job_dir):\n",
        "    job_id = job.get(\"id\")\n",
        "    title = job.get(\"title\", \"Cinema 16:9\")\n",
        "    crossfade_dur = float(job.get(\"crossfadeDuration\", 1.5))\n",
        "    waveform_key = job.get(\"waveformStyle\", \"auto\")\n",
        "    caption_style_key = job.get(\"captionStyle\", \"auto\")\n",
        "\n",
        "    if waveform_key == \"auto\" or not waveform_key:\n",
        "        waveform_key = \"spotify_horizontal\"\n",
        "    if caption_style_key == \"auto\" or not caption_style_key:\n",
        "        caption_style_key = \"hormozi_1w_yellow\"\n",
        "\n",
        "    print(f\"\\n{'='*65}\", flush=True)\n",
        "    print(f\"\ud83c\udfac [MOTOR CINEMA 16:9] Generando Video Panor\u00e1mico 1920x1080\", flush=True)\n",
        "    print(f\"\ud83d\udccb T\u00edtulo: {title} | Transici\u00f3n: {crossfade_dur}s | Onda: {waveform_key} | Subs: {caption_style_key}\", flush=True)\n",
        "    print(f\"{'='*65}\\n\", flush=True)\n",
        "\n",
        "    await ws.send(json.dumps({\n",
        "        \"type\": \"JOB_PROGRESS\",\n",
        "        \"jobId\": job_id,\n",
        "        \"percent\": 10,\n",
        "        \"stage\": \"SINCRONIZANDO RECURSOS\",\n",
        "        \"message\": \"\ud83d\udce5 Descargando audio y secuencia de im\u00e1genes 16:9...\"\n",
        "    }))\n",
        "\n",
        "    audio_files = []\n",
        "    image_files = []\n",
        "\n",
        "    # 1. Descarga desde Google Drive si viene link\n",
        "    drive_audio = job.get(\"driveAudiosUrl\") or job.get(\"driveAudioUrl\")\n",
        "    drive_images = job.get(\"driveImagesUrl\") or job.get(\"driveCoversUrl\")\n",
        "\n",
        "    if drive_audio and \"drive.google.com\" in str(drive_audio):\n",
        "        a_dir = job_dir / \"gdrive_audio\"\n",
        "        a_dir.mkdir(exist_ok=True)\n",
        "        import gdown\n",
        "        try:\n",
        "            gdown.download_folder(url=drive_audio, output=str(a_dir), quiet=False, use_cookies=False)\n",
        "        except Exception:\n",
        "            gdown.download(url=drive_audio, output=str(a_dir / \"soundtrack.mp3\"), quiet=False, fuzzy=True)\n",
        "        for ext in [\"*.mp3\", \"*.wav\", \"*.ogg\", \"*.flac\", \"*.m4a\"]:\n",
        "            audio_files.extend(list(a_dir.glob(ext)))\n",
        "\n",
        "    if drive_images and \"drive.google.com\" in str(drive_images):\n",
        "        i_dir = job_dir / \"gdrive_images\"\n",
        "        i_dir.mkdir(exist_ok=True)\n",
        "        import gdown\n",
        "        try:\n",
        "            gdown.download_folder(url=drive_images, output=str(i_dir), quiet=False, use_cookies=False)\n",
        "        except Exception:\n",
        "            gdown.download(url=drive_images, output=str(i_dir / \"photo.jpg\"), quiet=False, fuzzy=True)\n",
        "        for ext in [\"*.png\", \"*.jpg\", \"*.jpeg\", \"*.webp\"]:\n",
        "            image_files.extend(list(i_dir.glob(ext)))\n",
        "\n",
        "    # 2. Descarga desde servidor Master si vienen subidas\n",
        "    if not audio_files and job.get(\"audioUrl\"):\n",
        "        local_a = job_dir / \"soundtrack.mp3\"\n",
        "        if download_file_robust(job.get(\"audioUrl\"), local_a):\n",
        "            audio_files.append(local_a)\n",
        "\n",
        "    if not image_files:\n",
        "        raw_imgs = job.get(\"imageUrls\") or job.get(\"coverUrls\") or []\n",
        "        for idx, r_img in enumerate(raw_imgs):\n",
        "            local_i = job_dir / f\"frame_{idx+1:03d}.png\"\n",
        "            if download_file_robust(r_img, local_i):\n",
        "                image_files.append(local_i)\n",
        "\n",
        "    if not audio_files:\n",
        "        raise ValueError(\"No se encontr\u00f3 archivo de audio para el proyecto Cinema 16:9\")\n",
        "    if not image_files:\n",
        "        raise ValueError(\"No se encontraron im\u00e1genes para el proyecto Cinema 16:9\")\n",
        "\n",
        "    # 3. ORDENAMIENTO NATURAL ESTRICTO (01.jpg, 02.png, ..., 10.jpg)\n",
        "    image_files.sort(key=lambda p: natural_sort_key(p.name))\n",
        "    print(f\"\ud83d\uddbc\ufe0f Secuencia de {len(image_files)} im\u00e1genes ordenadas:\", flush=True)\n",
        "    for idx, img_p in enumerate(image_files):\n",
        "        print(f\"   [{idx+1:02d}] {img_p.name}\", flush=True)\n",
        "\n",
        "    soundtrack = audio_files[0]\n",
        "    total_dur = get_media_duration(soundtrack)\n",
        "    if total_dur <= 0: total_dur = 180.0\n",
        "    print(f\"\ud83c\udfb5 Duraci\u00f3n Total Soundtrack: {total_dur:.2f}s\", flush=True)\n",
        "\n",
        "    # 4. Transcripci\u00f3n y Subt\u00edtulos 16:9 con Faster-Whisper\n",
        "    ass_path = None\n",
        "    if caption_style_key != \"none\":\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_PROGRESS\",\n",
        "            \"jobId\": job_id,\n",
        "            \"percent\": 25,\n",
        "            \"stage\": \"WHISPER SUBTITLES IA\",\n",
        "            \"message\": \"\ud83c\udf99\ufe0f Transcribiendo audio completo y generando subt\u00edtulos 16:9...\"\n",
        "        }))\n",
        "        ass_path = job_dir / \"cinema_169_subs.ass\"\n",
        "        transcribe_audio_to_ass_169(soundtrack, ass_path, style_key=caption_style_key)\n",
        "\n",
        "    # 5. CONSTRUIR FILTRO FFMPEG CROSSFADE MULTI-IMAGEN\n",
        "    await ws.send(json.dumps({\n",
        "        \"type\": \"JOB_PROGRESS\",\n",
        "        \"jobId\": job_id,\n",
        "        \"percent\": 45,\n",
        "        \"stage\": \"FFMPEG GPU RENDER\",\n",
        "        \"message\": \"\ud83c\udfac Renderizando secuencia multi-imagen con fundido cinem\u00e1tico y onda panor\u00e1mica...\"\n",
        "    }))\n",
        "\n",
        "    N = len(image_files)\n",
        "    time_per_image = total_dur / N\n",
        "\n",
        "    # Ajustar duraci\u00f3n de crossfade si el tiempo por imagen es corto\n",
        "    actual_xfade = min(crossfade_dur, max(0.2, time_per_image * 0.4))\n",
        "    if actual_xfade <= 0.05 or N == 1:\n",
        "        actual_xfade = 0.0\n",
        "\n",
        "    # Armar comando FFmpeg con entradas de video\n",
        "    inputs = []\n",
        "    filter_complex_parts = []\n",
        "\n",
        "    # Cada imagen como input\n",
        "    for i, img_path in enumerate(image_files):\n",
        "        inputs.extend([\"-loop\", \"1\", \"-t\", f\"{total_dur + 5.0:.2f}\", \"-i\", str(img_path)])\n",
        "        # Normalizar a 1920x1080 30fps\n",
        "        filter_complex_parts.append(\n",
        "            f\"[{i}:v]scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,setsar=1,fps=30,format=yuv420p[v{i}]\"\n",
        "        )\n",
        "\n",
        "    # Audio input al final de los inputs\n",
        "    audio_input_idx = N\n",
        "    inputs.extend([\"-i\", str(soundtrack)])\n",
        "\n",
        "    # Cadena de Crossfades\n",
        "    if N == 1 or actual_xfade <= 0:\n",
        "        filter_complex_parts.append(f\"[v0]null[slideshow]\")\n",
        "    else:\n",
        "        last_v = \"v0\"\n",
        "        for k in range(N - 1):\n",
        "            offset = (k + 1) * time_per_image - actual_xfade\n",
        "            out_v = f\"xf{k+1}\" if (k < N - 2) else \"slideshow\"\n",
        "            filter_complex_parts.append(\n",
        "                f\"[{last_v}][v{k+1}]xfade=transition=fade:duration={actual_xfade:.2f}:offset={offset:.2f}[{out_v}]\"\n",
        "            )\n",
        "            last_v = out_v\n",
        "\n",
        "    # Onda Panor\u00e1mica 16:9\n",
        "    wave_cfg = PANORAMIC_WAVEFORMS_169.get(waveform_key, PANORAMIC_WAVEFORMS_169[\"spotify_horizontal\"])\n",
        "    if waveform_key != \"none\":\n",
        "        filter_complex_parts.append(f\"[{audio_input_idx}:a]{wave_cfg['filter']}\")\n",
        "        filter_complex_parts.append(f\"[slideshow][wave]overlay=(W-w)/2:{wave_cfg['y']}[v_with_wave]\")\n",
        "        final_video_label = \"v_with_wave\"\n",
        "    else:\n",
        "        final_video_label = \"slideshow\"\n",
        "\n",
        "    # Burn Subt\u00edtulos ASS 16:9\n",
        "    if ass_path and os.path.exists(str(ass_path)):\n",
        "        escaped_ass = str(ass_path).replace(\":\", \"\\\\:\").replace(\"\\\\\", \"/\")\n",
        "        filter_complex_parts.append(f\"[{final_video_label}]ass='{escaped_ass}'[final_v]\")\n",
        "        map_out_v = \"[final_v]\"\n",
        "    else:\n",
        "        map_out_v = f\"[{final_video_label}]\"\n",
        "\n",
        "    final_filter_str = \"; \".join(filter_complex_parts)\n",
        "    out_video = job_dir / f\"cinema_169_{title.replace(' ', '_')}.mp4\"\n",
        "\n",
        "    cmd = [\n",
        "        \"ffmpeg\", \"-y\"\n",
        "    ] + inputs + [\n",
        "        \"-filter_complex\", final_filter_str,\n",
        "        \"-map\", map_out_v,\n",
        "        \"-map\", f\"{audio_input_idx}:a\",\n",
        "        \"-c:v\", \"libx264\", \"-preset\", \"veryfast\", \"-crf\", \"18\", \"-profile:v\", \"high\", \"-pix_fmt\", \"yuv420p\",\n",
        "        \"-c:a\", \"aac\", \"-b:a\", \"320k\", \"-ar\", \"48000\",\n",
        "        \"-t\", f\"{total_dur:.2f}\",\n",
        "        \"-movflags\", \"+faststart\",\n",
        "        str(out_video)\n",
        "    ]\n",
        "\n",
        "    print(f\"\ud83d\ude80 Ejecutando Renderizado Cinem\u00e1tico FFmpeg...\", flush=True)\n",
        "    res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
        "    if res.returncode != 0:\n",
        "        print(f\"\u274c FFmpeg Error: {res.stderr.decode('utf-8', errors='ignore')}\", flush=True)\n",
        "        raise RuntimeError(f\"Error en renderizado FFmpeg Cinema 16:9\")\n",
        "\n",
        "    # 6. Subir Video Final al Master Server\n",
        "    await ws.send(json.dumps({\n",
        "        \"type\": \"JOB_PROGRESS\",\n",
        "        \"jobId\": job_id,\n",
        "        \"percent\": 88,\n",
        "        \"stage\": \"SUBIENDO AL MASTER\",\n",
        "        \"message\": \"\u2b06\ufe0f Subiendo Video Cinema 16:9 Full HD al servidor...\"\n",
        "    }))\n",
        "\n",
        "    print(f\"\u2b06\ufe0f Subiendo video final 16:9 ({out_video.name}) al Master...\", flush=True)\n",
        "    up_ok = upload_single_video_robust(job_id, out_video, is_final=True, max_retries=4)\n",
        "    if not up_ok:\n",
        "        print(\"\u26a0\ufe0f Advertencia: Fall\u00f3 subida directa tras reintentos.\", flush=True)\n",
        "\n",
        "    await ws.send(json.dumps({\n",
        "        \"type\": \"JOB_COMPLETED\",\n",
        "        \"jobId\": job_id,\n",
        "        \"result\": {\n",
        "            \"success\": True,\n",
        "            \"videosCount\": 1,\n",
        "            \"aspectRatio\": \"16:9\",\n",
        "            \"message\": f\"\ud83c\udf89 \u00a1Video Cinema 16:9 Full HD completado y subido con \u00e9xito!\"\n",
        "        }\n",
        "    }))\n",
        "    print(f\"\ud83c\udf89 \u00a1PROYECTO CINEMA 16:9 {job_id} COMPLETADO AL 100%!\\n\", flush=True)\n",
        "\n",
        "\n",
        "# =========================================================================\n",
        "# \ud83d\udcf1 MOTOR 9:16 VERTICAL BATCH (FRASES INTELIGENTES + SUBT\u00cdTULOS CINTURA)\n",
        "# =========================================================================\n",
        "\n",
        "# =========================================================================\n",
        "# \ud83c\udf0c MOTOR 3D PARALLAX (DEPTH-ANYTHING-V2) + AUDIO-REACTIVE BASS PULSE\n",
        "# =========================================================================\n",
        "GLOBAL_DEPTH_PIPE = None\n",
        "\n",
        "def get_depth_estimator():\n",
        "    global GLOBAL_DEPTH_PIPE\n",
        "    if GLOBAL_DEPTH_PIPE is not None:\n",
        "        return GLOBAL_DEPTH_PIPE\n",
        "    try:\n",
        "        from transformers import pipeline\n",
        "        import torch\n",
        "        dev = 0 if torch.cuda.is_available() else -1\n",
        "        print(\"\u26a1 [Depth-Anything-V2] Inicializando modelo 3D en GPU...\", flush=True)\n",
        "        GLOBAL_DEPTH_PIPE = pipeline(task=\"depth-estimation\", model=\"depth-anything/Depth-Anything-V2-Small-hf\", device=dev)\n",
        "        print(\"\u2705 [Depth-Anything-V2] Motor 3D Parallax listo.\", flush=True)\n",
        "    except Exception as e:\n",
        "        print(f\"\u2139\ufe0f [Depth-Anything-V2] No disponible ({e}). Operando en modo Ken Burns Cinem\u00e1tico.\", flush=True)\n",
        "        GLOBAL_DEPTH_PIPE = False\n",
        "    return GLOBAL_DEPTH_PIPE\n",
        "\n",
        "def extract_bass_energy_envelope(audio_path, duration, fps=30):\n",
        "    try:\n",
        "        import scipy.io.wavfile as wavfile\n",
        "        import scipy.signal as signal\n",
        "        import numpy as np\n",
        "        \n",
        "        # Convert audio to wav in memory or temp if needed\n",
        "        tmp_wav = audio_path.parent / f\"tmp_pulse_{os.getpid()}.wav\"\n",
        "        subprocess.run([\"ffmpeg\", \"-y\", \"-i\", str(audio_path), \"-ar\", \"22050\", \"-ac\", \"1\", \"-t\", str(duration), str(tmp_wav)], capture_output=True)\n",
        "        if not tmp_wav.exists(): return None\n",
        "        \n",
        "        sr, data = wavfile.read(str(tmp_wav))\n",
        "        try: os.remove(str(tmp_wav))\n",
        "        except Exception: pass\n",
        "        \n",
        "        if data.ndim > 1: data = data[:, 0]\n",
        "        data = data.astype(np.float32) / (np.max(np.abs(data)) + 1e-6)\n",
        "        \n",
        "        # Lowpass filter for Sub-Bass (< 130 Hz)\n",
        "        sos = signal.butter(4, 130, 'lowpass', fs=sr, output='sos')\n",
        "        bass = signal.sosfilt(sos, data)\n",
        "        \n",
        "        # Frame-by-frame energy\n",
        "        hop = int(sr / fps)\n",
        "        num_frames = int(duration * fps)\n",
        "        envelope = []\n",
        "        for f in range(num_frames):\n",
        "            start = f * hop\n",
        "            end = min(start + hop, len(bass))\n",
        "            if start < len(bass):\n",
        "                e = np.sqrt(np.mean(bass[start:end]**2))\n",
        "            else:\n",
        "                e = 0.0\n",
        "            envelope.append(e)\n",
        "            \n",
        "        envelope = np.array(envelope, dtype=np.float32)\n",
        "        max_e = np.max(envelope) if np.max(envelope) > 0 else 1.0\n",
        "        return envelope / max_e\n",
        "    except Exception as e:\n",
        "        return None\n",
        "\n",
        "def generate_3d_parallax_shot(img_path, duration, out_video_path, fps=30, shot_idx=0, bass_env=None):\n",
        "    try:\n",
        "        import cv2\n",
        "        import numpy as np\n",
        "        from PIL import Image\n",
        "        \n",
        "        pipe = get_depth_estimator()\n",
        "        if not pipe:\n",
        "            return False\n",
        "            \n",
        "        pil_img = Image.open(str(img_path)).convert(\"RGB\")\n",
        "        target_w, target_h = 1080, 1920\n",
        "        pil_img = pil_img.resize((target_w, target_h), Image.Resampling.LANCZOS)\n",
        "        img_np = np.array(pil_img)[:, :, ::-1] # RGB to BGR\n",
        "        \n",
        "        # Compute depth map (0.0=background, 1.0=foreground artist)\n",
        "        depth_out = pipe(pil_img)\n",
        "        depth_map = np.array(depth_out[\"depth\"]).astype(np.float32)\n",
        "        depth_map = cv2.resize(depth_map, (target_w, target_h), interpolation=cv2.INTER_LINEAR)\n",
        "        d_min, d_max = depth_map.min(), depth_map.max()\n",
        "        depth_map = (depth_map - d_min) / (d_max - d_min + 1e-6)\n",
        "        depth_map = cv2.GaussianBlur(depth_map, (25, 25), 0)\n",
        "        \n",
        "        num_frames = int(duration * fps)\n",
        "        grid_y, grid_x = np.indices((target_h, target_w), dtype=np.float32)\n",
        "        \n",
        "        fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n",
        "        writer = cv2.VideoWriter(str(out_video_path), fourcc, fps, (target_w, target_h))\n",
        "        \n",
        "        max_shift = 28.0 # Max parallax amplitude\n",
        "        \n",
        "        for f in range(num_frames):\n",
        "            prog = f / num_frames\n",
        "            \n",
        "            # Bass punch modulation\n",
        "            pulse = 1.0\n",
        "            if bass_env is not None and f < len(bass_env):\n",
        "                pulse += bass_env[f] * 0.45\n",
        "                \n",
        "            # Camera trajectory patterns\n",
        "            if shot_idx % 3 == 0:\n",
        "                cam_x = np.sin(prog * np.pi) * max_shift * pulse\n",
        "                cam_y = 0.0\n",
        "            elif shot_idx % 3 == 1:\n",
        "                cam_x = -np.sin(prog * np.pi) * max_shift * pulse\n",
        "                cam_y = np.cos(prog * np.pi) * (max_shift * 0.4)\n",
        "            else:\n",
        "                cam_x = np.cos(prog * 2 * np.pi) * (max_shift * 0.6) * pulse\n",
        "                cam_y = np.sin(prog * 2 * np.pi) * (max_shift * 0.4)\n",
        "                \n",
        "            map_x = (grid_x + (depth_map - 0.5) * cam_x).astype(np.float32)\n",
        "            map_y = (grid_y + (depth_map - 0.5) * cam_y).astype(np.float32)\n",
        "            \n",
        "            frame = cv2.remap(img_np, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)\n",
        "            writer.write(frame)\n",
        "            \n",
        "        writer.release()\n",
        "        return out_video_path.exists() and out_video_path.stat().st_size > 0\n",
        "    except Exception as e:\n",
        "        print(f\"\u26a0\ufe0f Parallax shot error: {e}\", flush=True)\n",
        "        return False\n",
        "\n",
        "\n",
        "def render_single_video_variant(audio_path, cover_path, out_video_path, ass_path, duration=30, variant_idx=0, dynamic_wave_y=None):\n",
        "    try:\n",
        "        wave = MODERN_WAVEFORMS_916[variant_idx % len(MODERN_WAVEFORMS_916)]\n",
        "        final_wave_y = dynamic_wave_y if dynamic_wave_y is not None else wave.get(\"base_y\", 1420)\n",
        "        \n",
        "        covers = cover_path if isinstance(cover_path, list) else [cover_path]\n",
        "        num_covers = len(covers)\n",
        "        dur = float(duration) if duration and float(duration) > 0 else 30.0\n",
        "\n",
        "        zoom_presets = [\n",
        "            \"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920:(in_w-1080)/2:(in_h-1920)/2,zoompan=z='min(1.0+0.00008*in,1.06)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s=1080x1920:fps=30\",\n",
        "            \"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920:(in_w-1080)/2:(in_h-1920)/2,zoompan=z='max(1.06-0.00008*in,1.00)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s=1080x1920:fps=30\",\n",
        "            \"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920:(in_w-1080)/2:(in_h-1920)/2,zoompan=z=1.04:x='(iw-iw/zoom)*(0.5+0.5*sin(2*3.14159*in/180))':y='ih/2-(ih/zoom/2)':d=1:s=1080x1920:fps=30\",\n",
        "            \"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920:(in_w-1080)/2:(in_h-1920)/2,zoompan=z='1.02+0.02*sin(2*3.14159*in/240)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s=1080x1920:fps=30\"\n",
        "        ]\n",
        "\n",
        "        # Check if 3D Parallax generation is possible\n",
        "        use_3d = False\n",
        "        parallax_shots = []\n",
        "        if num_covers > 1:\n",
        "            shot_dur = dur / num_covers\n",
        "            bass_env = extract_bass_energy_envelope(audio_path, dur, fps=30)\n",
        "            all_ok = True\n",
        "            for k, c_file in enumerate(covers):\n",
        "                p_shot = out_video_path.parent / f\"p3d_{variant_idx}_{k}_{out_video_path.stem}.mp4\"\n",
        "                if generate_3d_parallax_shot(c_file, shot_dur, p_shot, fps=30, shot_idx=k, bass_env=bass_env):\n",
        "                    parallax_shots.append(p_shot)\n",
        "                else:\n",
        "                    all_ok = False\n",
        "                    break\n",
        "            if all_ok and len(parallax_shots) == num_covers:\n",
        "                use_3d = True\n",
        "\n",
        "        if use_3d:\n",
        "            # Concatenate 3D parallax MP4 clips\n",
        "            cmd_inputs = []\n",
        "            concat_tags = []\n",
        "            for k, p_shot in enumerate(parallax_shots):\n",
        "                cmd_inputs.extend([\"-i\", str(p_shot)])\n",
        "                concat_tags.append(f\"[{k}:v]\")\n",
        "            concat_expr = \"\".join(concat_tags) + f\"concat=n={num_covers}:v=1:a=0[bg]\"\n",
        "            filter_complex = concat_expr + f\"; {wave['filter']}; [bg][wave]overlay=(W-w)/2:{final_wave_y}[v_wave]\"\n",
        "            audio_stream_idx = num_covers\n",
        "        elif num_covers <= 1:\n",
        "            zm = zoom_presets[variant_idx % len(zoom_presets)]\n",
        "            filter_complex = f\"[0:v]{zm}[bg]; {wave['filter']}; [bg][wave]overlay=(W-w)/2:{final_wave_y}[v_wave]\"\n",
        "            cmd_inputs = [\"-loop\", \"1\", \"-i\", str(covers[0])]\n",
        "            audio_stream_idx = 1\n",
        "        else:\n",
        "            shot_dur = dur / num_covers\n",
        "            cmd_inputs = []\n",
        "            filter_parts = []\n",
        "            concat_tags = []\n",
        "            for k, c_file in enumerate(covers):\n",
        "                cmd_inputs.extend([\"-loop\", \"1\", \"-t\", f\"{shot_dur:.2f}\", \"-i\", str(c_file)])\n",
        "                zm = zoom_presets[(variant_idx + k) % len(zoom_presets)]\n",
        "                filter_parts.append(f\"[{k}:v]{zm},trim=duration={shot_dur:.2f}[v{k}]\")\n",
        "                concat_tags.append(f\"[v{k}]\")\n",
        "            concat_expr = \"\".join(concat_tags) + f\"concat=n={num_covers}:v=1:a=0[bg]\"\n",
        "            filter_complex = \"; \".join(filter_parts) + \"; \" + concat_expr + f\"; {wave['filter']}; [bg][wave]overlay=(W-w)/2:{final_wave_y}[v_wave]\"\n",
        "            audio_stream_idx = num_covers\n",
        "\n",
        "        if ass_path and os.path.exists(str(ass_path)):\n",
        "            escaped_ass = str(ass_path).replace(\":\", \"\\:\").replace(\"\\\", \"/\")\n",
        "            filter_complex += f\"; [v_wave]ass='{escaped_ass}'[v_out]\"\n",
        "            map_v = \"[v_out]\"\n",
        "        else:\n",
        "            map_v = \"[v_wave]\"\n",
        "            \n",
        "        dur_arg = [\"-t\", str(dur)]\n",
        "        encoder_args = [\"-c:v\", \"h264_nvenc\", \"-preset\", \"p4\", \"-cq\", \"20\"]\n",
        "        try:\n",
        "            check_nvenc = subprocess.run([\"ffmpeg\", \"-encoders\"], capture_output=True, text=True)\n",
        "            if \"h264_nvenc\" not in check_nvenc.stdout:\n",
        "                encoder_args = [\"-c:v\", \"libx264\", \"-preset\", \"veryfast\", \"-crf\", \"18\"]\n",
        "        except Exception:\n",
        "            encoder_args = [\"-c:v\", \"libx264\", \"-preset\", \"veryfast\", \"-crf\", \"18\"]\n",
        "\n",
        "        cmd = [\n",
        "            \"ffmpeg\", \"-y\",\n",
        "            *cmd_inputs,\n",
        "            \"-i\", str(audio_path),\n",
        "            \"-filter_complex\", filter_complex,\n",
        "            \"-map\", map_v,\n",
        "            \"-map\", f\"{audio_stream_idx}:a\",\n",
        "            *encoder_args,\n",
        "            \"-profile:v\", \"high\", \"-pix_fmt\", \"yuv420p\", \"-movflags\", \"+faststart\",\n",
        "            \"-c:a\", \"aac\", \"-b:a\", \"320k\", \"-ar\", \"48000\", \"-shortest\"\n",
        "        ] + dur_arg + [str(out_video_path)]\n",
        "        \n",
        "        res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
        "        \n",
        "        # Cleanup temporary 3D shots\n",
        "        if use_3d:\n",
        "            for p_shot in parallax_shots:\n",
        "                try: os.remove(str(p_shot))\n",
        "                except Exception: pass\n",
        "\n",
        "        return res.returncode == 0 and os.path.exists(str(out_video_path))\n",
        "    except Exception as e:\n",
        "        print(f\"\u26a0\ufe0f Error renderizando video variant 9:16: {e}\", flush=True)\n",
        "        return False\n",
        "\n",
        "async def render_voxforja_916_batch(job, ws, job_dir):\n",
        "    job_id = job.get(\"id\")\n",
        "    title = job.get(\"title\", \"Lote 9:16\")\n",
        "    raw_dur = job.get(\"duration\", \"auto\")\n",
        "    variants_count = int(job.get(\"variantsCount\", 10))\n",
        "\n",
        "    audio_files = []\n",
        "    image_files = []\n",
        "\n",
        "    # 1. Drive Sync\n",
        "    drive_audio = job.get(\"driveAudiosUrl\") or job.get(\"driveAudioUrl\")\n",
        "    drive_images = job.get(\"driveCoversUrl\") or job.get(\"driveImagesUrl\")\n",
        "\n",
        "    if drive_audio and \"drive.google.com\" in str(drive_audio):\n",
        "        a_dir = job_dir / \"gdrive_audio\"\n",
        "        a_dir.mkdir(exist_ok=True)\n",
        "        import gdown\n",
        "        try:\n",
        "            gdown.download_folder(url=drive_audio, output=str(a_dir), quiet=False, use_cookies=False)\n",
        "        except Exception:\n",
        "            gdown.download(url=drive_audio, output=str(a_dir / \"audio_source.mp3\"), quiet=False, fuzzy=True)\n",
        "        for ext in [\"*.mp3\", \"*.wav\", \"*.ogg\", \"*.flac\", \"*.m4a\"]:\n",
        "            audio_files.extend(list(a_dir.glob(ext)))\n",
        "\n",
        "    if drive_images and \"drive.google.com\" in str(drive_images):\n",
        "        img_dir = job_dir / \"gdrive_images\"\n",
        "        img_dir.mkdir(exist_ok=True)\n",
        "        import gdown\n",
        "        try:\n",
        "            gdown.download_folder(url=drive_images, output=str(img_dir), quiet=False, use_cookies=False)\n",
        "        except Exception:\n",
        "            gdown.download(url=drive_images, output=str(img_dir / \"cover_source.png\"), quiet=False, fuzzy=True)\n",
        "        for ext in [\"*.png\", \"*.jpg\", \"*.jpeg\", \"*.webp\"]:\n",
        "            image_files.extend(list(img_dir.glob(ext)))\n",
        "\n",
        "    # 2. Master Server Uploads\n",
        "    if not audio_files:\n",
        "        raw_audios = []\n",
        "        if job.get(\"audioUrl\"): raw_audios.append(job.get(\"audioUrl\"))\n",
        "        if job.get(\"audioUrls\") and isinstance(job.get(\"audioUrls\"), list): raw_audios.extend(job.get(\"audioUrls\"))\n",
        "        for idx, r_aud in enumerate(raw_audios):\n",
        "            local_a = job_dir / f\"master_audio_{idx+1}.wav\"\n",
        "            if download_file_robust(r_aud, local_a):\n",
        "                audio_files.append(local_a)\n",
        "\n",
        "    if not image_files:\n",
        "        raw_covers = []\n",
        "        if job.get(\"coverUrl\"): raw_covers.append(job.get(\"coverUrl\"))\n",
        "        if job.get(\"coverUrls\") and isinstance(job.get(\"coverUrls\"), list): raw_covers.extend(job.get(\"coverUrls\"))\n",
        "        for idx, r_cov in enumerate(raw_covers):\n",
        "            local_c = job_dir / f\"master_cover_{idx+1}.png\"\n",
        "            if download_file_robust(r_cov, local_c):\n",
        "                image_files.append(local_c)\n",
        "\n",
        "    if not audio_files or not image_files:\n",
        "        raise ValueError(f\"No se encontraron archivos v\u00e1lidos (Audios: {len(audio_files)}, Fotos: {len(image_files)})\")\n",
        "\n",
        "    phrase_blocks = extract_smart_phrase_segments(audio_files[0], variants_count=variants_count, requested_duration=raw_dur)\n",
        "\n",
        "    produced_videos = []\n",
        "    step_pct = 80.0 / variants_count\n",
        "\n",
        "    for i in range(variants_count):\n",
        "        if job_id in CURRENT_CANCELLED_JOBS:\n",
        "            print(f\"\ud83d\uded1 Trabajo {job_id} cancelado.\", flush=True)\n",
        "            return\n",
        "\n",
        "        if len(image_files) > 1:\n",
        "            num_shots = min(len(image_files), 5)\n",
        "            curr_covers = [image_files[(i * 2 + k) % len(image_files)] for k in range(num_shots)]\n",
        "            ref_cover = curr_covers[0]\n",
        "        else:\n",
        "            curr_covers = [image_files[0]]\n",
        "            ref_cover = image_files[0]\n",
        "\n",
        "        curr_audio = audio_files[i % len(audio_files)]\n",
        "        block = phrase_blocks[i % len(phrase_blocks)]\n",
        "        \n",
        "        layout_info = analyze_smart_layout(ref_cover)\n",
        "        dynamic_wave_y = layout_info.get(\"wave_y\", 1420)\n",
        "        dynamic_sub_margin_v = layout_info.get(\"sub_margin_v\", 900)\n",
        "        \n",
        "        style_info = VIRAL_CAPTION_STYLES[i % len(VIRAL_CAPTION_STYLES)]\n",
        "        wave_info = MODERN_WAVEFORMS_916[i % len(MODERN_WAVEFORMS_916)]\n",
        "        \n",
        "        print(f\"\\n\ud83c\udfac [{i+1}/{variants_count}] Renderizando: {block['label']} | Subt\u00edtulos CINTURA '{style_info['name']}' + Onda '{wave_info['name']}'...\", flush=True)\n",
        "\n",
        "        seg_audio = job_dir / f\"seg_{i+1}.mp3\"\n",
        "        extract_audio_segment(curr_audio, seg_audio, block[\"start\"], block[\"duration\"])\n",
        "\n",
        "        seg_ass = job_dir / f\"sub_{i+1}.ass\"\n",
        "        transcribe_audio_to_ass(seg_audio, seg_ass, style_idx=i, sub_margin_v=dynamic_sub_margin_v)\n",
        "\n",
        "        out_vid = job_dir / f\"variant_{i+1}_{title.replace(' ', '_')}.mp4\"\n",
        "        success = render_single_video_variant(\n",
        "            audio_path=seg_audio,\n",
        "            cover_path=curr_covers,\n",
        "            out_video_path=out_vid,\n",
        "            ass_path=seg_ass,\n",
        "            duration=block[\"duration\"],\n",
        "            variant_idx=i,\n",
        "            dynamic_wave_y=dynamic_wave_y\n",
        "        )\n",
        "\n",
        "        if success and out_vid.exists() and out_vid.stat().st_size > 0:\n",
        "            is_last = (i == variants_count - 1)\n",
        "            print(f\"  \u2b06\ufe0f Subiendo variante [{i+1}/{variants_count}] al servidor...\", flush=True)\n",
        "            up_ok = upload_single_video_robust(job_id, out_vid, is_final=is_last, max_retries=4)\n",
        "            if up_ok:\n",
        "                print(f\"  \u2705 [Variante {i+1}/{variants_count}] Subida completada ({out_vid.name})\", flush=True)\n",
        "                produced_videos.append(str(out_vid))\n",
        "            else:\n",
        "                print(f\"  \u26a0\ufe0f [Variante {i+1}/{variants_count}] Fall\u00f3 la subida.\", flush=True)\n",
        "                produced_videos.append(str(out_vid))\n",
        "        else:\n",
        "            print(f\"  \u274c [Variante {i+1}/{variants_count}] Fall\u00f3 el renderizado FFmpeg.\", flush=True)\n",
        "\n",
        "        curr_progress = int(10 + (i + 1) * step_pct)\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_PROGRESS\",\n",
        "            \"jobId\": job_id,\n",
        "            \"progress\": min(98, curr_progress),\n",
        "            \"percent\": min(98, curr_progress),\n",
        "            \"message\": f\"\ud83c\udfac Variante {i+1}/{variants_count} completada ({block['duration']}s)\"\n",
        "        }))\n",
        "\n",
        "    await ws.send(json.dumps({\n",
        "        \"type\": \"JOB_COMPLETED\",\n",
        "        \"jobId\": job_id,\n",
        "        \"result\": {\n",
        "            \"success\": True,\n",
        "            \"videosCount\": len(produced_videos),\n",
        "            \"message\": f\"\ud83c\udf89 \u00a1Completadas {len(produced_videos)} variantes con Subt\u00edtulos en Cintura!\"\n",
        "        }\n",
        "    }))\n",
        "    print(f\"\\n\ud83c\udf89 \u00a1TRABAJO 9:16 {job_id} COMPLETADO! Subidas {len(produced_videos)} variantes.\\n\", flush=True)\n",
        "\n",
        "def normalize_speech_language(value):\n",
        "    if not isinstance(value, str) or not value.strip():\n",
        "        raise ValueError(\"Selecciona el idioma de la locuci\u00f3n; no se detecta al azar.\")\n",
        "    language = value.strip().lower().replace(\"_\", \"-\")\n",
        "    language = \"zh-cn\" if language.split(\"-\")[0] == \"zh\" else language.split(\"-\")[0]\n",
        "    supported = {\"es\", \"en\", \"pt\", \"fr\", \"it\", \"de\", \"pl\", \"tr\", \"ru\", \"nl\", \"cs\", \"ar\", \"zh-cn\", \"ja\", \"hu\", \"ko\", \"hi\"}\n",
        "    if language not in supported:\n",
        "        raise ValueError(f\"Idioma no compatible con XTTS-v2: {value}\")\n",
        "    return language\n",
        "\n",
        "def confirm_speech_upload(response, job_id):\n",
        "    response.raise_for_status()\n",
        "    receipt = response.json()\n",
        "    if not isinstance(receipt, dict) or receipt.get(\"ok\") is not True:\n",
        "        raise RuntimeError(\"El servidor rechaz\u00f3 la subida del audio.\")\n",
        "    audio_url = receipt.get(\"audioUrl\")\n",
        "    if not audio_url:\n",
        "        # Servidores anteriores guardan el audio pero no incluyen su URL en el recibo.\n",
        "        state_response = requests.get(\n",
        "            f\"{MASTER_HTTP_URL}/api/jobs/{job_id}\", timeout=(10, 30), verify=False)\n",
        "        state_response.raise_for_status()\n",
        "        state = state_response.json()\n",
        "        saved_job = state.get(\"job\", {}) if isinstance(state, dict) else {}\n",
        "        if isinstance(state, dict) and state.get(\"ok\") is True and isinstance(saved_job, dict) and saved_job.get(\"id\") == job_id:\n",
        "            audio_url = (saved_job.get(\"results\") or {}).get(\"audioUrl\")\n",
        "    if not isinstance(audio_url, str) or not audio_url.strip():\n",
        "        raise RuntimeError(\"La subida respondi\u00f3 correctamente, pero no se encontr\u00f3 el audio guardado para esta tarea.\")\n",
        "    return audio_url\n",
        "\n",
        "async def render_speech_generation(job, ws, job_dir):\n",
        "    job_id = job.get(\"id\")\n",
        "    title = job.get(\"title\", \"Locuci\u00f3n IA\")\n",
        "    script = job.get(\"script\", \"\")\n",
        "    language = normalize_speech_language(job.get(\"language\"))\n",
        "    artist_name = job.get(\"artistName\", \"Relator\")\n",
        "    voice_sample_url = job.get(\"voiceSampleUrl\")\n",
        "    if not isinstance(script, str) or not script.strip():\n",
        "        raise ValueError(\"El guion de la locuci\u00f3n est\u00e1 vac\u00edo.\")\n",
        "    if not isinstance(voice_sample_url, str) or not voice_sample_url.strip():\n",
        "        raise ValueError(\"Selecciona una muestra de voz para clonar; no se sustituir\u00e1 por otra voz.\")\n",
        "    voice_sample_url = voice_sample_url.strip()\n",
        "    auto_video = job.get(\"autoVideo\", False)\n",
        "    social_handle = job.get(\"socialHandle\", \"@wentixai\")\n",
        "    cta_text = job.get(\"ctaText\", \"\")\n",
        "\n",
        "    print(f\"\\n\ud83c\udf99\ufe0f [VOICE STUDIO] Iniciando s\u00edntesis de voz en [{language.upper()}] para tarea {job_id}...\", flush=True)\n",
        "    print(f\"   \ud83d\udc64 Voz / Relator: {artist_name}\", flush=True)\n",
        "    print(f\"   \ud83c\udf10 Idioma Seleccionado: {language.upper()}\", flush=True)\n",
        "    print(f\"   \ud83d\udcdd Guion ({len(script)} caracteres): \\\"{script[:80]}...\\\"\", flush=True)\n",
        "\n",
        "    await ws.send(json.dumps({\n",
        "        \"type\": \"JOB_PROGRESS\",\n",
        "        \"jobId\": job_id,\n",
        "        \"percent\": 15,\n",
        "        \"stage\": \"S\u00cdNTESIS VOCAL\",\n",
        "        \"message\": f\"\ud83c\udf99\ufe0f Preparando clonaci\u00f3n y s\u00edntesis en {language.upper()} para {artist_name}...\"\n",
        "    }))\n",
        "\n",
        "    out_audio_path = job_dir / f\"speech_raw_{job_id[:8]}.wav\"\n",
        "    out_mp3_path = job_dir / f\"speech_{job_id[:8]}.mp3\"\n",
        "    cloned_success = False\n",
        "\n",
        "    # 1. INTENTO DE CLONACI\u00d3N ZERO-SHOT (XTTS-v2 / F5-TTS) SI HAY MUESTRA VOCAL\n",
        "    if voice_sample_url:\n",
        "        sample_path = job_dir / \"voice_reference.mp3\"\n",
        "        sample_wav = job_dir / \"voice_reference_clean.wav\"\n",
        "        try:\n",
        "            full_sample_url = normalize_asset_url(voice_sample_url)\n",
        "            print(f\"\ud83d\udce5 Descargando muestra de referencia vocal desde {full_sample_url}...\", flush=True)\n",
        "            if not download_file_robust(full_sample_url, sample_path):\n",
        "                raise RuntimeError(\"No se pudo descargar la muestra de voz seleccionada.\")\n",
        "            if sample_path.exists():\n",
        "                # Normalizar audio a WAV mono 24000Hz PCM 16-bit (est\u00e1ndar \u00f3ptimo de XTTS-v2)\n",
        "                subprocess.run([\n",
        "                    \"ffmpeg\", \"-y\", \"-i\", str(sample_path),\n",
        "                    \"-ar\", \"24000\", \"-ac\", \"1\", \"-c:a\", \"pcm_s16le\", str(sample_wav)\n",
        "                ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)\n",
        "\n",
        "                if not sample_wav.exists() or sample_wav.stat().st_size <= 1000:\n",
        "                    raise RuntimeError(\"La muestra de voz no contiene audio v\u00e1lido.\")\n",
        "                if sample_wav.exists() and sample_wav.stat().st_size > 1000:\n",
        "                    print(f\"\ud83e\uddec Clonando voz de '{artist_name}' en idioma '{language}'...\", flush=True)\n",
        "                    await ws.send(json.dumps({\n",
        "                        \"type\": \"JOB_PROGRESS\",\n",
        "                        \"jobId\": job_id,\n",
        "                        \"percent\": 35,\n",
        "                        \"stage\": \"CLONANDO VOZ IA\",\n",
        "                        \"message\": f\"\ud83e\uddec Clonando timbre vocal exacto de {artist_name} en {language.upper()}...\"\n",
        "                    }))\n",
        "\n",
        "                    cloner = get_voice_cloner()\n",
        "                    xtts_lang = language\n",
        "                    print(f\"\u26a1 Ejecutando Coqui XTTS-v2 en GPU (idioma={xtts_lang}, muestra={sample_wav.name})...\", flush=True)\n",
        "                    try:\n",
        "                        cloner.tts_to_file(\n",
        "                            text=script,\n",
        "                            speaker_wav=str(sample_wav),\n",
        "                            language=xtts_lang,\n",
        "                            file_path=str(out_audio_path),\n",
        "                            split_sentences=True\n",
        "                        )\n",
        "                    except Exception as e_tts_split:\n",
        "                        print(f\"\u2139\ufe0f Reintentando sin split_sentences: {e_tts_split}\", flush=True)\n",
        "                        cloner.tts_to_file(\n",
        "                            text=script,\n",
        "                            speaker_wav=str(sample_wav),\n",
        "                            language=xtts_lang,\n",
        "                            file_path=str(out_audio_path),\n",
        "                            split_sentences=False\n",
        "                        )\n",
        "\n",
        "                    if out_audio_path.exists() and out_audio_path.stat().st_size > 1000:\n",
        "                        # Convertir a MP3 a 320kbps\n",
        "                        subprocess.run([\n",
        "                            \"ffmpeg\", \"-y\", \"-i\", str(out_audio_path),\n",
        "                            \"-c:a\", \"libmp3lame\", \"-b:a\", \"320k\", str(out_mp3_path)\n",
        "                        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)\n",
        "                        if out_mp3_path.exists() and out_mp3_path.stat().st_size > 1000:\n",
        "                            cloned_success = True\n",
        "                            print(f\"\u2705 \u00a1Clonaci\u00f3n vocal aut\u00e9ntica completada en {language.upper()} para {artist_name}!\", flush=True)\n",
        "        except Exception as clone_err:\n",
        "            import traceback\n",
        "            traceback.print_exc()\n",
        "            print(f\"\u274c Error durante clonaci\u00f3n XTTS-v2: {clone_err}\", flush=True)\n",
        "            raise RuntimeError(f\"Error en clonaci\u00f3n vocal XTTS-v2: {clone_err}\")\n",
        "\n",
        "    if not cloned_success:\n",
        "        raise RuntimeError(\"XTTS-v2 no gener\u00f3 una locuci\u00f3n clonada v\u00e1lida.\")\n",
        "\n",
        "    final_audio_path = out_mp3_path if (out_mp3_path.exists() and out_mp3_path.stat().st_size > 1000) else out_audio_path\n",
        "    print(f\"\u2705 Audio final generado: {final_audio_path.name}\", flush=True)\n",
        "\n",
        "    if not auto_video:\n",
        "        # Modo 1: Solo Audio / Locuci\u00f3n Clonada (MP3)\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_PROGRESS\",\n",
        "            \"jobId\": job_id,\n",
        "            \"percent\": 85,\n",
        "            \"stage\": \"SUBIENDO AUDIO\",\n",
        "            \"message\": \"\u2b06\ufe0f Subiendo locuci\u00f3n final al servidor...\"\n",
        "        }))\n",
        "\n",
        "        with open(final_audio_path, \"rb\") as f_aud:\n",
        "            files = {\"audio\": (final_audio_path.name, f_aud, \"audio/mpeg\")}\n",
        "            data = {\"jobId\": job_id, \"isFinal\": \"true\"}\n",
        "            r_up = requests.post(f\"{MASTER_HTTP_URL}/api/worker/upload-result\", files=files, data=data, timeout=60, verify=False)\n",
        "            uploaded_audio_url = confirm_speech_upload(r_up, job_id)\n",
        "            print(f\"\u2b06\ufe0f Respuesta de subida audio: {r_up.status_code}\", flush=True)\n",
        "\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_COMPLETED\",\n",
        "            \"jobId\": job_id,\n",
        "            \"result\": {\n",
        "                \"success\": True,\n",
        "                \"audioUrl\": uploaded_audio_url,\n",
        "                \"language\": language,\n",
        "                \"voiceSampleUrl\": voice_sample_url,\n",
        "                \"message\": f\"\ud83c\udf89 \u00a1Locuci\u00f3n de voz ({language.upper()}) para {artist_name} completada con \u00e9xito!\"\n",
        "            }\n",
        "        }))\n",
        "        print(f\"\ud83c\udf89 \u00a1TAREA DE VOZ {job_id} FINALIZADA AL 100%!\\n\", flush=True)\n",
        "    else:\n",
        "        # Modo 2: Video 9:16 con Subt\u00edtulos Whisper IA\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_PROGRESS\",\n",
        "            \"jobId\": job_id,\n",
        "            \"percent\": 60,\n",
        "            \"stage\": \"WHISPER SUBTITLES IA\",\n",
        "            \"message\": \"\ud83c\udf99\ufe0f Transcribiendo audio y generando subt\u00edtulos virales 9:16...\"\n",
        "        }))\n",
        "\n",
        "        ass_path = job_dir / \"speech_subs.ass\"\n",
        "        transcribe_audio_to_ass(final_audio_path, ass_path, style_index=0, language=\"zh\" if language == \"zh-cn\" else language)\n",
        "\n",
        "        cover_path = job_dir / \"cover_speech.png\"\n",
        "        create_default_gradient_cover(cover_path, title=artist_name, subtitle=social_handle)\n",
        "\n",
        "        out_video_path = job_dir / f\"speech_video_{job_id[:8]}.mp4\"\n",
        "        audio_dur = get_media_duration(final_audio_path)\n",
        "\n",
        "        render_single_video_variant(\n",
        "            audio_path=final_audio_path,\n",
        "            cover_path=cover_path,\n",
        "            out_video_path=out_video_path,\n",
        "            ass_path=ass_path,\n",
        "            duration=audio_dur,\n",
        "            variant_idx=0\n",
        "        )\n",
        "\n",
        "        upload_single_video_robust(job_id, out_video_path, is_final=True)\n",
        "\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_COMPLETED\",\n",
        "            \"jobId\": job_id,\n",
        "            \"result\": {\n",
        "                \"success\": True,\n",
        "                \"videosCount\": 1,\n",
        "                \"message\": f\"\ud83c\udf89 \u00a1Video 9:16 con voz de {artist_name} y subt\u00edtulos completado!\"\n",
        "            }\n",
        "        }))\n",
        "        print(f\"\ud83c\udf89 \u00a1VIDEO 9:16 CON VOZ {job_id} COMPLETADO AL 100%!\\n\", flush=True)\n",
        "\n",
        "# Dispatcher General\n",
        "async def process_job(job, ws):\n",
        "    job_id = job.get(\"id\")\n",
        "    job_type = job.get(\"type\", \"VIDEO_GENERATION\")\n",
        "    aspect_ratio = job.get(\"aspectRatio\", \"9:16\")\n",
        "\n",
        "    job_dir = Path(f\"/tmp/voxforja_jobs/{job_id}\")\n",
        "    if job_dir.exists(): shutil.rmtree(job_dir, ignore_errors=True)\n",
        "    job_dir.mkdir(parents=True, exist_ok=True)\n",
        "\n",
        "    if job_type == \"SPEECH_GENERATION\":\n",
        "        await render_speech_generation(job, ws, job_dir)\n",
        "    elif job_type == \"VIDEO_16_9\" or aspect_ratio == \"16:9\":\n",
        "        await render_cinema_169_full(job, ws, job_dir)\n",
        "    else:\n",
        "        await render_voxforja_916_batch(job, ws, job_dir)\n",
        "\n",
        "\n",
        "async def worker_loop():\n",
        "    last_job_time = time.time()\n",
        "    IDLE_TIMEOUT = 300  # 5 minutos (300 segundos) de espera sin tareas antes de auto-apagarse\n",
        "    \n",
        "    while True:\n",
        "        try:\n",
        "            print(f\"\ud83d\udd17 Conectando Worker GPU a {MASTER_WS_URL}...\", flush=True)\n",
        "            async with websockets.connect(MASTER_WS_URL, ssl=ssl_context, max_size=500*1024*1024) as ws:\n",
        "                print(f\"\\n{'='*65}\", flush=True)\n",
        "                print(f\"[OK] \u00a1KAGGLE CLOUD GPU WORKER CONECTADO Y AUTENTICADO!\", flush=True)\n",
        "                print(f\"\ud83d\udd25 GPU: {GPU_NAME} | VRAM: {GPU_VRAM}\", flush=True)\n",
        "                print(f\"\u26a1 Motores Activos: VOXFORJA 9:16 + CINEMA 16:9 + VOICE STUDIO (XTTS-v2)\", flush=True)\n",
        "                print(f\"\u23f0 Modo On-Demand: Auto-apagado programado tras 5 min de inactividad\", flush=True)\n",
        "                print(f\"{'='*65}\\n\", flush=True)\n",
        "\n",
        "                await ws.send(json.dumps({\n",
        "                    \"type\": \"WORKER_REGISTER\",\n",
        "                    \"gpuName\": f\"Kaggle {GPU_NAME} (On-Demand)\",\n",
        "                    \"gpuVram\": GPU_VRAM,\n",
        "                    \"supportedEngines\": [\"VOXFORJA_9_16\", \"CINEMA_16_9\", \"VOICE_STUDIO\", \"SMART_PHRASING\", \"WAIST_SUBTITLES\", \"HYPERFRAMES\"]\n",
        "                }))\n",
        "\n",
        "                last_job_time = time.time()\n",
        "\n",
        "                while True:\n",
        "                    try:\n",
        "                        # Esperar mensajes con timeout de 10s para verificar inactividad\n",
        "                        msg_str = await asyncio.wait_for(ws.recv(), timeout=10.0)\n",
        "                        msg = json.loads(msg_str)\n",
        "                        mtype = msg.get(\"type\")\n",
        "                        if mtype in [\"DISPATCH_JOB\", \"NEW_JOB\", \"JOB_DISPATCH\"]:\n",
        "                            job = msg.get(\"job\", {})\n",
        "                            jid = job.get(\"id\")\n",
        "                            if jid and jid not in ACTIVE_RUNNING_JOBS:\n",
        "                                ACTIVE_RUNNING_JOBS.add(jid)\n",
        "                                last_job_time = time.time()\n",
        "                                asyncio.create_task(process_job_safe(job, ws))\n",
        "                        elif mtype == \"CANCEL_JOB\":\n",
        "                            c_id = msg.get(\"jobId\")\n",
        "                            if c_id: CURRENT_CANCELLED_JOBS.add(c_id)\n",
        "                    except asyncio.TimeoutError:\n",
        "                        if len(ACTIVE_RUNNING_JOBS) > 0:\n",
        "                            last_job_time = time.time()\n",
        "                        else:\n",
        "                            idle_elapsed = time.time() - last_job_time\n",
        "                            if idle_elapsed >= IDLE_TIMEOUT:\n",
        "                                print(f\"\u23f0 [KAGGLE AUTO-SLEEP] Inactividad superada ({int(idle_elapsed)}s sin tareas). Cerrando worker para apagar GPU y ahorrar cuota semanal.\", flush=True)\n",
        "                                try:\n",
        "                                    await ws.close()\n",
        "                                except Exception:\n",
        "                                    pass\n",
        "                                sys.exit(0)\n",
        "                    except Exception as loop_inner_err:\n",
        "                        print(f\"\u26a0\ufe0f Error en lectura de socket: {loop_inner_err}\", flush=True)\n",
        "                        break\n",
        "        except Exception as conn_err:\n",
        "            print(f\"\u26a0\ufe0f Desconectado del Master: {conn_err}. Reintentando en 4s...\", flush=True)\n",
        "            await asyncio.sleep(4)\n",
        "\n",
        "async def process_job_safe(job, ws):\n",
        "    jid = job.get(\"id\")\n",
        "    try:\n",
        "        await process_job(job, ws)\n",
        "    except Exception as e:\n",
        "        print(f\"\u274c Error procesando trabajo {jid}: {e}\", flush=True)\n",
        "        await ws.send(json.dumps({\n",
        "            \"type\": \"JOB_FAILED\",\n",
        "            \"jobId\": jid,\n",
        "            \"error\": str(e)\n",
        "        }))\n",
        "    finally:\n",
        "        ACTIVE_RUNNING_JOBS.discard(jid)\n",
        "\n",
        "# Iniciar bucle del worker en segundo plano\n",
        "loop = asyncio.get_event_loop()\n",
        "loop.run_until_complete(worker_loop())\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.10"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}