{"id":14,"date":"2026-04-22T08:39:39","date_gmt":"2026-04-22T08:39:39","guid":{"rendered":"https:\/\/twainstudio.dev\/?page_id=14"},"modified":"2026-04-22T08:47:11","modified_gmt":"2026-04-22T08:47:11","slug":"director-ai","status":"publish","type":"page","link":"https:\/\/twainstudio.dev\/?page_id=14","title":{"rendered":"DIRECTOR.AI"},"content":{"rendered":"\n<h2 class=\"wp-block-heading alignfull has-text-align-center is-style-text-subtitle has-accent-3-color has-text-color has-background has-link-color wp-elements-ab0b6d4e72aeba3b75d55cd7159070bd is-style-text-subtitle--1\" style=\"background:linear-gradient(135deg,rgb(6,147,227) 0%,rgb(174,133,133) 66%,rgb(155,81,224) 100%)\">DIRECTOR \u2014 The AI Cinematography Studio That Costs You Nothing<\/h2>\n\n\n\n<div class=\"wp-block-media-text alignfull is-image-fill-element\" style=\"grid-template-columns:31% auto\" id=\"DIRECTORSTUDIOS\"><figure class=\"wp-block-media-text__media\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1024\" src=\"https:\/\/twainstudio.dev\/wp-content\/uploads\/2026\/04\/DALL\u00b7E-2026-04-22-03.59.12-A-refined-premium-logo-on-a-deep-black-background-for-a-brand-named-DIRECTOR.-Create-a-single-unified-symbol-that-seamlessly-merges-a-directors-cha.webp\" alt=\"CAMERA SITTING IN A DIRECTOR CHAIR. \" class=\"wp-image-17 size-full\" style=\"object-position:49% 60%\" srcset=\"https:\/\/twainstudio.dev\/wp-content\/uploads\/2026\/04\/DALL\u00b7E-2026-04-22-03.59.12-A-refined-premium-logo-on-a-deep-black-background-for-a-brand-named-DIRECTOR.-Create-a-single-unified-symbol-that-seamlessly-merges-a-directors-cha.webp 1024w, https:\/\/twainstudio.dev\/wp-content\/uploads\/2026\/04\/DALL\u00b7E-2026-04-22-03.59.12-A-refined-premium-logo-on-a-deep-black-background-for-a-brand-named-DIRECTOR.-Create-a-single-unified-symbol-that-seamlessly-merges-a-directors-cha-300x300.webp 300w, https:\/\/twainstudio.dev\/wp-content\/uploads\/2026\/04\/DALL\u00b7E-2026-04-22-03.59.12-A-refined-premium-logo-on-a-deep-black-background-for-a-brand-named-DIRECTOR.-Create-a-single-unified-symbol-that-seamlessly-merges-a-directors-cha-150x150.webp 150w, https:\/\/twainstudio.dev\/wp-content\/uploads\/2026\/04\/DALL\u00b7E-2026-04-22-03.59.12-A-refined-premium-logo-on-a-deep-black-background-for-a-brand-named-DIRECTOR.-Create-a-single-unified-symbol-that-seamlessly-merges-a-directors-cha-768x768.webp 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure><div class=\"wp-block-media-text__content\">\n<p class=\"has-accent-3-color has-accent-1-background-color has-text-color has-background has-link-color wp-elements-1cbdf4ac3165cac6fa6d8cba1752a5b4\">Most people think professional video production requires a budget. DIRECTOR proves them wrong.<\/p>\n\n\n\n<p class=\"has-accent-3-color has-accent-1-background-color has-text-color has-background has-link-color wp-elements-8753ec2b094f4ce44a510a484a86bf75\">DIRECTOR is the first AI-powered video production studio that doesn&#8217;t just help you write scripts \u2014 it thinks like a real cinematographer. It analyzes your content, breaks it into scenes, and intelligently routes each one to the exact free-tier AI platform built to nail that specific shot. Talking head? HeyGen. Cinematic hero reveal? Google Vids. Social ad effect? Pika. Human emotion close-up? HaiLuo. All routed automatically. All free.<\/p>\n\n\n\n<p data-wp-context---core-fit-text=\"core\/fit-text::{&quot;fontSize&quot;:&quot;&quot;}\" data-wp-init---core-fit-text=\"core\/fit-text::callbacks.init\" data-wp-interactive data-wp-style--font-size=\"core\/fit-text::context.fontSize\" class=\"has-fit-text\">Behind the scenes, DIRECTOR is powered by your choice of AI engine \u2014 OpenAI, Claude, Gemini, Groq, DeepSeek, OpenRouter, or even a fully local Ollama model. You bring the idea. DIRECTOR builds the production plan.<\/p>\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group alignfull has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>DIRECTOR \u2014 AI Cinematography Studio<\/title>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Bebas+Neue&#038;family=Barlow:ital,wght@0,300;0,400;0,600;1,300&#038;family=JetBrains+Mono:wght@400;700&#038;display=swap\" rel=\"stylesheet\">\n<style>\n:root {\n  --bg: #080a0e;\n  --surface: #0d1117;\n  --card: #111820;\n  --border: #1e2a38;\n  --accent: #00d4ff;\n  --accent2: #ff3d6b;\n  --accent3: #ffd23f;\n  --gold: #c9a84c;\n  --text: #e8edf5;\n  --muted: #5a6a7e;\n  --dim: #2a3545;\n  --green: #00e676;\n  --orange: #ff9500;\n}\n* { margin:0; padding:0; box-sizing:border-box; }\nbody {\n  background: var(--bg);\n  color: var(--text);\n  font-family: 'Barlow', sans-serif;\n  font-weight: 300;\n  min-height: 100vh;\n  overflow-x: hidden;\n}\nbody::before {\n  content:'';\n  position:fixed;\n  inset:0;\n  background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,212,255,0.015) 2px, rgba(0,212,255,0.015) 4px);\n  pointer-events:none;\n  z-index:9999;\n}\n\n\/* \u2500\u2500 MODAL OVERLAY \u2500\u2500 *\/\n.modal-overlay {\n  position: fixed;\n  inset: 0;\n  background: rgba(0,0,0,0.85);\n  z-index: 10000;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  backdrop-filter: blur(4px);\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.25s;\n}\n.modal-overlay.open {\n  opacity: 1;\n  pointer-events: all;\n}\n.modal {\n  background: var(--surface);\n  border: 1px solid var(--border);\n  border-radius: 14px;\n  width: 580px;\n  max-width: 95vw;\n  max-height: 90vh;\n  overflow-y: auto;\n  transform: translateY(20px);\n  transition: transform 0.25s;\n  box-shadow: 0 0 60px rgba(0,212,255,0.1);\n}\n.modal-overlay.open .modal { transform: translateY(0); }\n.modal-header {\n  padding: 20px 24px 16px;\n  border-bottom: 1px solid var(--border);\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  position: sticky;\n  top: 0;\n  background: var(--surface);\n  z-index: 2;\n}\n.modal-title {\n  font-family: 'Bebas Neue', sans-serif;\n  font-size: 20px;\n  letter-spacing: 4px;\n  color: var(--accent);\n}\n.modal-close {\n  width: 30px; height: 30px;\n  border-radius: 50%;\n  background: var(--dim);\n  border: none;\n  color: var(--muted);\n  font-size: 16px;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.2s;\n}\n.modal-close:hover { background: var(--accent2); color: #fff; }\n.modal-body { padding: 20px 24px; }\n\n\/* Provider Cards *\/\n.provider-grid {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 10px;\n  margin-bottom: 20px;\n}\n.provider-card {\n  border: 1px solid var(--border);\n  border-radius: 10px;\n  padding: 12px 14px;\n  cursor: pointer;\n  transition: all 0.2s;\n  position: relative;\n}\n.provider-card:hover { border-color: var(--accent); background: rgba(0,212,255,0.04); }\n.provider-card.active { border-color: var(--accent); background: rgba(0,212,255,0.1); }\n.provider-card.active::after {\n  content: '\u2713';\n  position: absolute;\n  top: 10px; right: 10px;\n  width: 18px; height: 18px;\n  border-radius: 50%;\n  background: var(--accent);\n  color: #000;\n  font-size: 10px;\n  font-weight: bold;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  line-height: 18px;\n  text-align: center;\n}\n.prov-icon { font-size: 20px; margin-bottom: 6px; }\n.prov-name { font-size: 13px; font-weight: 600; }\n.prov-tag { font-size: 9px; color: var(--muted); letter-spacing: 1px; text-transform: uppercase; margin-top: 2px; }\n.prov-free {\n  font-size: 8px;\n  padding: 1px 5px;\n  border-radius: 3px;\n  background: rgba(0,230,118,0.15);\n  color: var(--green);\n  letter-spacing: 1px;\n  text-transform: uppercase;\n  display: inline-block;\n  margin-top: 4px;\n}\n\n\/* Config Section *\/\n.config-section { margin-bottom: 16px; }\n.config-label {\n  font-size: 9px;\n  letter-spacing: 3px;\n  text-transform: uppercase;\n  color: var(--muted);\n  margin-bottom: 6px;\n  display: block;\n}\n.config-input {\n  width: 100%;\n  background: var(--card);\n  border: 1px solid var(--border);\n  color: var(--text);\n  padding: 9px 12px;\n  border-radius: 6px;\n  font-size: 13px;\n  font-family: 'JetBrains Mono', monospace;\n  outline: none;\n  transition: border-color 0.2s;\n}\n.config-input:focus { border-color: var(--accent); }\n.config-input::placeholder { color: var(--muted); }\n.config-row { display: flex; gap: 10px; }\n.config-row .config-section { flex: 1; }\n\n.model-pills {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  margin-top: 6px;\n}\n.model-pill {\n  font-size: 10px;\n  padding: 4px 10px;\n  border-radius: 20px;\n  border: 1px solid var(--border);\n  color: var(--muted);\n  cursor: pointer;\n  transition: all 0.2s;\n  font-family: 'JetBrains Mono', monospace;\n  background: none;\n}\n.model-pill:hover { border-color: var(--accent); color: var(--accent); }\n.model-pill.active { background: var(--accent); color: #000; border-color: var(--accent); }\n\n.status-row {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 10px 12px;\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: 6px;\n  font-size: 11px;\n  margin-bottom: 16px;\n}\n.status-indicator {\n  width: 8px; height: 8px;\n  border-radius: 50%;\n  flex-shrink: 0;\n}\n.status-indicator.ok { background: var(--green); box-shadow: 0 0 6px var(--green); }\n.status-indicator.err { background: var(--accent2); box-shadow: 0 0 6px var(--accent2); }\n.status-indicator.idle { background: var(--muted); }\n\n.btn-test {\n  padding: 7px 14px;\n  background: var(--dim);\n  border: 1px solid var(--border);\n  color: var(--text);\n  border-radius: 5px;\n  font-family: 'Bebas Neue', sans-serif;\n  letter-spacing: 2px;\n  font-size: 12px;\n  cursor: pointer;\n  transition: all 0.2s;\n  margin-left: auto;\n}\n.btn-test:hover { border-color: var(--accent); color: var(--accent); }\n\n.note-box {\n  background: rgba(255,210,63,0.06);\n  border: 1px solid rgba(255,210,63,0.2);\n  border-radius: 6px;\n  padding: 10px 12px;\n  font-size: 11px;\n  color: var(--accent3);\n  line-height: 1.6;\n  margin-bottom: 16px;\n}\n.note-box a { color: var(--accent); text-decoration: none; }\n.note-box a:hover { text-decoration: underline; }\n\n.divider {\n  height: 1px;\n  background: var(--border);\n  margin: 16px 0;\n}\n\n\/* Active provider badge in header *\/\n.active-provider-badge {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 4px 10px;\n  background: rgba(0,212,255,0.08);\n  border: 1px solid rgba(0,212,255,0.2);\n  border-radius: 20px;\n  font-size: 10px;\n  letter-spacing: 1px;\n  text-transform: uppercase;\n  color: var(--accent);\n  cursor: pointer;\n  transition: all 0.2s;\n}\n.active-provider-badge:hover { background: rgba(0,212,255,0.15); }\n.provider-dot {\n  width: 6px; height: 6px;\n  border-radius: 50%;\n  background: var(--green);\n  box-shadow: 0 0 5px var(--green);\n}\n\n\/* \u2500\u2500 HEADER \u2500\u2500 *\/\n.masthead {\n  background: linear-gradient(180deg, #000 0%, var(--bg) 100%);\n  border-bottom: 1px solid var(--border);\n  padding: 0 24px;\n  display: flex;\n  align-items: center;\n  gap: 20px;\n  height: 56px;\n  position: sticky;\n  top:0;\n  z-index: 100;\n}\n.logo {\n  font-family: 'Bebas Neue', sans-serif;\n  font-size: 26px;\n  letter-spacing: 6px;\n  color: var(--accent);\n  text-shadow: 0 0 20px rgba(0,212,255,0.5);\n}\n.logo span { color: var(--accent2); }\n.tagline {\n  font-size: 10px;\n  letter-spacing: 3px;\n  text-transform: uppercase;\n  color: var(--muted);\n  border-left: 1px solid var(--border);\n  padding-left: 20px;\n}\n.status-dot {\n  width: 7px; height: 7px;\n  border-radius: 50%;\n  background: var(--green);\n  box-shadow: 0 0 8px var(--green);\n  animation: pulse 2s ease-in-out infinite;\n}\n@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }\n.status-label { font-size: 10px; letter-spacing: 2px; color: var(--green); text-transform: uppercase; }\n\n\/* \u2500\u2500 LAYOUT \u2500\u2500 *\/\n.studio {\n  display: grid;\n  grid-template-columns: 300px 1fr 280px;\n  height: calc(100vh - 56px);\n  overflow: hidden;\n}\n\n\/* LEFT PANEL *\/\n.platform-panel {\n  border-right: 1px solid var(--border);\n  overflow-y: auto;\n  background: var(--surface);\n}\n.panel-header {\n  padding: 14px 16px 10px;\n  font-family: 'Bebas Neue', sans-serif;\n  font-size: 13px;\n  letter-spacing: 4px;\n  color: var(--muted);\n  border-bottom: 1px solid var(--border);\n  position: sticky;\n  top: 0;\n  background: var(--surface);\n  z-index: 2;\n}\n.platform-section { padding: 10px 0; }\n.section-label {\n  font-size: 9px;\n  letter-spacing: 3px;\n  text-transform: uppercase;\n  color: var(--muted);\n  padding: 4px 16px 8px;\n}\n.platform-card {\n  margin: 4px 10px;\n  border: 1px solid var(--border);\n  border-radius: 8px;\n  padding: 10px 12px;\n  cursor: pointer;\n  transition: all 0.2s;\n  position: relative;\n  overflow: hidden;\n}\n.platform-card:hover { border-color: var(--accent); background: rgba(0,212,255,0.04); }\n.platform-card.selected { border-color: var(--accent); background: rgba(0,212,255,0.08); }\n.platform-card::before {\n  content: '';\n  position: absolute;\n  left: 0; top: 0; bottom: 0;\n  width: 3px;\n  background: var(--platform-color, var(--accent));\n  border-radius: 8px 0 0 8px;\n}\n.plat-name { font-size: 13px; font-weight: 600; }\n.plat-type { font-size: 10px; color: var(--muted); letter-spacing: 1px; margin-top: 1px; }\n.plat-tags { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 7px; }\n.tag {\n  font-size: 9px;\n  padding: 2px 6px;\n  border-radius: 3px;\n  background: var(--dim);\n  color: var(--muted);\n  letter-spacing: 1px;\n  text-transform: uppercase;\n}\n.tag.best { background: rgba(201,168,76,0.15); color: var(--gold); }\n.plat-limit {\n  font-size: 10px;\n  color: var(--accent3);\n  margin-top: 6px;\n  font-family: 'JetBrains Mono', monospace;\n}\n.free-badge {\n  position: absolute;\n  top: 8px; right: 10px;\n  font-size: 8px;\n  letter-spacing: 2px;\n  padding: 2px 5px;\n  border-radius: 3px;\n  background: rgba(0,230,118,0.15);\n  color: var(--green);\n  text-transform: uppercase;\n}\n\n\/* CENTER *\/\n.director-panel {\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.director-tabs {\n  display: flex;\n  border-bottom: 1px solid var(--border);\n  background: var(--surface);\n  padding: 0 16px;\n}\n.tab-btn {\n  padding: 14px 18px;\n  font-family: 'Bebas Neue', sans-serif;\n  letter-spacing: 3px;\n  font-size: 13px;\n  color: var(--muted);\n  cursor: pointer;\n  border-bottom: 2px solid transparent;\n  transition: all 0.2s;\n  background: none;\n  border-top: none;\n  border-left: none;\n  border-right: none;\n}\n.tab-btn:hover { color: var(--text); }\n.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }\n\n.tab-content { display: none; flex: 1; overflow: hidden; }\n.tab-content.active { display: flex; flex-direction: column; }\n\n\/* SCRIPT TAB *\/\n.script-area {\n  display: grid;\n  grid-template-rows: auto 1fr auto;\n  flex: 1;\n  overflow: hidden;\n}\n.script-toolbar {\n  display: flex;\n  gap: 8px;\n  padding: 12px 16px;\n  border-bottom: 1px solid var(--border);\n  align-items: center;\n  flex-wrap: wrap;\n}\n.input-group { display: flex; flex-direction: column; gap: 3px; }\n.input-group label { font-size: 9px; letter-spacing: 2px; color: var(--muted); text-transform: uppercase; }\n.ctrl-select, .ctrl-input {\n  background: var(--card);\n  border: 1px solid var(--border);\n  color: var(--text);\n  padding: 5px 10px;\n  font-size: 12px;\n  border-radius: 4px;\n  font-family: 'Barlow', sans-serif;\n}\n.ctrl-select:focus, .ctrl-input:focus { outline: none; border-color: var(--accent); }\n.btn {\n  padding: 8px 16px;\n  border: none;\n  border-radius: 5px;\n  font-family: 'Bebas Neue', sans-serif;\n  letter-spacing: 2px;\n  font-size: 13px;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n.btn-primary { background: var(--accent); color: #000; }\n.btn-primary:hover { background: #00b8d9; box-shadow: 0 0 15px rgba(0,212,255,0.3); }\n.btn-danger { background: var(--accent2); color: #fff; }\n.btn-danger:hover { box-shadow: 0 0 15px rgba(255,61,107,0.3); }\n.btn-ghost { background: var(--dim); color: var(--text); }\n.btn-ghost:hover { background: var(--border); }\n.btn-gold { background: var(--gold); color: #000; }\n.btn-gold:hover { box-shadow: 0 0 15px rgba(201,168,76,0.4); }\n\ntextarea.script-input {\n  background: var(--card);\n  color: var(--text);\n  border: none;\n  padding: 20px;\n  font-family: 'Barlow', sans-serif;\n  font-size: 14px;\n  line-height: 1.7;\n  resize: none;\n  flex: 1;\n  width: 100%;\n  outline: none;\n  border-bottom: 1px solid var(--border);\n}\ntextarea.script-input::placeholder { color: var(--muted); }\n\n.analyze-bar {\n  display: flex;\n  gap: 8px;\n  padding: 12px 16px;\n  align-items: center;\n  background: var(--surface);\n}\n.analyze-hint { font-size: 11px; color: var(--muted); flex: 1; }\n\n\/* SCENE ROUTER *\/\n.scene-container {\n  flex: 1;\n  overflow-y: auto;\n  padding: 16px;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n.empty-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  flex: 1;\n  gap: 12px;\n  opacity: 0.4;\n}\n.empty-icon { font-size: 48px; }\n.empty-msg { font-size: 13px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); }\n\n.scene-block {\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: 10px;\n  overflow: hidden;\n  transition: border-color 0.2s;\n  animation: slideIn 0.3s ease;\n}\n@keyframes slideIn { from { opacity:0; transform: translateY(10px); } }\n.scene-block:hover { border-color: var(--dim); }\n.scene-block.routed { border-left: 3px solid var(--platform-color, var(--accent)); }\n.scene-header {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 10px 14px;\n  border-bottom: 1px solid var(--border);\n}\n.scene-num {\n  font-family: 'Bebas Neue', sans-serif;\n  font-size: 14px;\n  letter-spacing: 2px;\n  color: var(--muted);\n  min-width: 32px;\n}\n.scene-type-badge {\n  font-size: 9px;\n  letter-spacing: 2px;\n  padding: 3px 8px;\n  border-radius: 4px;\n  text-transform: uppercase;\n  background: var(--dim);\n  color: var(--text);\n}\n.scene-dur { font-size: 11px; color: var(--muted); margin-left: auto; font-family: 'JetBrains Mono', monospace; }\n.scene-body { padding: 12px 14px; }\n.scene-desc { font-size: 13px; line-height: 1.6; margin-bottom: 10px; }\n.scene-assignment {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 8px 10px;\n  background: var(--surface);\n  border-radius: 6px;\n  border: 1px solid var(--border);\n}\n.assignment-label { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); }\n.assigned-platform { font-size: 12px; font-weight: 600; color: var(--accent); }\n.assignment-reason { font-size: 10px; color: var(--muted); margin-left: 4px; }\n.scene-actions { display: flex; gap: 6px; margin-top: 10px; }\n.btn-sm {\n  padding: 5px 10px;\n  font-size: 10px;\n  letter-spacing: 1px;\n  border-radius: 4px;\n  border: 1px solid var(--border);\n  background: none;\n  color: var(--muted);\n  cursor: pointer;\n  transition: all 0.2s;\n  font-family: 'Bebas Neue', sans-serif;\n}\n.btn-sm:hover { border-color: var(--accent); color: var(--accent); }\n.btn-sm.generate-btn { border-color: var(--accent2); color: var(--accent2); }\n.btn-sm.generate-btn:hover { background: var(--accent2); color: #fff; }\n\n\/* STORYBOARD *\/\n.storyboard-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n  gap: 12px;\n  padding: 16px;\n  overflow-y: auto;\n  flex: 1;\n}\n.board-card {\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: 8px;\n  overflow: hidden;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n.board-card:hover { border-color: var(--accent); transform: translateY(-2px); }\n.board-frame {\n  height: 100px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 28px;\n  position: relative;\n}\n.board-platform-badge {\n  position: absolute;\n  bottom: 6px; right: 6px;\n  font-size: 9px;\n  padding: 2px 6px;\n  border-radius: 3px;\n  background: rgba(0,0,0,0.7);\n  letter-spacing: 1px;\n}\n.board-info { padding: 8px 10px; }\n.board-title { font-size: 12px; font-weight: 600; }\n.board-desc { font-size: 10px; color: var(--muted); margin-top: 2px; line-height: 1.4; }\n\n\/* TIMELINE *\/\n.timeline-area { flex: 1; overflow: hidden; display: flex; flex-direction: column; }\n.timeline-scroll { overflow-x: auto; padding: 20px 16px; flex: 1; }\n.timeline-track { display: flex; gap: 3px; min-width: max-content; align-items: flex-end; }\n.tl-clip {\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: 6px;\n  padding: 8px;\n  cursor: pointer;\n  transition: all 0.2s;\n  min-width: 80px;\n  position: relative;\n  overflow: hidden;\n}\n.tl-clip::before {\n  content: '';\n  position: absolute;\n  top: 0; left: 0; right: 0;\n  height: 3px;\n  background: var(--clip-color, var(--accent));\n}\n.tl-clip:hover { border-color: var(--accent); }\n.tl-label { font-size: 10px; color: var(--muted); }\n.tl-plat { font-size: 9px; font-weight: 600; margin-top: 2px; }\n.tl-dur { font-size: 11px; font-family: 'JetBrains Mono', monospace; color: var(--accent3); margin-top: 4px; }\n.tl-transition { width: 20px; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 10px; }\n\n\/* AI CHAT *\/\n.ai-panel {\n  border-left: 1px solid var(--border);\n  display: flex;\n  flex-direction: column;\n  background: var(--surface);\n  overflow: hidden;\n}\n.chat-header {\n  padding: 14px 16px;\n  border-bottom: 1px solid var(--border);\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n.director-avatar {\n  width: 32px; height: 32px;\n  border-radius: 50%;\n  background: linear-gradient(135deg, var(--accent), var(--accent2));\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 16px;\n  flex-shrink: 0;\n}\n.director-name { font-family: 'Bebas Neue', sans-serif; letter-spacing: 2px; font-size: 14px; }\n.director-role { font-size: 9px; color: var(--muted); letter-spacing: 1px; }\n.chat-messages {\n  flex: 1;\n  overflow-y: auto;\n  padding: 12px;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n.msg {\n  padding: 10px 12px;\n  border-radius: 8px;\n  font-size: 12px;\n  line-height: 1.6;\n  animation: fadeIn 0.3s ease;\n}\n@keyframes fadeIn { from { opacity:0; transform: translateY(4px); } }\n.msg-director {\n  background: rgba(0,212,255,0.08);\n  border: 1px solid rgba(0,212,255,0.15);\n  border-radius: 2px 8px 8px 8px;\n}\n.msg-user {\n  background: var(--card);\n  border: 1px solid var(--border);\n  border-radius: 8px 2px 8px 8px;\n  align-self: flex-end;\n  text-align: right;\n}\n.msg-system {\n  text-align: center;\n  color: var(--muted);\n  font-size: 10px;\n  letter-spacing: 2px;\n  text-transform: uppercase;\n  background: none;\n  border: none;\n}\n.msg-label { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 4px; }\n.msg-director .msg-label { color: var(--accent); }\n.msg-user .msg-label { color: var(--muted); }\n\n.typing-indicator {\n  display: flex;\n  gap: 4px;\n  align-items: center;\n  padding: 10px 12px;\n}\n.dot {\n  width: 5px; height: 5px;\n  background: var(--accent);\n  border-radius: 50%;\n  animation: bounce 1.2s infinite;\n}\n.dot:nth-child(2) { animation-delay: 0.2s; }\n.dot:nth-child(3) { animation-delay: 0.4s; }\n@keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} }\n\n.chat-input-area { border-top: 1px solid var(--border); padding: 10px; }\n.chat-input-wrap { display: flex; gap: 6px; align-items: flex-end; }\ntextarea.chat-input {\n  flex: 1;\n  background: var(--card);\n  border: 1px solid var(--border);\n  color: var(--text);\n  padding: 8px 10px;\n  border-radius: 6px;\n  font-size: 12px;\n  font-family: 'Barlow', sans-serif;\n  resize: none;\n  min-height: 36px;\n  max-height: 100px;\n  outline: none;\n  line-height: 1.4;\n}\ntextarea.chat-input:focus { border-color: var(--accent); }\n.send-btn {\n  width: 36px; height: 36px;\n  border-radius: 6px;\n  background: var(--accent);\n  color: #000;\n  border: none;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 16px;\n  transition: all 0.2s;\n  flex-shrink: 0;\n}\n.send-btn:hover { background: #00b8d9; }\n\n.quick-prompts { padding: 8px 10px; display: flex; flex-wrap: wrap; gap: 5px; }\n.qp {\n  font-size: 9px;\n  padding: 4px 8px;\n  border-radius: 4px;\n  border: 1px solid var(--border);\n  color: var(--muted);\n  cursor: pointer;\n  letter-spacing: 1px;\n  text-transform: uppercase;\n  transition: all 0.2s;\n  background: none;\n}\n.qp:hover { border-color: var(--accent); color: var(--accent); }\n\n.progress-strip { height: 3px; background: var(--border); position: relative; overflow: hidden; }\n.progress-fill {\n  height: 100%;\n  background: linear-gradient(90deg, var(--accent), var(--accent2));\n  width: 0%;\n  transition: width 0.5s ease;\n}\n\n.export-bar {\n  border-top: 1px solid var(--border);\n  padding: 10px 16px;\n  display: flex;\n  gap: 8px;\n  align-items: center;\n  background: var(--surface);\n}\n.export-stats { font-size: 11px; color: var(--muted); flex: 1; font-family: 'JetBrains Mono', monospace; }\n\n::-webkit-scrollbar { width: 4px; height: 4px; }\n::-webkit-scrollbar-track { background: transparent; }\n::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 2px; }\n::-webkit-scrollbar-thumb:hover { background: var(--muted); }\n\n.watermark-note { font-size: 9px; color: var(--orange); letter-spacing: 1px; text-transform: uppercase; }\n.reroute-select {\n  background: var(--surface);\n  border: 1px solid var(--border);\n  color: var(--text);\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-size: 11px;\n  cursor: pointer;\n}\n\n\/* No API key warning *\/\n.no-key-banner {\n  background: rgba(255,61,107,0.1);\n  border: 1px solid rgba(255,61,107,0.3);\n  border-radius: 6px;\n  padding: 8px 12px;\n  font-size: 11px;\n  color: var(--accent2);\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  margin: 8px 10px;\n  cursor: pointer;\n}\n.no-key-banner:hover { background: rgba(255,61,107,0.16); }\n<\/style>\n<\/head>\n<body>\n\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 API SETTINGS MODAL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<div class=\"modal-overlay\" id=\"settingsModal\">\n  <div class=\"modal\">\n    <div class=\"modal-header\">\n      <div class=\"modal-title\">\u2699 API Configuration<\/div>\n      <button class=\"modal-close\" onclick=\"closeSettings()\">\u2715<\/button>\n    <\/div>\n    <div class=\"modal-body\">\n\n      <div class=\"note-box\">\n        \ud83d\udd11 Your API key is stored only in your browser&#8217;s localStorage \u2014 never sent to any server other than the provider you select. Keys are never logged or shared.\n      <\/div>\n\n      <label class=\"config-label\">Select AI Provider<\/label>\n      <div class=\"provider-grid\" id=\"providerGrid\">\n        <!-- injected by JS -->\n      <\/div>\n\n      <div class=\"divider\"><\/div>\n\n      <div class=\"status-row\" id=\"connectionStatus\">\n        <div class=\"status-indicator idle\" id=\"statusDot\"><\/div>\n        <span id=\"statusText\" style=\"color:var(--muted)\">Not tested<\/span>\n        <button class=\"btn-test\" onclick=\"testConnection()\">Test Connection<\/button>\n      <\/div>\n\n      <div id=\"providerConfig\">\n        <!-- dynamic config fields injected here -->\n      <\/div>\n\n      <div style=\"display:flex;gap:10px;margin-top:4px;\">\n        <button class=\"btn btn-primary\" onclick=\"saveSettings()\" style=\"flex:1\">Save Settings<\/button>\n        <button class=\"btn btn-ghost\" onclick=\"closeSettings()\">Cancel<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 HEADER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n<header class=\"masthead\">\n  <div class=\"logo\">DIRECT<span>OR<\/span><\/div>\n  <div class=\"tagline\">AI Cinematography Studio \u00b7 Universal API<\/div>\n  <div style=\"margin-left:auto;display:flex;align-items:center;gap:10px;\">\n    <div class=\"active-provider-badge\" id=\"providerBadge\" onclick=\"openSettings()\">\n      <div class=\"provider-dot\"><\/div>\n      <span id=\"providerBadgeLabel\">Configure API \u2699<\/span>\n    <\/div>\n    <div class=\"status-dot\"><\/div>\n    <div class=\"status-label\">Studio<\/div>\n  <\/div>\n<\/header>\n\n<div class=\"studio\">\n\n  <!-- LEFT: PLATFORM ROSTER -->\n  <div class=\"platform-panel\">\n    <div class=\"panel-header\">\ud83c\udfac Platform Roster<\/div>\n\n    <div class=\"platform-section\">\n      <div class=\"section-label\">Text \u2192 Video Generation<\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#a855f7\" onclick=\"selectPlatform('kling')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Kling AI<\/div>\n        <div class=\"plat-type\">Motion \u00b7 Character Consistency<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Best Motion<\/span><span class=\"tag\">Cinematic<\/span><span class=\"tag\">720p<\/span><\/div>\n        <div class=\"plat-limit\">~66 credits\/day \u00b7 up to 10s<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#f97316\" onclick=\"selectPlatform('pika')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Pika<\/div>\n        <div class=\"plat-type\">Stylized Effects \u00b7 Social Ads<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Best Effects<\/span><span class=\"tag\">Ads<\/span><span class=\"tag\">Fast<\/span><\/div>\n        <div class=\"plat-limit\">~150 credits signup + daily \u00b7 3-4s<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#22c55e\" onclick=\"selectPlatform('hailuo')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">HaiLuo AI<\/div>\n        <div class=\"plat-type\">Human Movement \u00b7 Faces<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Best Faces<\/span><span class=\"tag\">Fastest<\/span><span class=\"tag\">Watermark<\/span><\/div>\n        <div class=\"plat-limit\">Several free\/day \u00b7 up to 6s<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#3b82f6\" onclick=\"selectPlatform('googlevids')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Google Vids (Veo 3.1)<\/div>\n        <div class=\"plat-type\">High Quality \u00b7 8s clips<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Highest Quality<\/span><span class=\"tag\">Google<\/span><\/div>\n        <div class=\"plat-limit\">10 videos\/month \u00b7 ~8s each<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#ef4444\" onclick=\"selectPlatform('runway')\">\n        <div class=\"free-badge\">TRIAL<\/div>\n        <div class=\"plat-name\">Runway Gen-4 Turbo<\/div>\n        <div class=\"plat-type\">Cinematic Leader \u00b7 One-time<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Industry Leader<\/span><span class=\"tag\">~25s total<\/span><\/div>\n        <div class=\"plat-limit\">125 credits one-time \u00b7 save wisely<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"platform-section\">\n      <div class=\"section-label\">AI Avatar \/ Talking Head<\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#ec4899\" onclick=\"selectPlatform('heygen')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">HeyGen<\/div>\n        <div class=\"plat-type\">Presenter \u00b7 Talking Head<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Best Avatar<\/span><span class=\"tag\">3min max<\/span><span class=\"tag\">Watermark<\/span><\/div>\n        <div class=\"plat-limit\">3 videos\/month \u00b7 up to 3min ea<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#8b5cf6\" onclick=\"selectPlatform('synthesia')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Synthesia<\/div>\n        <div class=\"plat-type\">Enterprise Avatar \u00b7 9 Stock<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Enterprise Grade<\/span><span class=\"tag\">9 Avatars<\/span><span class=\"tag\">Logo<\/span><\/div>\n        <div class=\"plat-limit\">10 min\/month \u00b7 1,200 credits<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"platform-section\">\n      <div class=\"section-label\">Text \u2192 Full Video Assembly<\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#f59e0b\" onclick=\"selectPlatform('invideo')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">InVideo AI<\/div>\n        <div class=\"plat-type\">Stock + Voiceover Assembly<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Full Videos<\/span><span class=\"tag\">Auto Assembly<\/span><span class=\"tag\">720p<\/span><\/div>\n        <div class=\"plat-limit\">10 min AI gen\/week \u00b7 watermark<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#06b6d4\" onclick=\"selectPlatform('arcade')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Arcade<\/div>\n        <div class=\"plat-type\">Product Demos \u00b7 Screen \u2192 Video<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Product Demos<\/span><span class=\"tag\">Figma<\/span><span class=\"tag\">SaaS<\/span><\/div>\n        <div class=\"plat-limit\">200 AI credits\/month (refreshes)<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#10b981\" onclick=\"selectPlatform('pictory')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Pictory AI<\/div>\n        <div class=\"plat-type\">Article\/Blog \u2192 Video<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Blog\u2192Video<\/span><span class=\"tag\">Auto Captions<\/span><span class=\"tag\">Stock<\/span><\/div>\n        <div class=\"plat-limit\">3 videos free trial \u00b7 watermark<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#6366f1\" onclick=\"selectPlatform('lumen5')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Lumen5<\/div>\n        <div class=\"plat-type\">Text \u2192 Slideshow Video \u00b7 Social<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Social Clips<\/span><span class=\"tag\">Templates<\/span><span class=\"tag\">720p<\/span><\/div>\n        <div class=\"plat-limit\">5 videos\/month free \u00b7 watermark<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#f43f5e\" onclick=\"selectPlatform('flexclip')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">FlexClip<\/div>\n        <div class=\"plat-type\">Online Editor \u00b7 Templates \u00b7 AI Tools<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Best Editor<\/span><span class=\"tag\">AI Script<\/span><span class=\"tag\">Templates<\/span><\/div>\n        <div class=\"plat-limit\">Free plan \u00b7 480p export \u00b7 watermark<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"platform-section\">\n      <div class=\"section-label\">Music \u00b7 Audio \u00b7 Stock<\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#7c3aed\" onclick=\"selectPlatform('artlist')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Artlist AI<\/div>\n        <div class=\"plat-type\">AI Music \u00b7 Sound Effects<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Best Music<\/span><span class=\"tag\">AI Generated<\/span><span class=\"tag\">Royalty-Free<\/span><\/div>\n        <div class=\"plat-limit\">Limited free tracks \u00b7 full library paid<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#0ea5e9\" onclick=\"selectPlatform('designsai')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Designs.ai<\/div>\n        <div class=\"plat-type\">AI Design Suite \u00b7 Logos \u00b7 Voiceover<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Full Suite<\/span><span class=\"tag\">Voiceover<\/span><span class=\"tag\">Logos<\/span><\/div>\n        <div class=\"plat-limit\">Free trial \u00b7 watermark on exports<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"platform-section\">\n      <div class=\"section-label\">Advanced \/ Open Source<\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#ff6b35\" onclick=\"selectPlatform('hunyuan')\">\n        <div class=\"free-badge\">FREE<\/div>\n        <div class=\"plat-name\">Hunyuan Video<\/div>\n        <div class=\"plat-type\">Tencent Open Source \u00b7 High Realism<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">Open Source<\/span><span class=\"tag\">High Realism<\/span><span class=\"tag\">Free<\/span><\/div>\n        <div class=\"plat-limit\">Free via Replicate\/HuggingFace<\/div>\n      <\/div>\n      <div class=\"platform-card\" style=\"--platform-color:#84cc16\" onclick=\"selectPlatform('comfyui')\">\n        <div class=\"free-badge\">LOCAL<\/div>\n        <div class=\"plat-name\">ComfyUI<\/div>\n        <div class=\"plat-type\">Local AI \u00b7 Full Control \u00b7 No Limits<\/div>\n        <div class=\"plat-tags\"><span class=\"tag best\">No Limits<\/span><span class=\"tag\">Local<\/span><span class=\"tag\">Custom Models<\/span><\/div>\n        <div class=\"plat-limit\">100% free \u00b7 needs GPU \u00b7 unlimited gen<\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- CENTER: DIRECTOR -->\n  <div class=\"director-panel\">\n    <div class=\"director-tabs\">\n      <button class=\"tab-btn active\" onclick=\"switchTab('script', this)\">Script<\/button>\n      <button class=\"tab-btn\" onclick=\"switchTab('router', this)\">Scene Router<\/button>\n      <button class=\"tab-btn\" onclick=\"switchTab('storyboard', this)\">Storyboard<\/button>\n      <button class=\"tab-btn\" onclick=\"switchTab('timeline', this)\">Timeline<\/button>\n    <\/div>\n    <div class=\"progress-strip\"><div class=\"progress-fill\" id=\"progressFill\"><\/div><\/div>\n\n    <!-- SCRIPT TAB -->\n    <div class=\"tab-content active\" id=\"tab-script\">\n      <div class=\"script-area\">\n        <div class=\"script-toolbar\">\n          <div class=\"input-group\">\n            <label>Format<\/label>\n            <select class=\"ctrl-select\" id=\"vidFormat\">\n              <option>YouTube Ad (30s)<\/option>\n              <option>TikTok Ad (15-60s)<\/option>\n              <option>Instagram Reel<\/option>\n              <option>YouTube Channel Intro<\/option>\n              <option>Product Demo<\/option>\n              <option>Brand Story<\/option>\n              <option>VSL (Long Form)<\/option>\n              <option>Podcast Promo<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"input-group\">\n            <label>Tone<\/label>\n            <select class=\"ctrl-select\" id=\"vidTone\">\n              <option>Cinematic \/ Premium<\/option>\n              <option>High Energy \/ Hype<\/option>\n              <option>Conversational \/ UGC<\/option>\n              <option>Professional \/ Corporate<\/option>\n              <option>Inspirational<\/option>\n              <option>Comedic \/ Playful<\/option>\n              <option>Urgent \/ Direct Response<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"input-group\">\n            <label>Target Length<\/label>\n            <select class=\"ctrl-select\" id=\"vidLength\">\n              <option>15 seconds<\/option>\n              <option>30 seconds<\/option>\n              <option>60 seconds<\/option>\n              <option>2 minutes<\/option>\n              <option>5 minutes<\/option>\n            <\/select>\n          <\/div>\n          <div class=\"input-group\">\n            <label>Product\/Brand<\/label>\n            <input type=\"text\" class=\"ctrl-input\" id=\"vidProduct\" placeholder=\"e.g. FitTrack Pro\" style=\"width:140px\">\n          <\/div>\n          <button class=\"btn btn-primary\" onclick=\"generateScript()\" style=\"align-self:flex-end\">Generate Script<\/button>\n        <\/div>\n        <textarea class=\"script-input\" id=\"scriptInput\" placeholder=\"Paste your script here, or use the controls above to generate one with AI.\n\nYour script will be analyzed and broken into scenes. Each scene is intelligently routed to the best free-tier platform.\n\nThe Director maximizes free credits across all platforms to build full professional videos for $0.\n\nStart with your hook \u2014 what's the first 3 seconds?\"><\/textarea>\n        <div class=\"analyze-bar\">\n          <div class=\"analyze-hint\" id=\"scriptStats\">0 words \u00b7 0 scenes estimated<\/div>\n          <button class=\"btn btn-ghost\" onclick=\"clearScript()\">Clear<\/button>\n          <button class=\"btn btn-gold\" onclick=\"analyzeScript()\">\u26a1 Analyze &amp; Route Scenes<\/button>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <!-- SCENE ROUTER TAB -->\n    <div class=\"tab-content\" id=\"tab-router\">\n      <div class=\"scene-container\" id=\"sceneContainer\">\n        <div class=\"empty-state\" id=\"routerEmpty\">\n          <div class=\"empty-icon\">\ud83c\udfac<\/div>\n          <div class=\"empty-msg\">Write or generate a script, then analyze it<\/div>\n        <\/div>\n      <\/div>\n      <div class=\"export-bar\">\n        <div class=\"export-stats\" id=\"routerStats\">No scenes routed yet<\/div>\n        <button class=\"btn btn-ghost\" onclick=\"addManualScene()\">+ Add Scene<\/button>\n        <button class=\"btn btn-primary\" onclick=\"buildStoryboard()\">Build Storyboard \u2192<\/button>\n      <\/div>\n    <\/div>\n\n    <!-- STORYBOARD TAB -->\n    <div class=\"tab-content\" id=\"tab-storyboard\">\n      <div class=\"storyboard-grid\" id=\"storyboardGrid\">\n        <div class=\"empty-state\" style=\"grid-column:1\/-1\">\n          <div class=\"empty-icon\">\ud83d\uddbc\ufe0f<\/div>\n          <div class=\"empty-msg\">Route scenes first to build storyboard<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <!-- TIMELINE TAB -->\n    <div class=\"tab-content\" id=\"tab-timeline\">\n      <div class=\"timeline-area\">\n        <div style=\"padding:12px 16px;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:center;\">\n          <span style=\"font-size:11px;color:var(--muted);\">Transition:<\/span>\n          <select class=\"ctrl-select\" id=\"transitionType\">\n            <option>Crossfade 0.5s<\/option>\n            <option>Hard Cut<\/option>\n            <option>Fade to Black<\/option>\n            <option>Zoom Blend<\/option>\n          <\/select>\n          <span style=\"font-size:11px;color:var(--muted);margin-left:auto;\" id=\"totalDur\">Total: 0s<\/span>\n        <\/div>\n        <div class=\"timeline-scroll\">\n          <div class=\"timeline-track\" id=\"timelineTrack\">\n            <div style=\"font-size:11px;color:var(--muted);padding:20px;\">Add scenes to see timeline<\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n      <div class=\"export-bar\">\n        <div class=\"export-stats\" id=\"exportStats\">Ready to export<\/div>\n        <button class=\"btn btn-ghost\" onclick=\"copyDirectorSheet()\">Copy Director Sheet<\/button>\n        <button class=\"btn btn-danger\" onclick=\"exportPlan()\">Export Production Plan<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- RIGHT: AI DIRECTOR CHAT -->\n  <div class=\"ai-panel\">\n    <div class=\"chat-header\">\n      <div class=\"director-avatar\">\ud83c\udfa5<\/div>\n      <div>\n        <div class=\"director-name\">DIRECTOR AI<\/div>\n        <div class=\"director-role\" id=\"directorRoleLabel\">Configure API to activate<\/div>\n      <\/div>\n    <\/div>\n    <div class=\"quick-prompts\">\n      <button class=\"qp\" onclick=\"quickPrompt('hook')\">Hook Ideas<\/button>\n      <button class=\"qp\" onclick=\"quickPrompt('cta')\">CTA Scripts<\/button>\n      <button class=\"qp\" onclick=\"quickPrompt('platform')\">Which Platform?<\/button>\n      <button class=\"qp\" onclick=\"quickPrompt('shots')\">Shot List<\/button>\n      <button class=\"qp\" onclick=\"quickPrompt('free')\">Maximize Free<\/button>\n      <button class=\"qp\" onclick=\"quickPrompt('voice')\">Voiceover Tips<\/button>\n    <\/div>\n    <div class=\"chat-messages\" id=\"chatMessages\">\n      <div class=\"msg msg-system\">Director AI Ready<\/div>\n      <div class=\"msg msg-director\">\n        <div class=\"msg-label\">Director<\/div>\n        I&#8217;m your AI Cinematographer and Platform Director. I know the strengths, limits, and tricks of every free-tier video platform.<br><br>\n        <strong>Click the provider badge in the top-right to configure your AI backend<\/strong> \u2014 supports OpenAI, Anthropic, Gemini, Copilot, DeepSeek, Groq, OpenRouter, and Ollama.<br><br>\n        Tell me what you&#8217;re making and I&#8217;ll write your script, break it into scenes, and route each one to the perfect free platform.\n      <\/div>\n    <\/div>\n    <div class=\"chat-input-area\">\n      <div class=\"chat-input-wrap\">\n        <textarea class=\"chat-input\" id=\"chatInputField\" placeholder=\"Describe your video, ask about platforms...\" rows=\"2\" onkeydown=\"handleChatKey(event)\"><\/textarea>\n        <button class=\"send-btn\" onclick=\"sendChat()\">\u27a4<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  PROVIDER DEFINITIONS\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst PROVIDERS = {\n  anthropic: {\n    label: 'Anthropic',\n    icon: '\ud83d\udfe3',\n    tag: 'Claude',\n    free: false,\n    endpoint: 'https:\/\/api.anthropic.com\/v1\/messages',\n    keyPlaceholder: 'sk-ant-api03-...',\n    keyHelp: 'https:\/\/console.anthropic.com\/settings\/keys',\n    defaultModel: 'claude-opus-4-5',\n    models: ['claude-opus-4-5', 'claude-sonnet-4-5', 'claude-haiku-4-5', 'claude-3-opus-20240229', 'claude-3-5-sonnet-20241022'],\n    hasBaseURL: false,\n  },\n  openai: {\n    label: 'OpenAI',\n    icon: '\u26ab',\n    tag: 'GPT',\n    free: false,\n    endpoint: 'https:\/\/api.openai.com\/v1\/chat\/completions',\n    keyPlaceholder: 'sk-proj-...',\n    keyHelp: 'https:\/\/platform.openai.com\/api-keys',\n    defaultModel: 'gpt-4o',\n    models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo', 'o1-preview', 'o1-mini'],\n    hasBaseURL: false,\n  },\n  gemini: {\n    label: 'Google Gemini',\n    icon: '\ud83d\udd35',\n    tag: 'Gemini',\n    free: true,\n    endpoint: 'https:\/\/generativelanguage.googleapis.com\/v1beta\/models\/{model}:generateContent',\n    keyPlaceholder: 'AIza...',\n    keyHelp: 'https:\/\/aistudio.google.com\/app\/apikey',\n    defaultModel: 'gemini-2.0-flash',\n    models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-2.5-pro-preview-06-05'],\n    hasBaseURL: false,\n  },\n  copilot: {\n    label: 'GitHub Copilot',\n    icon: '\ud83d\udc19',\n    tag: 'Copilot',\n    free: false,\n    endpoint: 'https:\/\/api.githubcopilot.com\/chat\/completions',\n    keyPlaceholder: 'ghu_... or Bearer token',\n    keyHelp: 'https:\/\/github.com\/settings\/tokens',\n    defaultModel: 'gpt-4o',\n    models: ['gpt-4o', 'gpt-4o-mini', 'claude-3.5-sonnet', 'o1-preview'],\n    hasBaseURL: false,\n  },\n  deepseek: {\n    label: 'DeepSeek',\n    icon: '\ud83d\udc0b',\n    tag: 'DeepSeek',\n    free: false,\n    endpoint: 'https:\/\/api.deepseek.com\/v1\/chat\/completions',\n    keyPlaceholder: 'sk-...',\n    keyHelp: 'https:\/\/platform.deepseek.com\/api_keys',\n    defaultModel: 'deepseek-chat',\n    models: ['deepseek-chat', 'deepseek-reasoner'],\n    hasBaseURL: false,\n  },\n  groq: {\n    label: 'Groq',\n    icon: '\u26a1',\n    tag: 'Ultra-Fast',\n    free: true,\n    endpoint: 'https:\/\/api.groq.com\/openai\/v1\/chat\/completions',\n    keyPlaceholder: 'gsk_...',\n    keyHelp: 'https:\/\/console.groq.com\/keys',\n    defaultModel: 'llama-3.3-70b-versatile',\n    models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768', 'gemma2-9b-it', 'llama3-groq-70b-8192-tool-use-preview'],\n    hasBaseURL: false,\n  },\n  openrouter: {\n    label: 'OpenRouter',\n    icon: '\ud83d\udd00',\n    tag: 'Multi-Model',\n    free: true,\n    endpoint: 'https:\/\/openrouter.ai\/api\/v1\/chat\/completions',\n    keyPlaceholder: 'sk-or-v1-...',\n    keyHelp: 'https:\/\/openrouter.ai\/keys',\n    defaultModel: 'meta-llama\/llama-3.3-70b-instruct',\n    models: ['meta-llama\/llama-3.3-70b-instruct', 'anthropic\/claude-3.5-sonnet', 'openai\/gpt-4o', 'google\/gemini-pro-1.5', 'mistralai\/mistral-large', 'deepseek\/deepseek-r1'],\n    hasBaseURL: false,\n  },\n  ollama: {\n    label: 'Ollama',\n    icon: '\ud83e\udd99',\n    tag: 'Local \u00b7 Free',\n    free: true,\n    endpoint: '{baseURL}\/api\/chat',\n    keyPlaceholder: '(no key needed)',\n    keyHelp: 'https:\/\/ollama.ai',\n    defaultModel: 'llama3.2',\n    models: ['llama3.2', 'llama3.1', 'mistral', 'phi3', 'gemma2', 'codellama', 'qwen2.5'],\n    hasBaseURL: true,\n    defaultBaseURL: 'http:\/\/localhost:11434',\n  },\n};\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  SETTINGS STATE\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet cfg = {\n  provider: localStorage.getItem('dir_provider') || '',\n  apiKey: '',\n  model: '',\n  baseURL: '',\n};\n\n\/\/ Load saved settings\nfunction loadSettings() {\n  cfg.provider = localStorage.getItem('dir_provider') || '';\n  cfg.model = localStorage.getItem('dir_model') || '';\n  cfg.baseURL = localStorage.getItem('dir_baseURL') || '';\n  \/\/ Key loaded from sessionStorage (safer for credentials)\n  cfg.apiKey = sessionStorage.getItem('dir_apiKey') || localStorage.getItem('dir_apiKey') || '';\n}\n\nfunction saveSettings() {\n  const provider = document.querySelector('.provider-card.active')?.dataset.provider || cfg.provider;\n  const keyInput = document.getElementById('cfgApiKey');\n  const modelInput = document.getElementById('cfgModel');\n  const baseURLInput = document.getElementById('cfgBaseURL');\n\n  cfg.provider = provider;\n  cfg.apiKey = keyInput ? keyInput.value.trim() : cfg.apiKey;\n  cfg.model = modelInput ? modelInput.value.trim() : cfg.model;\n  cfg.baseURL = baseURLInput ? baseURLInput.value.trim() : cfg.baseURL;\n\n  localStorage.setItem('dir_provider', cfg.provider);\n  localStorage.setItem('dir_model', cfg.model);\n  localStorage.setItem('dir_baseURL', cfg.baseURL);\n  \/\/ Store key in localStorage (user's choice \u2014 noted in UI)\n  if (cfg.apiKey) localStorage.setItem('dir_apiKey', cfg.apiKey);\n\n  updateProviderBadge();\n  closeSettings();\n  addChatMsg('director', `\u2705 Switched to **${PROVIDERS[cfg.provider]?.label || cfg.provider}** \u00b7 model: ${cfg.model}. Ready to direct your video!`);\n}\n\nfunction updateProviderBadge() {\n  const p = PROVIDERS[cfg.provider];\n  const badge = document.getElementById('providerBadgeLabel');\n  const role = document.getElementById('directorRoleLabel');\n  if (p && cfg.apiKey && cfg.provider !== 'ollama' || p && cfg.provider === 'ollama') {\n    badge.textContent = `${p.icon} ${p.label} \u00b7 ${cfg.model || p.defaultModel} \u2699`;\n    role.textContent = `${p.label} \u00b7 ${cfg.model || p.defaultModel}`;\n  } else {\n    badge.textContent = 'Configure API \u2699';\n    role.textContent = 'Configure API to activate';\n  }\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  SETTINGS MODAL\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction openSettings() {\n  loadSettings();\n  renderProviderGrid();\n  if (cfg.provider) selectProvider(cfg.provider, false);\n  document.getElementById('settingsModal').classList.add('open');\n}\nfunction closeSettings() {\n  document.getElementById('settingsModal').classList.remove('open');\n}\ndocument.getElementById('settingsModal').addEventListener('click', function(e) {\n  if (e.target === this) closeSettings();\n});\n\nfunction renderProviderGrid() {\n  const grid = document.getElementById('providerGrid');\n  grid.innerHTML = Object.entries(PROVIDERS).map(([key, p]) => `\n    <div class=\"provider-card ${cfg.provider === key ? 'active' : ''}\" data-provider=\"${key}\" onclick=\"selectProvider('${key}', true)\">\n      <div class=\"prov-icon\">${p.icon}<\/div>\n      <div class=\"prov-name\">${p.label}<\/div>\n      <div class=\"prov-tag\">${p.tag}<\/div>\n      ${p.free ? '<span class=\"prov-free\">Free Tier<\/span>' : ''}\n    <\/div>\n  `).join('');\n}\n\nfunction selectProvider(key, fromClick) {\n  document.querySelectorAll('.provider-card').forEach(c => c.classList.remove('active'));\n  const card = document.querySelector(`.provider-card[data-provider=\"${key}\"]`);\n  if (card) card.classList.add('active');\n\n  const p = PROVIDERS[key];\n  if (!p) return;\n\n  \/\/ Saved model for this provider, or default\n  const savedModel = (cfg.provider === key && cfg.model) ? cfg.model : p.defaultModel;\n\n  const configHTML = `\n    ${p.hasBaseURL ? `\n    <div class=\"config-section\">\n      <label class=\"config-label\">Base URL (Ollama server address)<\/label>\n      <input id=\"cfgBaseURL\" class=\"config-input\" type=\"text\" \n        value=\"${cfg.provider === key ? cfg.baseURL || p.defaultBaseURL : p.defaultBaseURL}\" \n        placeholder=\"${p.defaultBaseURL}\">\n    <\/div>` : ''}\n\n    ${key !== 'ollama' ? `\n    <div class=\"config-section\">\n      <label class=\"config-label\">API Key \u2014 <a href=\"${p.keyHelp}\" target=\"_blank\">Get yours here \u2197<\/a><\/label>\n      <input id=\"cfgApiKey\" class=\"config-input\" type=\"password\" \n        value=\"${cfg.provider === key ? cfg.apiKey : ''}\" \n        placeholder=\"${p.keyPlaceholder}\">\n    <\/div>` : `\n    <div class=\"config-section\">\n      <input id=\"cfgApiKey\" type=\"hidden\" value=\"ollama\">\n    <\/div>`}\n\n    <div class=\"config-section\">\n      <label class=\"config-label\">Model<\/label>\n      <input id=\"cfgModel\" class=\"config-input\" type=\"text\" \n        value=\"${savedModel}\" \n        placeholder=\"${p.defaultModel}\">\n      <div class=\"model-pills\">\n        ${p.models.map(m => `<button class=\"model-pill ${m === savedModel ? 'active' : ''}\" onclick=\"pickModel('${m}')\">${m}<\/button>`).join('')}\n      <\/div>\n    <\/div>\n\n    ${key === 'ollama' ? `<div class=\"note-box\">\ud83e\udd99 <strong>Ollama must be running locally.<\/strong> Install from <a href=\"https:\/\/ollama.ai\" target=\"_blank\">ollama.ai<\/a> and run <code style=\"background:var(--dim);padding:1px 4px;border-radius:3px;\">ollama serve<\/code>. No API key needed \u2014 completely free and private.<\/div>` : ''}\n    ${key === 'openrouter' ? `<div class=\"note-box\">\ud83d\udd00 <strong>OpenRouter<\/strong> gives access to 200+ models including free ones. Free models available: meta-llama\/llama-3.3-70b-instruct, google\/gemini-flash-1.5, and more. Check <a href=\"https:\/\/openrouter.ai\/models?q=free\" target=\"_blank\">openrouter.ai\/models<\/a> for the latest free options.<\/div>` : ''}\n    ${key === 'gemini' ? `<div class=\"note-box\">\ud83d\udd35 <strong>Google Gemini<\/strong> has a generous free tier via AI Studio. Get your free key at <a href=\"https:\/\/aistudio.google.com\/app\/apikey\" target=\"_blank\">aistudio.google.com<\/a> \u2014 no credit card required.<\/div>` : ''}\n    ${key === 'groq' ? `<div class=\"note-box\">\u26a1 <strong>Groq<\/strong> is blazing fast with free API access for LLaMA, Mixtral, and Gemma models. Perfect for rapid script generation.<\/div>` : ''}\n  `;\n\n  document.getElementById('providerConfig').innerHTML = configHTML;\n  resetStatus();\n}\n\nfunction pickModel(m) {\n  document.getElementById('cfgModel').value = m;\n  document.querySelectorAll('.model-pill').forEach(p => p.classList.remove('active'));\n  event.target.classList.add('active');\n}\n\nfunction resetStatus() {\n  document.getElementById('statusDot').className = 'status-indicator idle';\n  document.getElementById('statusText').textContent = 'Not tested';\n  document.getElementById('statusText').style.color = 'var(--muted)';\n}\n\nasync function testConnection() {\n  const btn = document.querySelector('.btn-test');\n  btn.textContent = 'Testing...';\n  btn.disabled = true;\n\n  const provider = document.querySelector('.provider-card.active')?.dataset.provider;\n  const keyInput = document.getElementById('cfgApiKey');\n  const modelInput = document.getElementById('cfgModel');\n  const baseURLInput = document.getElementById('cfgBaseURL');\n  const key = keyInput ? keyInput.value.trim() : '';\n  const model = modelInput ? modelInput.value.trim() : '';\n  const baseURL = baseURLInput ? baseURLInput.value.trim() : '';\n\n  const tempCfg = { provider, apiKey: key, model, baseURL };\n\n  try {\n    const result = await callAI('Say \"OK\" only.', '', tempCfg);\n    if (result) {\n      document.getElementById('statusDot').className = 'status-indicator ok';\n      document.getElementById('statusText').textContent = `Connected \u2713  \u2014  \"${result.substring(0,60)}\"`;\n      document.getElementById('statusText').style.color = 'var(--green)';\n    }\n  } catch(e) {\n    document.getElementById('statusDot').className = 'status-indicator err';\n    document.getElementById('statusText').textContent = `Error: ${e.message}`;\n    document.getElementById('statusText').style.color = 'var(--accent2)';\n  }\n  btn.textContent = 'Test Connection';\n  btn.disabled = false;\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  UNIVERSAL AI CALL\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nasync function callAI(userPrompt, systemPrompt = '', overrideCfg = null) {\n  const c = overrideCfg || cfg;\n  if (!c.provider) throw new Error('No provider configured. Click the badge in the header.');\n  \n  const p = PROVIDERS[c.provider];\n  if (!p) throw new Error('Unknown provider: ' + c.provider);\n\n  const model = c.model || p.defaultModel;\n  const key = c.apiKey;\n\n  \/\/ \u2500\u2500 ANTHROPIC \u2500\u2500\n  if (c.provider === 'anthropic') {\n    const res = await fetch('https:\/\/api.anthropic.com\/v1\/messages', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application\/json',\n        'x-api-key': key,\n        'anthropic-version': '2023-06-01',\n        'anthropic-dangerous-direct-browser-access': 'true',\n      },\n      body: JSON.stringify({\n        model,\n        max_tokens: 2048,\n        ...(systemPrompt ? { system: systemPrompt } : {}),\n        messages: [{ role: 'user', content: userPrompt }]\n      })\n    });\n    if (!res.ok) { const e = await res.json(); throw new Error(e.error?.message || res.statusText); }\n    const data = await res.json();\n    return data.content?.[0]?.text || '';\n  }\n\n  \/\/ \u2500\u2500 GOOGLE GEMINI \u2500\u2500\n  if (c.provider === 'gemini') {\n    const url = `https:\/\/generativelanguage.googleapis.com\/v1beta\/models\/${model}:generateContent?key=${key}`;\n    const body = {\n      contents: [{ parts: [{ text: (systemPrompt ? systemPrompt + '\\n\\n' : '') + userPrompt }] }],\n      generationConfig: { maxOutputTokens: 2048 }\n    };\n    const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application\/json' }, body: JSON.stringify(body) });\n    if (!res.ok) { const e = await res.json(); throw new Error(e.error?.message || res.statusText); }\n    const data = await res.json();\n    return data.candidates?.[0]?.content?.parts?.[0]?.text || '';\n  }\n\n  \/\/ \u2500\u2500 OLLAMA \u2500\u2500\n  if (c.provider === 'ollama') {\n    const baseURL = c.baseURL || 'http:\/\/localhost:11434';\n    const res = await fetch(`${baseURL}\/api\/chat`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify({\n        model,\n        messages: [\n          ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),\n          { role: 'user', content: userPrompt }\n        ],\n        stream: false\n      })\n    });\n    if (!res.ok) { const e = await res.text(); throw new Error(e || res.statusText); }\n    const data = await res.json();\n    return data.message?.content || '';\n  }\n\n  \/\/ \u2500\u2500 OPENAI-COMPATIBLE (OpenAI, DeepSeek, Groq, OpenRouter, Copilot) \u2500\u2500\n  let endpoint = p.endpoint;\n  const headers = { 'Content-Type': 'application\/json', 'Authorization': `Bearer ${key}` };\n\n  if (c.provider === 'openrouter') {\n    headers['HTTP-Referer'] = window.location.href;\n    headers['X-Title'] = 'DIRECTOR AI Cinematography Studio';\n  }\n  if (c.provider === 'copilot') {\n    headers['Copilot-Integration-Id'] = 'vscode-chat';\n  }\n\n  const body = {\n    model,\n    max_tokens: 2048,\n    messages: [\n      ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),\n      { role: 'user', content: userPrompt }\n    ]\n  };\n\n  const res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });\n  if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error?.message || res.statusText); }\n  const data = await res.json();\n  return data.choices?.[0]?.message?.content || '';\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  PLATFORM DATA\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst PLATFORMS = {\n  kling: { name:'Kling AI', color:'#a855f7', icon:'\ud83d\udfe3', type:'text-to-video', maxDur:10, strengths:['smooth motion','cinematic','character consistency'], bestFor:['hero shots','smooth product movement','landscape'], watermark:false, promptTip:'Use cinematic language: \"golden hour\", \"smooth dolly shot\", \"shallow depth of field\"' },\n  pika: { name:'Pika', color:'#f97316', icon:'\ud83d\udfe0', type:'text-to-video', maxDur:4, strengths:['creative effects','social media energy','stylized transitions'], bestFor:['social ad clips','quick transitions','product effects'], watermark:false, promptTip:'Describe the effect you want: \"zoom into product\", \"shatter effect\", \"liquid morph\"' },\n  hailuo: { name:'HaiLuo AI', color:'#22c55e', icon:'\ud83d\udfe2', type:'text-to-video', maxDur:6, strengths:['human movement','facial expressions','fast generation'], bestFor:['people scenes','lifestyle footage','emotional close-ups'], watermark:true, promptTip:'Great for: \"person smiling confidently\", \"couple walking on beach\", \"professional at desk\"' },\n  googlevids: { name:'Google Vids (Veo 3.1)', color:'#3b82f6', icon:'\ud83d\udd35', type:'text-to-video', maxDur:8, strengths:['highest quality','realism','complex prompts'], bestFor:['hero shots','important scenes','photorealistic product shots'], watermark:false, promptTip:'Save these 10\/month for your MOST important clips.' },\n  runway: { name:'Runway Gen-4 Turbo', color:'#ef4444', icon:'\ud83d\udd34', type:'text-to-video', maxDur:25, strengths:['industry benchmark','cinematic quality'], bestFor:['absolute hero shots','one cinematic sequence'], watermark:false, promptTip:'ONE-TIME CREDITS. Use for your single most important cinematic moment only.' },\n  heygen: { name:'HeyGen', color:'#ec4899', icon:'\ud83e\ude77', type:'avatar', maxDur:180, strengths:['professional talking head','realistic presenter'], bestFor:['main presenter segment','product explainer with spokesperson'], watermark:true, promptTip:'Use for your primary spokesperson\/presenter segments.' },\n  synthesia: { name:'Synthesia', color:'#8b5cf6', icon:'\ud83d\udfe4', type:'avatar', maxDur:600, strengths:['enterprise quality','multiple avatars'], bestFor:['corporate explainer','product walkthrough with presenter'], watermark:true, promptTip:'Best for corporate\/professional tone. Pick avatar that matches your audience.' },\n  invideo: { name:'InVideo AI', color:'#f59e0b', icon:'\ud83d\udfe1', type:'assembly', maxDur:600, strengths:['full video assembly','stock footage','auto voiceover'], bestFor:['quick full video drafts','B-roll explainers'], watermark:true, promptTip:'Best for: \"create a 60-second video about [product] for YouTube\".' },\n  arcade: { name:'Arcade', color:'#06b6d4', icon:'\ud83d\udd37', type:'assembly', maxDur:300, strengths:['screen recording','product demos','SaaS walkthroughs'], bestFor:['SaaS product demos','app walkthroughs'], watermark:true, promptTip:'Perfect for any software\/app product.' },\n  artlist: { name:'Artlist AI', color:'#7c3aed', icon:'\ud83c\udfb5', type:'audio', maxDur:300, strengths:['AI music','royalty-free','sound effects'], bestFor:['background music','mood-specific soundtracks'], watermark:false, promptTip:'Describe the mood: \"upbeat corporate\", \"cinematic tension\", \"feel-good acoustic\".' },\n  designsai: { name:'Designs.ai', color:'#0ea5e9', icon:'\ud83c\udfa8', type:'assembly', maxDur:180, strengths:['AI voiceover','logo maker','video maker'], bestFor:['AI voiceover for any video','brand assets'], watermark:true, promptTip:'Use primarily for AI VOICEOVER \u2014 generate voice track from your script.' },\n  flexclip: { name:'FlexClip', color:'#f43f5e', icon:'\u2702\ufe0f', type:'editor', maxDur:600, strengths:['online editor','templates','text animations'], bestFor:['splicing platform clips together','adding captions','final edit'], watermark:true, promptTip:'\u2b50 USE AS YOUR EDITOR. Import clips from Kling, Pika, HaiLuo \u2014 splice here.' },\n  hunyuan: { name:'Hunyuan Video', color:'#ff6b35', icon:'\ud83d\udc09', type:'text-to-video', maxDur:12, strengths:['high realism','open source','no watermark'], bestFor:['cinematic B-roll','realistic environments'], watermark:false, promptTip:'Run free at replicate.com\/tencent\/hunyuan-video. No watermark, rivals Runway.' },\n  comfyui: { name:'ComfyUI', color:'#84cc16', icon:'\ud83d\udda5\ufe0f', type:'local', maxDur:999, strengths:['unlimited free','full control','no watermark'], bestFor:['unlimited generation','custom styles','overnight batch rendering'], watermark:false, promptTip:'\ud83d\udda5\ufe0f LOCAL POWERHOUSE. Install once, generate forever free.' },\n};\n\nfunction routeScene(scene) {\n  const s = (scene.type + ' ' + scene.desc).toLowerCase();\n  if (s.includes('presenter') || s.includes('talking head') || s.includes('spokesperson')) return { platform:'heygen', reason:'Best free talking-head presenter' };\n  if (s.includes('corporate') || s.includes('enterprise')) return { platform:'synthesia', reason:'Enterprise-grade avatar quality' };\n  if (s.includes('app') || s.includes('software') || s.includes('screen') || s.includes('saas')) return { platform:'arcade', reason:'Built for product\/software demos' };\n  if (s.includes('blog') || s.includes('article') || s.includes('repurpose')) return { platform:'pictory', reason:'Turns articles into videos automatically' };\n  if (s.includes('full video') || s.includes('assembled') || s.includes('stock')) return { platform:'invideo', reason:'Auto-assembles full video' };\n  if (s.includes('edit') || s.includes('splice') || s.includes('caption')) return { platform:'flexclip', reason:'Best free online editor' };\n  if (s.includes('music') || s.includes('audio') || s.includes('soundtrack')) return { platform:'artlist', reason:'AI music & sound effects' };\n  if (s.includes('voiceover') || s.includes('narration')) return { platform:'designsai', reason:'AI voiceover generation' };\n  if (s.includes('hero') || s.includes('establishing') || s.includes('epic')) return { platform:'googlevids', reason:'Highest quality \u2014 save for hero shots' };\n  if (s.includes('person') || s.includes('human') || s.includes('face') || s.includes('lifestyle')) return { platform:'hailuo', reason:'Best free platform for human movement' };\n  if (s.includes('effect') || s.includes('social') || s.includes('tiktok') || s.includes('quick')) return { platform:'pika', reason:'Best for social ad effects' };\n  if (s.includes('cinematic') || s.includes('dramatic') || s.includes('premium')) return { platform:'hunyuan', reason:'Open-source cinematic quality, no watermark' };\n  if (scene.duration <= 4) return { platform:'pika', reason:'Short clip \u2014 Pika is fastest' };\n  if (scene.duration <= 6) return { platform:'hailuo', reason:'Up to 6s, fast generation' };\n  return { platform:'kling', reason:'Best all-around motion quality' };\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  STATE &#038; TABS\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet scenes = [];\n\nfunction switchTab(id, btn) {\n  document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));\n  document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));\n  document.getElementById('tab-' + id).classList.add('active');\n  btn.classList.add('active');\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  SCRIPT\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst scriptInput = document.getElementById('scriptInput');\nscriptInput.addEventListener('input', updateScriptStats);\n\nfunction updateScriptStats() {\n  const words = scriptInput.value.trim().split(\/\\s+\/).filter(Boolean).length;\n  const estScenes = Math.max(1, Math.round(words \/ 25));\n  document.getElementById('scriptStats').textContent = `${words} words \u00b7 ~${estScenes} scenes estimated`;\n}\n\nfunction clearScript() { scriptInput.value = ''; updateScriptStats(); }\n\nasync function generateScript() {\n  if (!checkConfigured()) return;\n  const format = document.getElementById('vidFormat').value;\n  const tone = document.getElementById('vidTone').value;\n  const length = document.getElementById('vidLength').value;\n  const product = document.getElementById('vidProduct').value || 'your product';\n  addChatMsg('director', `Generating a ${length} ${format} script for **${product}** with a ${tone} tone...`);\n  showTyping();\n  try {\n    const result = await callAI(\n      `Write a complete, production-ready video script.\\nFormat: ${format}\\nTone: ${tone}\\nLength: ${length}\\nProduct: ${product}\\n\\nUse scene markers like:\\n[SCENE 1 - HOOK - 3s]\\nInclude voiceover, visual direction, and shot description for each scene. Make it conversion-focused and highly engaging.`,\n      'You are an expert video ad copywriter and director.'\n    );\n    scriptInput.value = result;\n    updateScriptStats();\n    removeTyping();\n    addChatMsg('director', `Script generated! \u2705 Click **Analyze & Route Scenes** to assign each scene to the perfect platform.`);\n  } catch(e) {\n    removeTyping();\n    addChatMsg('director', `\u274c Error: ${e.message}`);\n  }\n}\n\nasync function analyzeScript() {\n  if (!checkConfigured()) return;\n  const script = scriptInput.value.trim();\n  if (!script) { addChatMsg('director', 'Write or generate a script first!'); return; }\n  showTyping();\n  setProgress(20);\n  try {\n    const result = await callAI(\n      `Analyze this video script and break it into production scenes.\\n\\nSCRIPT:\\n${script}\\n\\nReturn ONLY a JSON array of scenes:\\n[{\"num\":1,\"type\":\"Hook|Problem|Solution|Demo|Testimonial|CTA|Transition|B-Roll|Presenter|Effect\",\"desc\":\"Visual description\",\"voiceover\":\"Exact words spoken\",\"duration\":3,\"mood\":\"Cinematic|Energetic|Calm|Dramatic|Conversational\",\"shotType\":\"Wide|Medium|Close-up|Extreme Close-up|Aerial|Product|Motion\"}]\\n\\nReturn ONLY the JSON array, nothing else.`,\n      'You are an expert video director. Return only valid JSON.'\n    );\n    let raw = result.replace(\/```json|```\/g, '').trim();\n    scenes = JSON.parse(raw);\n    scenes = scenes.map(s => { const r = routeScene(s); return { ...s, platform: r.platform, routingReason: r.reason }; });\n    removeTyping();\n    setProgress(70);\n    renderScenes();\n    renderTimeline();\n    setProgress(100);\n    const total = scenes.reduce((sum, s) => sum + (s.duration || 3), 0);\n    addChatMsg('director', `\ud83c\udfac Routed **${scenes.length} scenes** (${total}s total) across ${new Set(scenes.map(s=>s.platform)).size} platforms. Check the **Scene Router** tab.`);\n    setTimeout(() => setProgress(0), 1500);\n    switchTab('router', document.querySelectorAll('.tab-btn')[1]);\n  } catch(e) {\n    removeTyping();\n    setProgress(0);\n    addChatMsg('director', `\u274c Error parsing scenes: ${e.message}. Try using clearer scene breaks in your script.`);\n  }\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  RENDER SCENES\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction renderScenes() {\n  const container = document.getElementById('sceneContainer');\n  document.getElementById('routerEmpty').style.display = scenes.length === 0 ? 'flex' : 'none';\n  container.querySelectorAll('.scene-block').forEach(el => el.remove());\n  scenes.forEach((scene, i) => {\n    const p = PLATFORMS[scene.platform] || PLATFORMS.kling;\n    const block = document.createElement('div');\n    block.className = 'scene-block routed';\n    block.style.setProperty('--platform-color', p.color);\n    block.innerHTML = `\n      <div class=\"scene-header\">\n        <div class=\"scene-num\">SCENE ${scene.num || i+1}<\/div>\n        <div class=\"scene-type-badge\" style=\"background:${p.color}22;color:${p.color}\">${scene.type||'General'}<\/div>\n        <div style=\"font-size:10px;color:var(--muted)\">${scene.shotType||'Shot'}<\/div>\n        <div class=\"scene-dur\">${scene.duration||3}s<\/div>\n      <\/div>\n      <div class=\"scene-body\">\n        <div class=\"scene-desc\">${scene.desc||''}<\/div>\n        ${scene.voiceover ? `<div style=\"font-size:11px;color:var(--muted);font-style:italic;margin-bottom:8px;padding:6px 8px;border-left:2px solid var(--border);\">\"${scene.voiceover}\"<\/div>` : ''}\n        <div class=\"scene-assignment\">\n          <div class=\"assignment-label\">Routed to<\/div>\n          <div class=\"assigned-platform\" style=\"color:${p.color}\">${p.icon} ${p.name}<\/div>\n          <div class=\"assignment-reason\">\u2014 ${scene.routingReason||''}<\/div>\n          <select class=\"reroute-select\" style=\"margin-left:auto\" onchange=\"rerouteScene(${i}, this.value)\">\n            ${Object.entries(PLATFORMS).map(([k,v]) => `<option value=\"${k}\" ${k===scene.platform?'selected':''}>${v.name}<\/option>`).join('')}\n          <\/select>\n        <\/div>\n        ${p.watermark ? '<div class=\"watermark-note\" style=\"margin-top:6px\">\u26a0 Free tier includes watermark<\/div>' : ''}\n        <div class=\"scene-actions\">\n          <button class=\"btn-sm\" onclick=\"copyPrompt(${i})\">Copy Prompt<\/button>\n          <button class=\"btn-sm\" onclick=\"openPlatform(${i})\">Open Platform \u2197<\/button>\n          <button class=\"btn-sm generate-btn\" onclick=\"getPromptTip(${i})\">Prompt Tips<\/button>\n        <\/div>\n      <\/div>`;\n    container.appendChild(block);\n  });\n  const total = scenes.reduce((sum, s) => sum + (s.duration||3), 0);\n  document.getElementById('routerStats').textContent = `${scenes.length} scenes \u00b7 ${total}s total \u00b7 ${[...new Set(scenes.map(s=>s.platform))].length} platforms used`;\n}\n\nfunction rerouteScene(i, platform) { scenes[i].platform = platform; scenes[i].routingReason = 'Manually selected'; renderScenes(); renderTimeline(); }\n\nfunction copyPrompt(i) {\n  const scene = scenes[i]; const p = PLATFORMS[scene.platform];\n  navigator.clipboard.writeText(`${scene.desc}. ${scene.mood||'Cinematic'} mood. ${scene.shotType||'Medium shot'}. Duration: ${scene.duration}s. ${p?.promptTip||''}`);\n  addChatMsg('director', `\u2705 Prompt copied for Scene ${scene.num}! Paste it into ${p?.name}.`);\n}\n\nconst platformURLs = { kling:'https:\/\/klingai.com', pika:'https:\/\/pika.art', hailuo:'https:\/\/hailuoai.video', googlevids:'https:\/\/vids.google.com', runway:'https:\/\/runwayml.com', heygen:'https:\/\/www.heygen.com', synthesia:'https:\/\/www.synthesia.io', invideo:'https:\/\/invideo.io', arcade:'https:\/\/www.arcade.software', artlist:'https:\/\/artlist.io\/ai-music-generator', designsai:'https:\/\/designs.ai', flexclip:'https:\/\/www.flexclip.com', hunyuan:'https:\/\/replicate.com\/tencent\/hunyuan-video', comfyui:'https:\/\/github.com\/comfyanonymous\/ComfyUI' };\n\nfunction openPlatform(i) { const url = platformURLs[scenes[i].platform]; if (url) window.open(url, '_blank'); }\nfunction getPromptTip(i) { const scene = scenes[i]; const p = PLATFORMS[scene.platform]; addChatMsg('director', `\ud83d\udca1 **${p.name} Tips for Scene ${scene.num}:**\\n\\n${p.promptTip}\\n\\nFor your scene: \"${scene.desc}\" \u2192 try: \"${scene.desc}. ${scene.mood} atmosphere. ${scene.shotType}. ${scene.duration} seconds.\"`); }\n\nfunction addManualScene() {\n  scenes.push({ num: scenes.length+1, type:'General', desc:'New scene \u2014 describe what you see', duration:4, mood:'Cinematic', shotType:'Medium', platform:'kling', routingReason:'Default' });\n  renderScenes(); renderTimeline();\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  STORYBOARD\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction buildStoryboard() {\n  const grid = document.getElementById('storyboardGrid');\n  grid.innerHTML = '';\n  if (scenes.length === 0) { grid.innerHTML = '<div class=\"empty-state\" style=\"grid-column:1\/-1\"><div class=\"empty-icon\">\ud83d\uddbc\ufe0f<\/div><div class=\"empty-msg\">No scenes to storyboard<\/div><\/div>'; return; }\n  const icons = { Hook:'\ud83c\udfaf', Problem:'\ud83d\ude24', Solution:'\u2728', Demo:'\ud83d\udcf1', Testimonial:'\ud83d\udc64', CTA:'\ud83d\ude80', Transition:'\u27a1\ufe0f', 'B-Roll':'\ud83c\udfac', Presenter:'\ud83d\udde3\ufe0f', Effect:'\u26a1' };\n  scenes.forEach((scene, i) => {\n    const p = PLATFORMS[scene.platform] || PLATFORMS.kling;\n    const card = document.createElement('div');\n    card.className = 'board-card';\n    card.onclick = () => addChatMsg('director', `Scene ${scene.num}: \"${scene.desc}\" \u2192 ${p.name}. ${p.promptTip}`);\n    card.innerHTML = `<div class=\"board-frame\" style=\"background:linear-gradient(135deg,${p.color}22,${p.color}44)\"><span style=\"font-size:36px\">${icons[scene.type]||'\ud83c\udfac'}<\/span><div class=\"board-platform-badge\" style=\"color:${p.color}\">${p.icon} ${p.name.split(' ')[0]}<\/div><\/div><div class=\"board-info\"><div class=\"board-title\">Scene ${scene.num} \u00b7 ${scene.duration}s<\/div><div class=\"board-desc\">${(scene.desc||'').substring(0,60)}${(scene.desc||'').length>60?'...':''}<\/div><\/div>`;\n    grid.appendChild(card);\n  });\n  switchTab('storyboard', document.querySelectorAll('.tab-btn')[2]);\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  TIMELINE\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction renderTimeline() {\n  const track = document.getElementById('timelineTrack');\n  track.innerHTML = '';\n  if (scenes.length === 0) { track.innerHTML = '<div style=\"font-size:11px;color:var(--muted);padding:20px;\">Add scenes to see timeline<\/div>'; return; }\n  let total = 0;\n  scenes.forEach((scene, i) => {\n    const p = PLATFORMS[scene.platform] || PLATFORMS.kling;\n    const dur = scene.duration || 3;\n    total += dur;\n    if (i > 0) { const t = document.createElement('div'); t.className = 'tl-transition'; t.textContent = '\u25b7'; track.appendChild(t); }\n    const clip = document.createElement('div');\n    clip.className = 'tl-clip';\n    clip.style.setProperty('--clip-color', p.color);\n    clip.style.minWidth = Math.max(60, dur * 12) + 'px';\n    clip.innerHTML = `<div class=\"tl-label\">S${scene.num} \u00b7 ${scene.type}<\/div><div class=\"tl-plat\" style=\"color:${p.color}\">${p.icon} ${p.name.split(' ')[0]}<\/div><div class=\"tl-dur\">${dur}s<\/div>`;\n    track.appendChild(clip);\n  });\n  document.getElementById('totalDur').textContent = `Total: ${total}s (${(total\/60).toFixed(1)}min)`;\n  document.getElementById('exportStats').textContent = `${scenes.length} clips \u00b7 ${total}s \u00b7 ${[...new Set(scenes.map(s=>s.platform))].length} platforms`;\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  CHAT\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst DIRECTOR_SYSTEM = `You are DIRECTOR, an elite AI Cinematographer and Video Production Director. You are a master of all free-tier AI video platforms:\n\n- Kling AI: ~66 credits\/day, up to 10s, best for smooth motion & character consistency\n- Pika: ~150 signup credits + daily, 3-4s, best for stylized effects & social ads\n- HaiLuo AI: Several free\/day, up to 6s, best for human movement & faces, has watermark\n- Google Vids (Veo 3.1): 10 videos\/month, ~8s each, highest quality, use for hero shots\n- Runway Gen-4 Turbo: 125 ONE-TIME credits, industry leader\n- HeyGen: 3 videos\/month up to 3min, best talking-head presenter\n- Synthesia: 10 min\/month, 9 stock avatars, enterprise quality\n- InVideo AI: 10 min AI gen\/week, auto-assembles full videos\n- Arcade: 200 AI credits\/month, product\/SaaS demos\n\nGive expert, specific, actionable advice. Be direct and confident like a real film director.`;\n\nconst QUICK_PROMPTS = {\n  hook: 'Give me 5 different high-converting video hooks I can use in the first 3 seconds for a social media ad',\n  cta: 'Write 5 powerful call-to-action scripts for the end of a video ad',\n  platform: 'Which platform should I use for each scene type? Break down when to use Kling vs Pika vs HaiLuo vs Google Vids vs Runway',\n  shots: 'Give me a shot list template for a 30-second product ad with platform assignments for each shot',\n  free: 'How do I maximize my free credits across all platforms to produce the longest, highest quality video possible?',\n  voice: 'Give me tips for writing better voiceover scripts that sound natural when read by AI text-to-speech'\n};\n\nfunction quickPrompt(key) { document.getElementById('chatInputField').value = QUICK_PROMPTS[key]; sendChat(); }\n\nasync function sendChat() {\n  if (!checkConfigured()) return;\n  const input = document.getElementById('chatInputField');\n  const msg = input.value.trim();\n  if (!msg) return;\n  input.value = '';\n  addChatMsg('user', msg);\n  showTyping();\n  try {\n    const reply = await callAI(msg, DIRECTOR_SYSTEM);\n    removeTyping();\n    addChatMsg('director', reply);\n  } catch(e) {\n    removeTyping();\n    addChatMsg('director', `\u274c ${e.message}`);\n  }\n}\n\nfunction handleChatKey(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } }\n\nfunction addChatMsg(role, text) {\n  const container = document.getElementById('chatMessages');\n  const div = document.createElement('div');\n  div.className = `msg msg-${role}`;\n  div.innerHTML = `<div class=\"msg-label\">${role === 'director' ? 'Director' : 'You'}<\/div>${text.replace(\/\\*\\*(.*?)\\*\\*\/g, '<strong>$1<\/strong>').replace(\/\\n\/g, '<br>')}`;\n  container.appendChild(div);\n  container.scrollTop = container.scrollHeight;\n}\n\nfunction showTyping() {\n  const container = document.getElementById('chatMessages');\n  const div = document.createElement('div');\n  div.id = 'typingIndicator';\n  div.className = 'msg msg-director typing-indicator';\n  div.innerHTML = '<div class=\"dot\"><\/div><div class=\"dot\"><\/div><div class=\"dot\"><\/div>';\n  container.appendChild(div);\n  container.scrollTop = container.scrollHeight;\n}\nfunction removeTyping() { const el = document.getElementById('typingIndicator'); if (el) el.remove(); }\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  UTILITIES\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction setProgress(pct) { document.getElementById('progressFill').style.width = pct + '%'; }\n\nfunction checkConfigured() {\n  if (!cfg.provider) {\n    openSettings();\n    return false;\n  }\n  if (cfg.provider !== 'ollama' && !cfg.apiKey) {\n    openSettings();\n    addChatMsg('director', `\u2699 Please enter your API key for ${PROVIDERS[cfg.provider]?.label || cfg.provider} in the settings.`);\n    return false;\n  }\n  return true;\n}\n\nfunction selectPlatform(key) {\n  document.querySelectorAll('.platform-card').forEach(c => c.classList.remove('selected'));\n  event.currentTarget.classList.add('selected');\n  const p = PLATFORMS[key];\n  if (!p) return;\n  addChatMsg('director', `**${p.name}** \u2014 ${p.type}\\n\\nBest for: ${p.bestFor.join(', ')}\\n\\n\ud83d\udca1 ${p.promptTip}${p.watermark ? '\\n\\n\u26a0 Free tier includes watermark' : ''}`);\n}\n\nfunction copyDirectorSheet() {\n  if (!scenes.length) return;\n  const lines = ['DIRECTOR PRODUCTION SHEET', '='.repeat(40), ''];\n  scenes.forEach(s => {\n    const p = PLATFORMS[s.platform];\n    lines.push(`SCENE ${s.num} \u2014 ${s.type} (${s.duration}s)`, `Platform: ${p?.name}`, `Visual: ${s.desc}`, s.voiceover ? `VO: \"${s.voiceover}\"` : '', `Open: ${platformURLs[s.platform]||''}`, '');\n  });\n  navigator.clipboard.writeText(lines.join('\\n'));\n  addChatMsg('director', '\u2705 Director sheet copied to clipboard!');\n}\n\nfunction exportPlan() {\n  if (!scenes.length) return;\n  const plan = scenes.map(s => { const p = PLATFORMS[s.platform]; return `SCENE ${s.num} (${s.duration}s) \u2014 ${s.type}\\nPlatform: ${p?.name}\\nVisual: ${s.desc}\\nVO: ${s.voiceover||'None'}\\nOpen: ${platformURLs[s.platform]||''}\\n`; }).join('\\n');\n  const a = document.createElement('a');\n  a.href = URL.createObjectURL(new Blob([plan], { type: 'text\/plain' }));\n  a.download = 'director-production-plan.txt';\n  a.click();\n  addChatMsg('director', '\ud83d\udcc4 Production plan exported!');\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/  INIT\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nloadSettings();\nupdateProviderBadge();\n\n\/\/ Show configure prompt if no provider set\nif (!cfg.provider || (!cfg.apiKey && cfg.provider !== 'ollama')) {\n  setTimeout(() => {\n    const messages = document.getElementById('chatMessages');\n    const banner = document.createElement('div');\n    banner.className = 'no-key-banner';\n    banner.onclick = openSettings;\n    banner.innerHTML = '\u2699 No AI provider configured \u2014 click here to set up your API key';\n    messages.parentElement.insertBefore(banner, messages);\n  }, 500);\n}\n<\/script>\n<\/body>\n<\/html>\n<\/div>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>DIRECTOR \u2014 The AI Cinematography Studio That Costs You Nothing Most people think professional video production requires a budget. DIRECTOR proves them wrong. DIRECTOR is the first AI-powered video production studio that doesn&#8217;t just help you write scripts \u2014 it thinks like a real cinematographer. It analyzes your content, breaks it into scenes, and intelligently [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_themeisle_gutenberg_block_has_review":false,"footnotes":""},"class_list":["post-14","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages\/14","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=14"}],"version-history":[{"count":3,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages\/14\/revisions"}],"predecessor-version":[{"id":20,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages\/14\/revisions\/20"}],"wp:attachment":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=14"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}