<!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>