<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8" />

  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>AI Video Production Command Hub (Contabo VPS Plus 6 + RunPod)</title>

  <style>

    :root {

      --bg: #090d16;

      --panel: #111827;

      --panel-alt: #1e293b;

      --border: #334155;

      --text: #f8fafc;

      --muted: #94a3b8;

      --accent: #38bdf8;

      --accent-glow: rgba(56, 189, 248, 0.18);

      --green: #10b981;

      --amber: #f59e0b;

      --purple: #a855f7;

      --rose: #fb7185;

    }

    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {

      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

      background: var(--bg);

      color: var(--text);

      padding: 24px;

      line-height: 1.55;

    }

    .container { max-width: 1180px; margin: 0 auto; }


    /* Top Global Navigation Bar */

    .global-header {

      display: flex;

      flex-wrap: wrap;

      justify-content: space-between;

      align-items: center;

      gap: 16px;

      background: var(--panel);

      border: 1px solid var(--border);

      border-radius: 14px;

      padding: 18px 24px;

      margin-bottom: 22px;

    }

    .brand-title h1 { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.02em; }

    .brand-title p { color: var(--muted); font-size: 0.86rem; margin-top: 3px; }

    .view-switcher {

      display: flex;

      background: #060911;

      border: 1px solid var(--border);

      border-radius: 10px;

      padding: 4px;

      gap: 4px;

    }

    .view-btn {

      background: transparent;

      color: var(--muted);

      border: none;

      padding: 9px 16px;

      border-radius: 7px;

      font-size: 0.85rem;

      font-weight: 700;

      cursor: pointer;

      transition: all 0.2s;

    }

    .view-btn:hover { color: var(--text); }

    .view-btn.active {

      background: var(--accent);

      color: #060911;

    }


    .view-section { display: none; }

    .view-section.active { display: block; }


    /* Shared Card & Badge Styles */

    .card {

      background: var(--panel);

      border: 1px solid var(--border);

      border-radius: 14px;

      padding: 22px;

    }

    .badge {

      display: inline-block;

      font-size: 0.72rem;

      padding: 3px 8px;

      border-radius: 999px;

      font-weight: 600;

      margin-right: 4px;

      margin-bottom: 4px;

    }

    .badge-host { background: rgba(168, 85, 247, 0.16); color: #c084fc; }

    .badge-cost { background: rgba(16, 185, 129, 0.16); color: #34d399; }

    .badge-gate { background: rgba(245, 158, 11, 0.16); color: #fbbf24; }


    /* View 1: Build Checklist Styles */

    .progress-banner {

      display: flex;

      flex-wrap: wrap;

      justify-content: space-between;

      align-items: center;

      gap: 16px;

      background: var(--panel);

      border: 1px solid var(--border);

      border-radius: 12px;

      padding: 16px 20px;

      margin-bottom: 18px;

    }

    .progress-wrap { min-width: 280px; text-align: right; flex: 1; max-width: 420px; }

    .progress-label { font-size: 0.82rem; font-weight: 700; color: var(--accent); margin-bottom: 6px; }

    .progress-bar-bg {

      width: 100%;

      height: 10px;

      background: #060911;

      border-radius: 999px;

      overflow: hidden;

      border: 1px solid var(--border);

    }

    .progress-bar-fill {

      height: 100%;

      background: linear-gradient(90deg, var(--accent), var(--green));

      width: 0%;

      transition: width 0.3s ease;

    }

    .phase-tabs {

      display: grid;

      grid-template-columns: repeat(6, 1fr);

      gap: 10px;

      margin-bottom: 18px;

    }

    @media (max-width: 920px) { .phase-tabs { grid-template-columns: repeat(2, 1fr); } }

    .tab-btn {

      background: var(--panel);

      border: 1px solid var(--border);

      border-radius: 10px;

      padding: 12px;

      color: var(--text);

      cursor: pointer;

      text-align: left;

      transition: all 0.2s;

    }

    .tab-btn:hover { border-color: var(--accent); }

    .tab-btn.active {

      background: var(--panel-alt);

      border-color: var(--accent);

      box-shadow: 0 0 15px var(--accent-glow);

    }

    .tab-num { font-size: 0.7rem; text-transform: uppercase; color: var(--accent); font-weight: 700; }

    .tab-name { font-size: 0.88rem; font-weight: 700; margin-top: 2px; }

    .tab-status { font-size: 0.72rem; color: var(--muted); margin-top: 6px; }


    .main-grid {

      display: grid;

      grid-template-columns: 1.15fr 0.85fr;

      gap: 20px;

    }

    @media (max-width: 920px) { .main-grid { grid-template-columns: 1fr; } }

    .card h2 { font-size: 1.2rem; color: var(--accent); margin-bottom: 6px; }

    .card-sub { font-size: 0.86rem; color: var(--muted); margin-bottom: 18px; }

    .checklist { list-style: none; }

    .check-item {

      display: flex;

      align-items: flex-start;

      gap: 12px;

      background: rgba(255,255,255,0.02);

      border: 1px solid var(--border);

      border-radius: 10px;

      padding: 12px 14px;

      margin-bottom: 10px;

      cursor: pointer;

      transition: all 0.15s;

    }

    .check-item:hover { border-color: var(--accent); }

    .check-item.done {

      border-color: rgba(16, 185, 129, 0.45);

      background: rgba(16, 185, 129, 0.07);

    }

    .check-item input[type="checkbox"] {

      margin-top: 4px;

      width: 18px;

      height: 18px;

      accent-color: var(--green);

      cursor: pointer;

    }

    .check-text strong { display: block; font-size: 0.92rem; }

    .check-text span { font-size: 0.82rem; color: var(--muted); }

    .code-header {

      display: flex;

      justify-content: space-between;

      align-items: center;

      margin-bottom: 8px;

    }

    .code-header span {

      font-size: 0.75rem;

      text-transform: uppercase;

      letter-spacing: 0.06em;

      color: var(--muted);

      font-weight: 700;

    }

    .copy-btn, .btn {

      background: var(--panel-alt);

      color: var(--text);

      border: 1px solid var(--border);

      padding: 6px 12px;

      border-radius: 7px;

      font-size: 0.8rem;

      font-weight: 600;

      cursor: pointer;

    }

    .copy-btn { color: var(--accent); }

    .copy-btn:hover, .btn:hover { border-color: var(--accent); }

    pre {

      background: #05080f;

      border: 1px solid var(--border);

      border-radius: 10px;

      padding: 14px;

      font-family: "SFMono-Regular", Consolas, monospace;

      font-size: 0.78rem;

      color: #e2e8f0;

      overflow-x: auto;

      white-space: pre-wrap;

      margin-bottom: 14px;

    }

    .tip-box {

      background: rgba(245, 158, 11, 0.08);

      border-left: 3px solid var(--amber);

      padding: 12px 14px;

      border-radius: 6px;

      font-size: 0.82rem;

      color: #fde68a;

    }

    .nav-footer {

      display: flex;

      justify-content: space-between;

      margin-top: 16px;

      padding-top: 14px;

      border-top: 1px solid var(--border);

    }


    /* View 2: 5-Stage Pipeline Architecture Styles */

    .arch-toolbar {

      display: flex;

      flex-wrap: wrap;

      justify-content: space-between;

      align-items: center;

      gap: 12px;

      margin-bottom: 18px;

    }

    .mode-switch {

      display: flex;

      background: var(--panel);

      border: 1px solid var(--border);

      border-radius: 10px;

      padding: 4px;

      gap: 4px;

    }

    .mode-btn {

      background: transparent;

      color: var(--muted);

      border: none;

      padding: 8px 14px;

      border-radius: 7px;

      font-size: 0.82rem;

      font-weight: 600;

      cursor: pointer;

    }

    .mode-btn.active { background: var(--accent); color: #060911; }

    .pipeline-track {

      display: grid;

      grid-template-columns: repeat(5, 1fr);

      gap: 12px;

      margin-bottom: 20px;

    }

    @media (max-width: 920px) { .pipeline-track { grid-template-columns: 1fr; } }

    .node {

      background: var(--panel);

      border: 1px solid var(--border);

      border-radius: 12px;

      padding: 16px;

      cursor: pointer;

      transition: all 0.2s;

    }

    .node:hover { border-color: var(--accent); transform: translateY(-2px); }

    .node.active {

      border-color: var(--accent);

      background: var(--panel-alt);

      box-shadow: 0 0 18px var(--accent-glow);

    }

    .node-step { font-size: 0.72rem; text-transform: uppercase; color: var(--accent); font-weight: 700; margin-bottom: 4px; }

    .node-title { font-size: 0.96rem; font-weight: 700; margin-bottom: 10px; }

    .info-row {

      display: grid;

      grid-template-columns: repeat(3, 1fr);

      gap: 10px;

      margin-bottom: 16px;

    }

    .info-card {

      background: #060911;

      border: 1px solid var(--border);

      padding: 12px;

      border-radius: 8px;

    }

    .info-card span { display: block; font-size: 0.7rem; color: var(--muted); text-transform: uppercase; margin-bottom: 4px; }

    .info-card strong { font-size: 0.88rem; }

    .behavior-box {

      background: rgba(56, 189, 248, 0.06);

      border-left: 3px solid var(--accent);

      padding: 14px;

      border-radius: 6px;

      margin-bottom: 14px;

    }

    .behavior-box h4 { font-size: 0.8rem; text-transform: uppercase; color: var(--accent); margin-bottom: 6px; }


    /* View 3: Spec & Budget Table Styles */

    .spec-grid {

      display: grid;

      grid-template-columns: 1fr 1fr;

      gap: 20px;

      margin-bottom: 20px;

    }

    @media (max-width: 920px) { .spec-grid { grid-template-columns: 1fr; } }

    table {

      width: 100%;

      border-collapse: collapse;

      font-size: 0.85rem;

      margin-top: 10px;

    }

    th, td {

      text-align: left;

      padding: 10px 12px;

      border-bottom: 1px solid var(--border);

    }

    th { color: var(--accent); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }

  </style>

</head>

<body>

  <div class="container">

    <!-- Top Global Navigation -->

    <div class="global-header">

      <div class="brand-title">

        <h1>AI Video Production Studio Hub</h1>

        <p>Contabo Cloud VPS Plus 6 (Brain) + Cloudflare R2 (Bridge) + RunPod Serverless (Muscle)</p>

      </div>

      <div class="view-switcher">

        <button class="view-btn active" id="nav-checklist" onclick="switchView('checklist')">1. 6-Phase Build Checklist</button>

        <button class="view-btn" id="nav-pipeline" onclick="switchView('pipeline')">2. 5-Stage Pipeline Explorer</button>

        <button class="view-btn" id="nav-budget" onclick="switchView('budget')">3. Spec & Budget Sheet</button>

      </div>

    </div>


    <!-- VIEW 1: 6-PHASE BUILD CHECKLIST -->

    <div class="view-section active" id="view-checklist">

      <div class="progress-banner">

        <div>

          <strong style="font-size:0.95rem;">Interactive Launch Playbook</strong>

          <p style="font-size:0.82rem; color:var(--muted);">Check off tasks as you build. Progress saves automatically in your browser.</p>

        </div>

        <div class="progress-wrap">

          <div class="progress-label" id="prog-text">0 / 24 Steps Completed (0%)</div>

          <div class="progress-bar-bg">

            <div class="progress-bar-fill" id="prog-fill"></div>

          </div>

        </div>

      </div>


      <div class="phase-tabs" id="tabs"></div>


      <div class="main-grid">

        <div class="card">

          <h2 id="phase-title"></h2>

          <div class="card-sub" id="phase-sub"></div>

          <ul class="checklist" id="checklist"></ul>

          <div class="nav-footer">

            <button class="btn" onclick="changePhase(-1)">← Previous Phase</button>

            <button class="btn" onclick="resetProgress()" style="color:#fb7185;">Reset Checkboxes</button>

            <button class="btn" onclick="changePhase(1)">Next Phase →</button>

          </div>

        </div>


        <div class="card">

          <div class="code-header">

            <span id="code-title">Terminal / Configuration Snippet</span>

            <button class="copy-btn" onclick="copyCode()" id="copy-btn">Copy Snippet</button>

          </div>

          <pre id="code-block"></pre>

          <div class="tip-box" id="phase-tip"></div>

        </div>

      </div>

    </div>


    <!-- VIEW 2: 5-STAGE PIPELINE ARCHITECTURE EXPLORER -->

    <div class="view-section" id="view-pipeline">

      <div class="arch-toolbar">

        <div>

          <h2 style="font-size:1.15rem; color:var(--accent);">3-Agent Video Assembly Line Explorer</h2>

          <p style="font-size:0.84rem; color:var(--muted);">Click any stage below or toggle between In-House IP Vault Mode and Per-Project Mode.</p>

        </div>

        <div class="mode-switch">

          <button class="mode-btn active" id="btn-inhouse" onclick="setMode('inhouse')">In-House IP Mode (4-5 Shows)</button>

          <button class="mode-btn" id="btn-adhoc" onclick="setMode('adhoc')">Per-Project Mode (Ad-Hoc)</button>

        </div>

      </div>


      <div class="pipeline-track" id="track"></div>


      <div class="main-grid">

        <div class="card">

          <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px;">

            <h2 id="stage-title" style="margin:0;">Stage 1</h2>

            <div style="display:flex; gap:8px;">

              <button class="btn" onclick="stepStage(-1)">← Prev</button>

              <button class="btn" onclick="stepStage(1)">Next →</button>

            </div>

          </div>


          <div class="info-row">

            <div class="info-card">

              <span>Host Infrastructure</span>

              <strong id="stage-host"></strong>

            </div>

            <div class="info-card">

              <span>Active Compute</span>

              <strong id="stage-compute"></strong>

            </div>

            <div class="info-card">

              <span>Estimated Unit Cost</span>

              <strong id="stage-cost"></strong>

            </div>

          </div>


          <div class="behavior-box">

            <h4 id="mode-heading">In-House IP Behavior</h4>

            <p id="stage-desc" style="font-size:0.9rem;"></p>

          </div>


          <div class="tip-box" id="stage-note"></div>

        </div>


        <div class="card">

          <div class="code-header">

            <span>Live Data / API Payload Preview</span>

          </div>

          <pre id="stage-payload" style="margin-bottom:0;"></pre>

        </div>

      </div>

    </div>


    <!-- VIEW 3: STUDIO SPEC & BUDGET SHEET -->

    <div class="view-section" id="view-budget">

      <div class="spec-grid">

        <div class="card">

          <h2>Fixed Monthly Baseline (24/7 Operations)</h2>

          <p class="card-sub">Locked-in infrastructure running your studio brain, databases, and shared model volume.</p>

          <table>

            <thead>

              <tr><th>Component</th><th>Specification</th><th>Monthly Cost</th></tr>

            </thead>

            <tbody>

              <tr><td><strong>Contabo Cloud VPS Plus 6</strong></td><td>6 vCPU AMD EPYC / 12GB RAM / 300GB Gen4 NVMe</td><td><strong>$18.40 – $23.00</strong></td></tr>

              <tr><td><strong>Contabo Auto-Backup</strong></td><td>Automated server snapshots & rollback</td><td><strong>$2.00</strong></td></tr>

              <tr><td><strong>RunPod Network Volume</strong></td><td>250GB NVMe (Flux + Wan 2.2 + In-House LoRAs)</td><td><strong>$17.50</strong></td></tr>

              <tr><td><strong>Cloudflare R2 Storage</strong></td><td>~200GB keyframes, refs & MP4 clips ($0 egress)</td><td><strong>$3.00</strong></td></tr>

              <tr><td><strong>LLM API Tokens (Agent 1)</strong></td><td>50–100 storyboards per month</td><td><strong>$4.00 – $8.00</strong></td></tr>

              <tr><td><strong>Total Fixed Baseline</strong></td><td><strong>Always-On Brain + Model Storage</strong></td><td><strong>~$45.00 – $53.50/mo</strong></td></tr>

            </tbody>

          </table>

        </div>


        <div class="card">

          <h2>Pay-Per-Second GPU Economics (RunPod)</h2>

          <p class="card-sub">Serverless endpoints stay at $0.00/hr until an agent dispatches a render job.</p>

          <table>

            <thead>

              <tr><th>Task / Deliverable</th><th>Serverless Worker</th><th>Unit Cost</th></tr>

            </thead>

            <tbody>

              <tr><td><strong>1 Storyboard Still Keyframe</strong></td><td>Endpoint #1 (24GB GPU: Flux + LoRAs)</td><td><strong>~$0.005 / still</strong></td></tr>

              <tr><td><strong>20-Shot Visual Storyboard</strong></td><td>Includes 2x seed re-rolls (40 stills total)</td><td><strong>~$0.20 / board</strong></td></tr>

              <tr><td><strong>1 Approved 5-Sec Video Clip</strong></td><td>Endpoint #2 (48-80GB GPU: Wan 2.2 14B I2V)</td><td><strong>~$0.15 / clip</strong></td></tr>

              <tr><td><strong>Full 20-Shot Video Project</strong></td><td>20 approved video renders + 20% retakes</td><td><strong>~$3.60 – $4.50</strong></td></tr>

              <tr><td><strong>Custom LoRA Training Run</strong></td><td>On-Demand GPU Pod (Terminated after run)</td><td><strong>~$0.80 – $1.50</strong></td></tr>

              <tr><td><strong>25 Video Projects / Month</strong></td><td><strong>Fixed Baseline + 500 Video Shots</strong></td><td><strong>~$145 / month total</strong></td></tr>

            </tbody>

          </table>

        </div>

      </div>


      <div class="card">

        <h2>3 Mandatory Studio Cost Guardrails</h2>

        <div class="info-row" style="margin-top:12px; margin-bottom:0;">

          <div class="info-card">

            <span>Rule 1: Interactive Pods</span>

            <strong>Always TERMINATE temporary ComfyUI Pods when done. Your models stay safe on the $0.07/GB Network Volume.</strong>

          </div>

          <div class="info-card">

            <span>Rule 2: Stills Approval Gate</span>

            <strong>Never trigger Agent 3 (Video) until the still keyframe's character likeness, hands, and framing are approved ($0.005 vs $0.15).</strong>

          </div>

          <div class="info-card">

            <span>Rule 3: Storage Separation</span>

            <strong>Keep .safetensors checkpoints and LoRAs on RunPod; keep all PNG keyframes and MP4 footage on Cloudflare R2.</strong>

          </div>

        </div>

      </div>

    </div>

  </div>


  <script>

    // View Switcher

    function switchView(view) {

      ['checklist', 'pipeline', 'budget'].forEach(v => {

        document.getElementById(`view-${v}`).classList.toggle('active', v === view);

        document.getElementById(`nav-${v}`).classList.toggle('active', v === view);

      });

    }


    // VIEW 1 DATA: 6-Phase Build Checklist

    let currentPhase = 0;

    const savedState = JSON.parse(localStorage.getItem('studio_playbook_v1') || '{}');


    const phases = [

      {

        num: "Phase 1",

        name: "Contabo & Cloudflare DNS",

        title: "Phase 1: Order Contabo Cloud VPS Plus 6 & Set Cloudflare DNS",

        sub: "Provision your 6 vCPU / 12GB RAM / 300GB Gen4 NVMe server and point your Cloudflare domain.",

        tasks: [

          { id: "p1_t1", title: "Order Contabo Cloud VPS Plus 6 ($18.40/mo)", desc: "Select 300GB NVMe, nearest US Data Center, and enable the +$2/mo Auto-Backup add-on." },

          { id: "p1_t2", title: "Select Ubuntu 24.04 LTS (Clean OS)", desc: "Pick clean Ubuntu 24.04 LTS so Coolify can manage n8n, Postgres (pgvector), and FFmpeg together." },

          { id: "p1_t3", title: "Add 'ops' A-Record in Cloudflare DNS (Grey Cloud / DNS Only)", desc: "In Cloudflare -> DNS -> Records: Add Type A, Name 'ops', IPv4 = Contabo IP, Proxy OFF." },

          { id: "p1_t4", title: "Add 'n8n' A-Record in Cloudflare DNS (Grey Cloud / DNS Only)", desc: "In Cloudflare -> DNS -> Records: Add Type A, Name 'n8n', IPv4 = Contabo IP, Proxy OFF." }

        ],

        codeTitle: "Cloudflare DNS Records to Add",

        code: `Type: A   | Name: ops   | IPv4: YOUR_CONTABO_IP   | Proxy Status: DNS only (Grey Cloud)\nType: A   | Name: n8n   | IPv4: YOUR_CONTABO_IP   | Proxy Status: DNS only (Grey Cloud)`,

        tip: "Keeping Cloudflare's Proxy toggle OFF (Grey Cloud) allows Coolify to issue free Let's Encrypt HTTPS certificates in 15 seconds without redirect errors."

      },

      {

        num: "Phase 2",

        name: "Coolify & AI Brain",

        title: "Phase 2: Install Coolify, PostgreSQL (pgvector) & n8n",

        sub: "Turn your VPS into a visual PaaS and deploy your always-on orchestrator and database.",

        tasks: [

          { id: "p2_t1", title: "SSH into Contabo & Install FFmpeg + Coolify", desc: "Run the 2-line setup script in terminal to install FFmpeg and Coolify." },

          { id: "p2_t2", title: "Secure Coolify Dashboard on https://ops.yourdomain.com", desc: "Open http://YOUR_CONTABO_IP:8000, create admin user, and bind ops.yourdomain.com in Settings." },

          { id: "p2_t3", title: "Deploy PostgreSQL with pgvector:pg16 Image", desc: "In Coolify -> Add Database -> PostgreSQL -> set image to pgvector/pgvector:pg16 and click Start." },

          { id: "p2_t4", title: "Deploy n8n on https://n8n.yourdomain.com", desc: "In Coolify -> Add Resource -> n8n -> set domain to https://n8n.yourdomain.com and verify WEBHOOK_URL." }

        ],

        codeTitle: "SSH Commands (Run on Contabo VPS as root)",

        code: `ssh root@YOUR_CONTABO_IP\n\n# Update Ubuntu, install FFmpeg for video stitching, and install Coolify:\napt update && apt upgrade -y && apt install -y ffmpeg curl\ncurl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash\n\n# Required n8n Environment Variables in Coolify:\nN8N_EDITOR_BASE_URL=https://n8n.yourdomain.com\nWEBHOOK_URL=https://n8n.yourdomain.com/\nN8N_DEFAULT_BINARY_DATA_MODE=filesystem`,

        tip: "Setting N8N_DEFAULT_BINARY_DATA_MODE=filesystem stores temporary media files on your 300GB NVMe drive instead of filling up RAM."

      },

      {

        num: "Phase 3",

        name: "IP Vault SQL Schema",

        title: "Phase 3: Initialize the IP Vault & Shot State Machine",

        sub: "Create the PostgreSQL tables that store your 4-5 in-house shows, LoRA mappings, and shot approval status.",

        tasks: [

          { id: "p3_t1", title: "Connect n8n (or SQL client) to your Coolify Postgres DB", desc: "Use the internal Postgres credentials generated in Phase 2." },

          { id: "p3_t2", title: "Run the Master SQL Schema Script", desc: "Creates projects, ip_vault_assets, and storyboard_shots tables with approval status constraints." },

          { id: "p3_t3", title: "Verify Sample In-House Show Row", desc: "Confirm 'inhouse_show_01' and '@style_show1' are selectable in Postgres." },

          { id: "p3_t4", title: "Configure Automated DB Backup in Coolify", desc: "Enable daily Postgres dump backup in Coolify's database settings tab." }

        ],

        codeTitle: "PostgreSQL IP Vault & Shot Tracker Schema",

        code: `CREATE EXTENSION IF NOT EXISTS vector;\n\nCREATE TABLE projects (\n  project_id VARCHAR(64) PRIMARY KEY,\n  project_name VARCHAR(255) NOT NULL,\n  project_mode VARCHAR(32) CHECK (project_mode IN ('IN_HOUSE_LOCKED', 'AD_HOC_CLIENT')),\n  show_bible_rules TEXT\n);\n\nCREATE TABLE ip_vault_assets (\n  asset_tag VARCHAR(64) PRIMARY KEY,\n  project_id VARCHAR(64) REFERENCES projects(project_id),\n  asset_type VARCHAR(32) NOT NULL,\n  trigger_word VARCHAR(128) NOT NULL,\n  lora_filename VARCHAR(255),\n  lora_strength DECIMAL(3,2) DEFAULT 0.80,\n  ref_image_r2_url TEXT\n);\n\nCREATE TABLE storyboard_shots (\n  shot_id SERIAL PRIMARY KEY,\n  project_id VARCHAR(64) REFERENCES projects(project_id),\n  episode_code VARCHAR(64) NOT NULL,\n  shot_number INT NOT NULL,\n  active_tags TEXT[],\n  still_prompt TEXT NOT NULL,\n  video_motion_prompt TEXT NOT NULL,\n  seed BIGINT DEFAULT 42,\n  status VARCHAR(32) DEFAULT 'STORYBOARD_READY',\n  still_r2_url TEXT,\n  video_r2_url TEXT\n);`,

        tip: "Every shot moves strictly through: STORYBOARD_READY -> WAITING_APPROVAL -> STILL_APPROVED -> VIDEO_COMPLETE."

      },

      {

        num: "Phase 4",

        name: "Cloudflare R2 Bridge",

        title: "Phase 4: Set Up Cloudflare R2 Media Storage ($0 Egress)",

        sub: "Create the high-speed asset bucket where RunPod workers upload rendered keyframes and MP4 clips.",

        tasks: [

          { id: "p4_t1", title: "Create R2 Bucket 'studio-pipeline-media'", desc: "In Cloudflare Dashboard -> R2 -> Create Bucket (North America / Automatic)." },

          { id: "p4_t2", title: "Connect Custom Media Subdomain (media.yourdomain.com)", desc: "Enable custom domain access so Slack/Airtable and RunPod can read keyframe URLs." },

          { id: "p4_t3", title: "Create R2 S3 API Token (Object Read & Write)", desc: "Copy Access Key ID, Secret Access Key, and S3 Endpoint URL." },

          { id: "p4_t4", title: "Create /refs and /renders Folder Prefixes", desc: "Ready to hold character turnaround sheets and generated outputs." }

        ],

        codeTitle: "R2 Credentials Checklist (Save for RunPod & n8n)",

        code: `BUCKET_NAME=studio-pipeline-media\nBUCKET_ENDPOINT_URL=https://<ACCOUNT_ID>.r2.cloudflarestorage.com\nBUCKET_ACCESS_KEY_ID=<YOUR_R2_ACCESS_KEY>\nBUCKET_SECRET_ACCESS_KEY=<YOUR_R2_SECRET_KEY>\nPUBLIC_MEDIA_URL=https://media.yourdomain.com`,

        tip: "Never send large base64 video payloads back through RunPod API responses (capped at 10-20MB). Always let RunPod upload directly to R2."

      },

      {

        num: "Phase 5",

        name: "RunPod GPU Muscle",

        title: "Phase 5: Shared Network Volume & 2 Serverless Endpoints",

        sub: "Load Flux & Wan 2.2 onto one shared drive, export your API JSONs, and configure scale-to-zero workers.",

        tasks: [

          { id: "p5_t1", title: "Create 250GB RunPod Network Volume ('studio-comfy-models')", desc: "Pick a high-availability US datacenter with both 24GB and 48GB/80GB GPUs." },

          { id: "p5_t2", title: "Boot Temp Pod, Download Flux + Wan 2.2 + LoRAs, Export API JSONs", desc: "Enable Dev Mode in ComfyUI, export agent2_stills_api.json & agent3_video_api.json, then TERMINATE the Pod." },

          { id: "p5_t3", title: "Deploy Serverless Endpoint #1: agent2-stills-worker (24GB GPU)", desc: "Attach Network Volume, set Min Workers = 0, Max Workers = 2, Idle Timeout = 15s." },

          { id: "p5_t4", title: "Deploy Serverless Endpoint #2: agent3-video-worker (48-80GB GPU)", desc: "Select L40S/A100 pool, attach same Network Volume, set Min Workers = 0." }

        ],

        codeTitle: "RunPod Network Volume Directory Layout (/runpod-volume)",

        code: `/runpod-volume/models/\n  ├── checkpoints/   # flux1-dev-fp8.safetensors\n  ├── unet/          # wan2.2_i2v_720p_14B_fp8_scaled.safetensors\n  ├── clip/          # t5xxl_fp8_e4m3fn.safetensors, umt5_xxl.safetensors\n  ├── vae/           # ae.safetensors, wan_2.1_vae.safetensors\n  └── loras/         # show1_style_v1.safetensors, char_marcus_v1.safetensors\n\n# REMEMBER: Terminate the interactive Pod as soon as downloads & JSON exports finish!`,

        tip: "Both Serverless endpoints mount the exact same /runpod-volume drive simultaneously—so any new LoRA you drop in /loras is instantly available to all workers."

      },

      {

        num: "Phase 6",

        name: "3-Agent Test Run",

        title: "Phase 6: Wire the 3 n8n Agents & Run Your First Test Shot",

        sub: "Connect the Storyboard, Stills, Approval Gate, and Video webhooks, then run a 3-shot test concept.",

        tasks: [

          { id: "p6_t1", title: "Build Agent 1 Flow (Concept -> LLM -> Postgres Shot Rows)", desc: "Verify a test concept generates 3 rows in storyboard_shots with status STORYBOARD_READY." },

          { id: "p6_t2", title: "Build Agent 2 Flow (Postgres -> RunPod Stills Endpoint -> R2)", desc: "Verify 3 still keyframes render on the 24GB endpoint (~$0.015 total) and flip status to WAITING_APPROVAL." },

          { id: "p6_t3", title: "Test the Human Approval Gate on Shot #1 Only", desc: "Flip Shot #1 to STILL_APPROVED and confirm Shots #2 and #3 remain paused." },

          { id: "p6_t4", title: "Verify Agent 3 Renders Video for Shot #1 & FFmpeg Stitches", desc: "Confirm the 48GB/80GB video worker animates Shot #1 into an MP4 on R2 and scales back to 0 workers." }

        ],

        codeTitle: "n8n -> RunPod Async Job Payload + Contabo FFmpeg Stitch Command",

        code: `// 1. n8n HTTP Request POST to: https://api.runpod.ai/v2/<ENDPOINT_ID>/run\n{\n  "webhook": "https://n8n.yourdomain.com/webhook/runpod-callback",\n  "input": {\n    "workflow": { /* Paste exported ComfyUI API JSON here with dynamic {{ $json.still_prompt }} */ }\n  }\n}\n\n# 2. Contabo VPS Lossless FFmpeg Stitch (Runs in <1 sec via Execute Command node):\nffmpeg -f concat -safe 0 -i /tmp/approved_clips.txt -c copy /tmp/episode_rough_cut.mp4`,

        tip: "Using RunPod's /run endpoint with the 'webhook' parameter means n8n never times out waiting for a 2-minute video render."

      }

    ];


    // VIEW 2 DATA: 5-Stage Pipeline Architecture Explorer

    let currentStage = 0;

    let currentMode = 'inhouse';


    const stages = [

      {

        step: "Stage 1",

        title: "Concept & IP Vault",

        host: "Contabo VPS (Postgres + pgvector)",

        compute: "CPU Only (24/7 VPS)",

        cost: "$0.00 marginal",

        inhouse: "Loads your locked Show Bible, camera lens rules, and permanent asset tags (@char_lead, @style_show1, @prop_car) mapped to trained LoRA files on your RunPod Network Volume.",

        adhoc: "Creates an isolated project folder in Cloudflare R2 and loads client moodboards, product PNGs, and creative brief text into a temporary pgvector namespace.",

        note: "Storing metadata and RAG memory on your Contabo VPS Plus 6 keeps your studio brain awake 24/7 with zero GPU burn.",

        payloadInhouse: `{\n  "project_id": "inhouse_show_02",\n  "mode": "LOCKED_IP_VAULT",\n  "rag_namespace": "show_02_bible",\n  "available_loras": [\n    "@style_retro_cinema (weight: 0.80)",\n    "@char_marcus_v2 (weight: 0.85)",\n    "@asset_hero_jacket (weight: 0.75)"\n  ]\n}`,

        payloadAdhoc: `{\n  "project_id": "client_spot_409",\n  "mode": "AD_HOC_REFERENCE",\n  "r2_reference_bucket": "s3://studio-pipeline-media/client_409/refs/",\n  "conditioning_method": "Flux_Redux + IP_Adapter"\n}`

      },

      {

        step: "Stage 2",

        title: "Agent 1: Storyboard",

        host: "Contabo VPS (n8n + LLM API)",

        compute: "CPU + Text LLM API",

        cost: "~$0.02 / full storyboard",

        inhouse: "Reads the concept and writes a structured JSON Shot List into PostgreSQL. Automatically injects strict database tags (@char_marcus_v2) and separates the static Still Prompt from the camera Motion Prompt.",

        adhoc: "Breaks the client brief into sequential shots with rich zero-shot visual descriptions and assigns reference image URLs for product or style matching.",

        note: "Separating 'still_prompt' (composition/lighting) from 'video_motion_prompt' (camera movement/action) prevents muddy video renders.",

        payloadInhouse: `{\n  "shot_id": "S02_E04_SH012",\n  "status": "STORYBOARD_READY",\n  "active_tags": ["@style_retro_cinema", "@char_marcus_v2"],\n  "still_prompt": "Medium close-up of @char_marcus_v2 in neon diner booth, 35mm anamorphic lens, rim lighting",\n  "video_motion_prompt": "Slow push-in as Marcus looks up toward the window, rain streaking down glass"\n}`,

        payloadAdhoc: `{\n  "shot_id": "CLIENT_409_SH003",\n  "status": "STORYBOARD_READY",\n  "ref_images": ["https://media.yourdomain.com/client_409/refs/product_bottle.png"],\n  "still_prompt": "Commercial macro shot of matte black bottle on wet slate rock, dramatic backlight",\n  "video_motion_prompt": "Camera orbits 15 degrees right as water droplets mist across the frame"\n}`

      },

      {

        step: "Stage 3",

        title: "Agent 2: Stills Worker",

        host: "RunPod Serverless #1",

        compute: "24GB GPU (RTX 4090 / L4)",

        cost: "~$0.005 / still (4-8 sec)",

        inhouse: "Wakes up the 24GB ComfyUI Stills Endpoint, dynamically attaches your character/style LoRAs from the shared RunPod Network Volume, renders the 16:9 keyframe via Flux, and uploads the PNG to Cloudflare R2.",

        adhoc: "Wakes up the 24GB ComfyUI Stills Endpoint using Flux Redux / PulID / ControlNet to match client reference images without needing hours of LoRA training.",

        note: "Batching 20 storyboard stills together means you only pay for container boot once (~$0.10 total for a 20-shot visual board).",

        payloadInhouse: `POST https://api.runpod.ai/v2/stills-worker/run\n{\n  "input": {\n    "workflow": "flux_lora_keyframe_v3.json",\n    "loras": [\n      {"file": "/runpod-volume/models/loras/show2_style.safetensors", "strength": 0.8},\n      {"file": "/runpod-volume/models/loras/char_marcus.safetensors", "strength": 0.85}\n    ],\n    "resolution": "1344x768",\n    "output_r2_path": "s3://studio-pipeline-media/show2/ep4/sh012_keyframe.png"\n  }\n}`,

        payloadAdhoc: `POST https://api.runpod.ai/v2/stills-worker/run\n{\n  "input": {\n    "workflow": "flux_ipadapter_commercial.json",\n    "reference_image": "https://media.yourdomain.com/client_409/refs/product_bottle.png",\n    "resolution": "1344x768",\n    "output_r2_path": "s3://studio-pipeline-media/client_409/sh003_keyframe.png"\n  }\n}`

      },

      {

        step: "Stage 4",

        title: "Human Approval Gate",

        host: "Airtable / Slack / Web UI",

        compute: "Zero GPU (Paused State)",

        cost: "$0.00 while reviewing",

        inhouse: "You inspect the complete visual storyboard grid. If a character's hand or expression is off, click 'Re-Roll Still' ($0.005) or tweak the prompt. Only shots marked STILL_APPROVED unlock Agent 3.",

        adhoc: "Share the still storyboard grid with your producer or client for sign-off before spending a single dime on heavy video GPU compute.",

        note: "Cost Shield: Rejecting and fixing 10 shots at the Stills stage costs $0.05 instead of $2.00+ at the Video stage.",

        payloadInhouse: `{\n  "shot_id": "S02_E04_SH012",\n  "keyframe_url": "https://media.yourdomain.com/show2/ep4/sh012_keyframe.png",\n  "review_action": "APPROVE_FOR_VIDEO",\n  "new_status": "STILL_APPROVED",\n  "trigger_next": "agent_3_video_queue"\n}`,

        payloadAdhoc: `{\n  "shot_id": "CLIENT_409_SH003",\n  "keyframe_url": "https://media.yourdomain.com/client_409/sh003_keyframe.png",\n  "review_action": "REROLL_SEED",\n  "feedback_note": "Make lighting warmer on bottle label",\n  "loop_back_to": "Stage 3 (Agent 2: Stills Worker)"\n}`

      },

      {

        step: "Stage 5",

        title: "Agent 3: Video Worker",

        host: "RunPod Serverless #2",

        compute: "48GB-80GB GPU (L40S / A100)",

        cost: "~$0.12 - $0.25 / 5s clip",

        inhouse: "Pulls the approved keyframe from R2 as Frame 1 (locking in your character & style 100%) and runs Wan 2.2 14B Image-to-Video on a heavy GPU using the motion prompt. Saves MP4 to R2 and stitches the scene.",

        adhoc: "Animates the client-approved product/scene keyframe via Image-to-Video, delivering high-bitrate MP4s back to R2 and notifying your team.",

        note: "Once the queue of approved shots finishes rendering, the 80GB GPU scales back to 0 workers immediately.",

        payloadInhouse: `POST https://api.runpod.ai/v2/video-worker/run\n{\n  "input": {\n    "workflow": "wan22_i2v_14b_production.json",\n    "init_image": "https://media.yourdomain.com/show2/ep4/sh012_keyframe.png",\n    "motion_prompt": "Slow push-in as Marcus looks up toward the window, rain streaking down glass",\n    "fps": 24,\n    "frames": 121,\n    "output_r2_path": "s3://studio-pipeline-media/show2/ep4/sh012_render.mp4"\n  }\n}`,

        payloadAdhoc: `POST https://api.runpod.ai/v2/video-worker/run\n{\n  "input": {\n    "workflow": "wan22_i2v_14b_production.json",\n    "init_image": "https://media.yourdomain.com/client_409/sh003_keyframe.png",\n    "motion_prompt": "Camera orbits 15 degrees right as water droplets mist across the frame",\n    "fps": 24,\n    "frames": 121,\n    "output_r2_path": "s3://studio-pipeline-media/client_409/sh003_render.mp4"\n  }\n}`

      }

    ];


    // Checklist Functions

    function renderTabs() {

      const tabsEl = document.getElementById('tabs');

      let totalDone = 0;

      let totalTasks = 0;


      tabsEl.innerHTML = phases.map((p, idx) => {

        const doneCount = p.tasks.filter(t => savedState[t.id]).length;

        totalDone += doneCount;

        totalTasks += p.tasks.length;

        return `

          <button class="tab-btn ${idx === currentPhase ? 'active' : ''}" onclick="selectPhase(${idx})">

            <div class="tab-num">${p.num}</div>

            <div class="tab-name">${p.name}</div>

            <div class="tab-status">${doneCount} / ${p.tasks.length} done</div>

          </button>

        `;

      }).join('');


      const pct = Math.round((totalDone / totalTasks) * 100);

      document.getElementById('prog-text').textContent = `${totalDone} / ${totalTasks} Steps Completed (${pct}%)`;

      document.getElementById('prog-fill').style.width = `${pct}%`;

    }


    function renderPhase() {

      const p = phases[currentPhase];

      document.getElementById('phase-title').textContent = p.title;

      document.getElementById('phase-sub').textContent = p.sub;

      document.getElementById('code-title').textContent = p.codeTitle;

      document.getElementById('code-block').textContent = p.code;

      document.getElementById('phase-tip').textContent = p.tip;


      const listEl = document.getElementById('checklist');

      listEl.innerHTML = p.tasks.map(t => {

        const checked = !!savedState[t.id];

        return `

          <li class="check-item ${checked ? 'done' : ''}" onclick="toggleTask('${t.id}')">

            <input type="checkbox" ${checked ? 'checked' : ''} onclick="event.stopPropagation(); toggleTask('${t.id}')" />

            <div class="check-text">

              <strong>${t.title}</strong>

              <span>${t.desc}</span>

            </div>

          </li>

        `;

      }).join('');


      renderTabs();

    }


    function toggleTask(id) {

      savedState[id] = !savedState[id];

      localStorage.setItem('studio_playbook_v1', JSON.stringify(savedState));

      renderPhase();

    }


    function selectPhase(idx) { currentPhase = idx; renderPhase(); }

    function changePhase(dir) { currentPhase = (currentPhase + dir + phases.length) % phases.length; renderPhase(); }


    function resetProgress() {

      if (confirm('Reset all checklist progress?')) {

        Object.keys(savedState).forEach(k => delete savedState[k]);

        localStorage.removeItem('studio_playbook_v1');

        renderPhase();

      }

    }


    function copyCode() {

      const code = document.getElementById('code-block').textContent;

      navigator.clipboard.writeText(code);

      const btn = document.getElementById('copy-btn');

      btn.textContent = 'Copied!';

      setTimeout(() => { btn.textContent = 'Copy Snippet'; }, 1800);

    }


    // Pipeline Explorer Functions

    function renderTrack() {

      const track = document.getElementById('track');

      track.innerHTML = stages.map((s, idx) => `

        <div class="node ${idx === currentStage ? 'active' : ''}" onclick="selectStage(${idx})">

          <div class="node-step">${s.step}</div>

          <div class="node-title">${s.title}</div>

          <span class="badge badge-host">${s.host.split(' ')[0]}</span>

          <span class="badge ${idx === 3 ? 'badge-gate' : 'badge-cost'}">${s.cost}</span>

        </div>

      `).join('');

    }


    function updateStageDetails() {

      const s = stages[currentStage];

      document.getElementById('stage-title').textContent = `${s.step}: ${s.title}`;

      document.getElementById('stage-host').textContent = s.host;

      document.getElementById('stage-compute').textContent = s.compute;

      document.getElementById('stage-cost').textContent = s.cost;

      document.getElementById('mode-heading').textContent =

        currentMode === 'inhouse' ? 'In-House IP Vault Behavior (Locked LoRAs)' : 'Per-Project Ad-Hoc Behavior (Zero-Shot Refs)';

      document.getElementById('stage-desc').textContent =

        currentMode === 'inhouse' ? s.inhouse : s.adhoc;

      document.getElementById('stage-note').textContent = s.note;

      document.getElementById('stage-payload').textContent =

        currentMode === 'inhouse' ? s.payloadInhouse : s.payloadAdhoc;

      renderTrack();

    }


    function selectStage(idx) { currentStage = idx; updateStageDetails(); }

    function stepStage(dir) { currentStage = (currentStage + dir + stages.length) % stages.length; updateStageDetails(); }

    function setMode(mode) {

      currentMode = mode;

      document.getElementById('btn-inhouse').classList.toggle('active', mode === 'inhouse');

      document.getElementById('btn-adhoc').classList.toggle('active', mode === 'adhoc');

      updateStageDetails();

    }


    // Initialize both views

    renderPhase();

    updateStageDetails();

  </script>

</body>

</html>