motionrender 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +11 -11
- package/dist/types/packages/cli/src/ai-studio-page.d.ts +1 -1
- package/dist/types/packages/cli/src/ai.d.ts +1 -1
- package/package.json +1 -1
- package/skills/ove/SKILL.md +1 -1
- package/skills/ove-agent/SKILL.md +1 -1
- package/studio/assets/{index-BGdqA-kT.js → index-B1JV-2Jw.js} +3 -3
- package/studio/index.html +1 -1
package/dist/cli.js
CHANGED
|
@@ -3400,7 +3400,7 @@ var init_providers = __esm({
|
|
|
3400
3400
|
split: { by: "words", stagger: 4, preset: "rise" },
|
|
3401
3401
|
animations: [{ property: "opacity", keyframes: [{ frame: 0, value: 0 }, { frame: 18, value: 1, easing: "land" }] }]
|
|
3402
3402
|
},
|
|
3403
|
-
{ id: "sub", type: "text", content: "Set
|
|
3403
|
+
{ id: "sub", type: "text", content: "Set FREELLMAPI_API_KEY for real output.", x: width / 2, y: height * 0.74, fontFamily: "Inter", fontSize: Math.round(width * 0.03), color: "#97a0b6", textAlign: "center", maxWidth: width * 0.8 }
|
|
3404
3404
|
];
|
|
3405
3405
|
const camera = (frames) => ({ x: width / 2, y: height / 2, zoom: 1, animations: [{ property: "zoom", keyframes: [{ frame: 0, value: 1 }, { frame: frames, value: 1.06, easing: "drift" }] }] });
|
|
3406
3406
|
const glow = layers[0], title = { ...layers[1], split: { by: "words", stagger: 4, preset: "rise" } }, sub = layers[2];
|
|
@@ -8839,7 +8839,7 @@ function confirmBox(text, ok) { var m = h('div', { class: 'modal' }, h('div', nu
|
|
|
8839
8839
|
function updateStatusBadge() {
|
|
8840
8840
|
api('/api/status').then(function (s) {
|
|
8841
8841
|
var ptext = $('#pbadgetext'); if (!ptext) return;
|
|
8842
|
-
if (s.provider) ptext.textContent = (s.provider.id === '
|
|
8842
|
+
if (s.provider) ptext.textContent = (s.provider.id === 'freellmapi' ? '\u{1F680} FreeLLMAPI \xB7 ' : '\u2728 ' + s.provider.id + ' \xB7 ') + s.provider.model;
|
|
8843
8843
|
else ptext.textContent = '\u{1F4CB} Paste JSON mode';
|
|
8844
8844
|
}, function () {});
|
|
8845
8845
|
}
|
|
@@ -9077,7 +9077,7 @@ function create() {
|
|
|
9077
9077
|
});
|
|
9078
9078
|
|
|
9079
9079
|
contentPaste.append(
|
|
9080
|
-
h('div', { class: 'box-info', text: 'Zero-key workflow: Copy the official schema prompt into ChatGPT, Claude or
|
|
9080
|
+
h('div', { class: 'box-info', text: 'Zero-key workflow: Copy the official schema prompt into ChatGPT, Claude or FreeLLMAPI chat, then paste the returned JSON below.' }),
|
|
9081
9081
|
h('h2', { style: 'margin-top:16px', text: 'Step 1: Get AI Chat Prompt' }),
|
|
9082
9082
|
h('div', { class: 'two' }, [
|
|
9083
9083
|
h('label', { class: 'f' }, [h('span', { text: 'Format' }), pasteKind]),
|
|
@@ -9530,7 +9530,7 @@ function settings() {
|
|
|
9530
9530
|
page.appendChild(h('label', { class: 'f' }, [h('span', { text: r[0] }), h('input', { value: r[1], readonly: true })]));
|
|
9531
9531
|
});
|
|
9532
9532
|
if (s.error) {
|
|
9533
|
-
page.appendChild(h('div', { class: 'err', text: s.error + String.fromCharCode(10) + 'Tip: Add
|
|
9533
|
+
page.appendChild(h('div', { class: 'err', text: s.error + String.fromCharCode(10) + 'Tip: Add FREELLMAPI_API_KEY=... in your .env file, or use the zero-key Paste JSON workflow!' }));
|
|
9534
9534
|
} else {
|
|
9535
9535
|
page.appendChild(h('div', { class: 'note', text: 'Connected to ' + pinfo + '. AI generation and refinements will execute directly through this provider.' }));
|
|
9536
9536
|
}
|
|
@@ -9785,7 +9785,7 @@ async function runServe(env = process.env) {
|
|
|
9785
9785
|
makeThumbnail: async () => {
|
|
9786
9786
|
throw new Error("Thumbnails are drawn by the render workers");
|
|
9787
9787
|
},
|
|
9788
|
-
providerState: () => ({ provider: { id: "
|
|
9788
|
+
providerState: () => ({ provider: { id: "freellmapi", model: env.FREELLMAPI_MODEL ?? env.AI_MODEL ?? "auto", baseUrl: env.FREELLMAPI_BASE_URL ?? "https://ai.hamathopc.in/v1", capabilities: { chat: true, speech: true, transcription: true, image: true } }, error: null }),
|
|
9789
9789
|
// 127.0.0.1 and localhost are listed so a container health check can ask; they cannot be reached by a web page on another site.
|
|
9790
9790
|
allowHosts: [publicHost, "127.0.0.1", "localhost"],
|
|
9791
9791
|
publicOrigin
|
|
@@ -10430,7 +10430,7 @@ Options:
|
|
|
10430
10430
|
--fps <number> Frames per second (default: 30)
|
|
10431
10431
|
--no-render Generate and compile React code only; skip MP4 rendering
|
|
10432
10432
|
--no-plan skip the planner and always run the React agent loop
|
|
10433
|
-
--provider <name> AI provider:
|
|
10433
|
+
--provider <name> AI provider: freellmapi, mock (default: auto-detected)
|
|
10434
10434
|
--model <id> Model ID override
|
|
10435
10435
|
|
|
10436
10436
|
Examples:
|
|
@@ -10999,9 +10999,9 @@ function doctorCommand(argv2) {
|
|
|
10999
10999
|
console.log(`Capabilities ${Object.entries(capabilities).filter(([, on]) => on).map(([name2]) => name2).join(", ")}`);
|
|
11000
11000
|
console.log(`
|
|
11001
11001
|
${describeProvider(provider)}`);
|
|
11002
|
-
if (!capabilities.speech) console.log(" Narration needs speech:
|
|
11003
|
-
if (!capabilities.image) console.log(" Images need a provider with image;
|
|
11004
|
-
console.log("\nSet
|
|
11002
|
+
if (!capabilities.speech) console.log(" Narration needs speech: FreeLLMAPI provides native speech synthesis.");
|
|
11003
|
+
if (!capabilities.image) console.log(" Images need a provider with image; FreeLLMAPI provides native image generation.");
|
|
11004
|
+
console.log("\nSet FREELLMAPI_API_KEY in .env, or pass --provider.");
|
|
11005
11005
|
return 0;
|
|
11006
11006
|
}
|
|
11007
11007
|
async function capabilitiesCommand(argv2) {
|
|
@@ -11118,8 +11118,8 @@ var init_ai = __esm({
|
|
|
11118
11118
|
[--duration 18] [--fps 30] [render options]
|
|
11119
11119
|
ove ai studio [--port 3100] [--out ai-videos] open a local page for the AI commands in a browser
|
|
11120
11120
|
|
|
11121
|
-
Provider options (all commands): --provider
|
|
11122
|
-
Environment: OVE_AI_PROVIDER, OVE_AI_MODEL, OVE_AI_API_KEY,
|
|
11121
|
+
Provider options (all commands): --provider freellmapi|mock, --model name, --base-url URL
|
|
11122
|
+
Environment: OVE_AI_PROVIDER, OVE_AI_MODEL, OVE_AI_API_KEY, FREELLMAPI_API_KEY, and FREELLMAPI_BASE_URL.
|
|
11123
11123
|
Render options: --crf, --bitrate, --audio-bitrate, --scale, --loudness, --concurrency, --preset, --codec, --format`;
|
|
11124
11124
|
option3 = (argv2, name2) => {
|
|
11125
11125
|
const index = argv2.indexOf(name2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const aiStudioPage = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>MotionRender | OVE AI studio</title>\n<style>\n:root{\n --bg:#07090e;\n --bg-gradient:radial-gradient(circle at 50% -10%,rgba(45,212,191,0.08) 0%,transparent 55%),radial-gradient(circle at 95% 15%,rgba(129,140,248,0.06) 0%,transparent 45%),#07090e;\n --panel:rgba(15,22,34,0.78);\n --panel-solid:#0f1622;\n --panel2:rgba(10,15,24,0.88);\n --panel2-solid:#0a0f18;\n --line:rgba(255,255,255,0.08);\n --line-hover:rgba(255,255,255,0.16);\n --text:#f8fafc;\n --muted:#94a3b8;\n --dim:#64748b;\n --accent:#2dd4bf;\n --accent-glow:rgba(45,212,191,0.25);\n --accent2:#818cf8;\n --ok:#34d399;\n --warn:#fbbf24;\n --err:#fb7185;\n --radius:12px;\n --radius-sm:8px;\n}\n*{box-sizing:border-box}html,body{height:100%}\nbody{margin:0;background:var(--bg-gradient);background-attachment:fixed;color:var(--text);font:14px/1.55 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;letter-spacing:-.01em;display:flex;flex-direction:column;overflow:hidden;-webkit-font-smoothing:antialiased}\n::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:999px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.24)}\na{color:var(--accent);text-decoration:none;transition:color .15s}a:hover{filter:brightness(1.15)}\nheader.top{display:flex;align-items:center;gap:16px;padding:0 22px;height:58px;border-bottom:1px solid var(--line);background:rgba(9,13,20,0.85);backdrop-filter:blur(20px);flex:none;z-index:10}\n.brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em}\n.mark{font:800 11px/1 ui-monospace,Consolas,monospace;letter-spacing:.08em;color:#04211e;background:linear-gradient(135deg,#2dd4bf,#06b6d4);padding:6px 8px;border-radius:6px;box-shadow:0 0 12px var(--accent-glow)}\n.vpill{font-size:11px;font-weight:600;color:var(--muted);background:rgba(255,255,255,0.05);padding:2px 8px;border-radius:999px;border:1px solid var(--line)}\nnav.main{display:flex;gap:3px;background:rgba(255,255,255,0.03);padding:3px;border-radius:10px;border:1px solid var(--line)}\nnav.main a{color:var(--muted);padding:5px 13px;border-radius:7px;font-size:13px;font-weight:500;transition:all .15s}\nnav.main a:hover{color:var(--text);background:rgba(255,255,255,0.05)}\nnav.main a.on{color:#fff;background:rgba(255,255,255,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.3)}\n.grow{flex:1}\n.pbadge{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);background:rgba(255,255,255,0.03);border:1px solid var(--line);padding:4px 10px;border-radius:999px;text-decoration:none;transition:all .15s}\n.pbadge:hover{border-color:var(--line-hover);color:var(--text)}\n.pdot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok)}\n.busy{display:none;align-items:center;gap:9px;color:var(--muted);font-size:12.5px;max-width:320px;background:rgba(255,255,255,0.04);border:1px solid var(--line);padding:4px 11px;border-radius:999px}\n.busy.on{display:flex}.busy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.spin{width:12px;height:12px;border:2px solid rgba(255,255,255,0.15);border-top-color:var(--accent);border-radius:50%;animation:rot .8s linear infinite;flex:none}@keyframes rot{to{transform:rotate(360deg)}}\n#app{flex:1;min-height:0;overflow:auto}\n.page{max-width:1160px;margin:0 auto;padding:32px 24px 64px}\n.hero{margin-bottom:28px;display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}\nh1{margin:0 0 6px;font-size:26px;font-weight:700;letter-spacing:-.03em;color:#fff}\nh2{margin:0 0 10px;font-size:16px;font-weight:600;letter-spacing:-.01em}\n.lead{color:var(--muted);margin:0;font-size:14px}\nbutton{font:inherit;cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}\n.btn{background:linear-gradient(135deg,#2dd4bf 0%,#14b8a6 100%);color:#03211c;border:0;border-radius:8px;padding:8px 16px;font-weight:600;font-size:13.5px;box-shadow:0 2px 10px -2px rgba(45,212,191,0.4);transition:all .15s;display:inline-flex;align-items:center;justify-content:center;gap:7px}\n.btn:hover{filter:brightness(1.08);box-shadow:0 4px 16px -2px rgba(45,212,191,0.55);transform:translateY(-1px)}\n.btn:active{transform:translateY(0)}\n.btn.sec{background:rgba(255,255,255,0.05);color:var(--text);border:1px solid var(--line);font-weight:500;box-shadow:none}\n.btn.sec:hover{background:rgba(255,255,255,0.09);border-color:var(--line-hover);filter:none}\n.btn.sm{padding:5px 11px;font-size:12.5px;border-radius:6px}\n.btn.danger{color:#fda4af}.btn.danger:hover{background:rgba(244,63,94,0.15);border-color:rgba(244,63,94,0.3)}\ninput,textarea,select{width:100%;background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:9px 12px;font:inherit;transition:border-color .15s,box-shadow .15s}\ntextarea{resize:vertical;min-height:96px;font-size:13.5px}\ninput:focus,textarea:focus,select:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}\ninput[type=color]{padding:2px;height:36px;width:64px;border-radius:6px}input[type=checkbox]{width:auto}\nlabel.f{display:block;margin:0 0 16px}label.f>span{display:block;color:var(--muted);font-size:12.5px;font-weight:500;margin-bottom:6px}\n.bar{display:flex;align-items:center;gap:10px}\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:20px}\n.card{background:var(--panel);backdrop-filter:blur(16px);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column;transition:all .2s ease;box-shadow:0 4px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.04)}\n.card:hover{border-color:rgba(45,212,191,0.35);transform:translateY(-3px);box-shadow:0 12px 28px -6px rgba(0,0,0,0.6),0 0 0 1px rgba(45,212,191,0.25)}\n.thumb{aspect-ratio:16/10;background:#030508;display:flex;align-items:center;justify-content:center;overflow:hidden;cursor:pointer;position:relative}\n.thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}\n.card:hover .thumb img{transform:scale(1.03)}\n.thumb .ph{color:var(--dim);font-size:12px;display:flex;align-items:center;gap:6px}\n.tbadge{position:absolute;bottom:8px;right:8px;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);color:#fff;font-size:11px;font-weight:600;padding:2px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.1)}\n.abadge{position:absolute;top:8px;left:8px;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);color:var(--muted);font-size:10.5px;font-weight:500;padding:2px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.1)}\n.cardbody{padding:14px 16px 12px;display:flex;flex-direction:column;gap:5px;flex:1}\n.cardbody b{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff}\n.meta{color:var(--muted);font-size:12.5px;display:flex;align-items:center;gap:6px}\n.cardfoot{display:flex;gap:6px;padding:0 14px 14px;border-top:1px solid rgba(255,255,255,0.04);padding-top:10px}\n.tag{display:inline-block;font-size:11px;font-weight:500;background:rgba(255,255,255,0.04);border:1px solid var(--line);border-radius:999px;padding:1px 8px;color:var(--muted)}\n.tag.ok{color:var(--ok);border-color:rgba(52,211,153,0.3);background:rgba(52,211,153,0.06)}\n.empty{text-align:center;padding:70px 24px;border:1px dashed var(--line);border-radius:16px;background:rgba(15,22,34,0.4);color:var(--muted)}\n.empty h2{color:var(--text);font-size:20px;margin-bottom:8px}\n.empty-icon{font-size:36px;margin-bottom:12px;display:block}\n.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:18px}\n.tile{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:14px 12px;text-align:left;color:var(--text);transition:all .15s}\n.tile:hover{border-color:var(--line-hover);background:rgba(255,255,255,0.03)}\n.tile b{display:block;font-size:14px;margin-bottom:3px;color:#fff}\n.tile span{color:var(--muted);font-size:12px;line-height:1.4;display:block}\n.tile.on{border-color:var(--accent);background:rgba(45,212,191,0.08);box-shadow:0 0 14px rgba(45,212,191,0.15)}\n.tile.on b{color:var(--accent)}\n.chips{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0 18px}\n.chip{background:rgba(255,255,255,0.03);border:1px solid var(--line);color:var(--muted);border-radius:999px;padding:5px 13px;font-size:12.5px;transition:all .15s}\n.chip:hover{color:var(--text);border-color:rgba(45,212,191,0.4);background:rgba(45,212,191,0.05);transform:translateY(-1px)}\n.drop{border:1px dashed rgba(255,255,255,0.16);border-radius:10px;padding:22px;text-align:center;color:var(--muted);background:var(--panel2);transition:all .15s}\n.drop:hover,.drop.over{border-color:var(--accent);color:var(--text);background:rgba(45,212,191,0.04)}\n.files{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}\n.files span{background:rgba(255,255,255,0.06);border:1px solid var(--line);border-radius:6px;padding:4px 10px;font-size:12.5px;display:inline-flex;align-items:center;gap:6px}\ndetails{margin:8px 0 20px;background:rgba(255,255,255,0.02);border:1px solid var(--line);border-radius:10px;padding:12px 14px}\ndetails summary{cursor:pointer;color:var(--muted);font-weight:500;user-select:none}\ndetails summary:hover{color:var(--text)}\n.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n.narrow{max-width:740px}\n.err{color:#fda4af;background:rgba(244,63,94,0.1);border:1px solid rgba(244,63,94,0.3);border-radius:8px;padding:12px 14px;white-space:pre-wrap;margin:12px 0;font-size:13px}\n.note{color:#fef08a;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.25);border-radius:8px;padding:10px 14px;font-size:13px;margin:12px 0}\n.box-info{color:#a5f3fc;background:rgba(6,182,212,0.08);border:1px solid rgba(6,182,212,0.25);border-radius:8px;padding:10px 14px;font-size:13px;margin:12px 0}\n.log{background:var(--panel2-solid);border:1px solid var(--line);border-radius:10px;padding:12px 14px;font:12.5px/1.6 ui-monospace,Consolas,monospace;color:#93c5fd;max-height:260px;overflow:auto;white-space:pre-wrap}\n.center{max-width:580px;margin:80px auto;text-align:center}\n.center .spin{width:40px;height:40px;border-width:3px;margin:0 auto 24px;border-top-color:var(--accent)}\n/* creation mode switch */\n.mode-switch{display:flex;gap:4px;background:rgba(255,255,255,0.03);border:1px solid var(--line);padding:4px;border-radius:10px;margin-bottom:24px}\n.mode-switch button{flex:1;background:transparent;border:0;color:var(--muted);padding:8px 14px;border-radius:8px;font-size:13.5px;font-weight:600;transition:all .15s}\n.mode-switch button:hover{color:var(--text)}\n.mode-switch button.on{color:#fff;background:rgba(255,255,255,0.1);box-shadow:0 2px 6px rgba(0,0,0,0.3)}\n/* editor */\n.editor{display:grid;grid-template-columns:minmax(0,1fr) 420px;height:100%;min-height:0}\n.stage{display:flex;flex-direction:column;min-width:0;min-height:0;padding:16px 22px;gap:12px;overflow:auto}\n.etop{display:flex;align-items:center;gap:12px;flex-wrap:wrap}\n.back-link{color:var(--muted);font-size:13px;font-weight:500;padding:5px 9px;border-radius:6px;background:rgba(255,255,255,0.03);border:1px solid var(--line)}\n.back-link:hover{color:var(--text);border-color:var(--line-hover)}\n.etop input{font-size:17px;font-weight:700;color:#fff;background:transparent;border:1px solid transparent;width:auto;min-width:220px;padding:4px 8px;border-radius:6px}\n.etop input:hover,.etop input:focus{border-color:var(--line);background:var(--panel2)}\n.saved{color:var(--muted);font-size:12.5px;font-weight:500}\n.screen{background:#000;border-radius:14px;overflow:hidden;display:flex;justify-content:center;border:1px solid var(--line);box-shadow:0 8px 30px rgba(0,0,0,0.6);position:relative}\n#stage-player{width:100%;max-width:880px}\n#stage-player .ove-player{margin:0 auto}\n#stage-player canvas{max-height:56vh;margin:0 auto;width:auto!important;max-width:100%;box-shadow:0 0 30px rgba(0,0,0,0.8)}\n.xport{display:flex;align-items:center;gap:12px;background:rgba(15,22,34,0.6);border:1px solid var(--line);border-radius:10px;padding:8px 14px}\n.xport input[type=range]{flex:1;padding:0;accent-color:var(--accent);height:6px;cursor:pointer}\n.xport .t{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12.5px;min-width:96px;text-align:right;font-family:ui-monospace,Consolas,monospace}\n.scenes{display:flex;gap:8px;overflow:auto;padding-bottom:4px}\n.scenes button{background:rgba(255,255,255,0.03);border:1px solid var(--line);color:var(--text);border-radius:8px;padding:7px 12px;white-space:nowrap;font-size:12.5px;font-weight:500;transition:all .15s}\n.scenes button:hover{border-color:rgba(45,212,191,0.4);background:rgba(45,212,191,0.05);transform:translateY(-1px)}\n.scenes small{color:var(--dim);margin-left:6px;font-family:ui-monospace,Consolas,monospace}\n.side{border-left:1px solid var(--line);background:var(--panel);backdrop-filter:blur(20px);display:flex;flex-direction:column;min-height:0}\n.tabs{display:flex;border-bottom:1px solid var(--line);padding:0 8px;gap:2px}\n.tabs button{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);padding:14px 12px;font-size:13px;font-weight:600;transition:all .15s}\n.tabs button:hover{color:var(--text)}\n.tabs button.on{color:var(--accent);border-bottom-color:var(--accent)}\n.pane{padding:20px;overflow:auto;min-height:0;flex:1}\n.hist{border-top:1px solid var(--line);padding:12px 0;display:flex;gap:10px;align-items:flex-start;justify-content:space-between}\n.hist b{font-size:13.5px;color:#fff}.hist small{display:block;color:var(--muted);margin-top:2px}\n.themes{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}\n.theme{border:1px solid var(--line);border-radius:10px;padding:10px;background:var(--panel2);color:var(--text);text-align:left;transition:all .15s}\n.theme:hover{border-color:rgba(45,212,191,0.4);transform:translateY(-1px)}\n.sw{display:flex;height:24px;border-radius:6px;overflow:hidden;margin-bottom:8px;box-shadow:0 1px 4px rgba(0,0,0,0.4)}\n.sw i{flex:1}\n.blocklist{display:flex;flex-direction:column;gap:8px}\n.block{display:flex;gap:10px;align-items:center;justify-content:space-between;border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:var(--panel2);transition:all .15s}\n.block:hover{border-color:var(--line-hover)}\n.block b{font-size:13.5px;color:#fff}.block small{display:block;color:var(--muted);margin-top:2px}\n.result video,.result img{width:100%;border-radius:10px;border:1px solid var(--line);background:#000;margin-top:12px;box-shadow:0 4px 20px rgba(0,0,0,0.5)}\n.cat{margin:28px 0 10px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;font-size:11.5px;font-weight:700}\n.toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:#131c2a;border:1px solid #2a3c54;color:#fff;border-radius:10px;padding:11px 18px;z-index:99;max-width:80vw;box-shadow:0 10px 30px rgba(0,0,0,0.6);font-size:13.5px;font-weight:500;animation:toastIn .2s ease}\n.toast.bad{background:#2a1117;border-color:#5c1d29;color:#fecdd3}\n@keyframes toastIn{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.modal{position:fixed;inset:0;background:rgba(2,4,8,0.75);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:80}.modal>div{background:var(--panel-solid);border:1px solid var(--line);border-radius:14px;padding:24px;width:min(460px,92vw);box-shadow:0 20px 40px rgba(0,0,0,0.8)}\n@media (max-width:960px){.editor{grid-template-columns:1fr;height:auto}.side{border-left:0;border-top:1px solid var(--line)}header.top{gap:10px;padding:0 12px}.busy{display:none!important}}\n</style></head><body>\n<header class=\"top\">\n<a class=\"brand\" href=\"#/\"><span class=\"mark\">MR</span> MotionRender <span class=\"vpill\">AI Studio</span></a>\n<nav class=\"main\"><a href=\"#/\" data-nav=\"home\">Projects</a><a href=\"#/library\" data-nav=\"library\">Library</a><a href=\"#/settings\" data-nav=\"settings\">Settings</a></nav>\n<div class=\"grow\"></div>\n<a class=\"pbadge\" id=\"pbadge\" href=\"#/settings\"><span class=\"pdot\"></span><span id=\"pbadgetext\">AI Ready</span></a>\n<div class=\"busy\" id=\"busy\"><div class=\"spin\"></div><span id=\"busytext\"></span></div>\n<a class=\"btn\" href=\"#/new\">+ New Video</a>\n</header>\n<div id=\"app\"></div>\n<script>\nvar $ = function (s, r) { return (r || document).querySelector(s); };\nfunction h(tag, attrs, kids) {\n var el = document.createElement(tag);\n for (var k in (attrs || {})) { var v = attrs[k]; if (v === false || v == null) continue; if (k === 'class') el.className = v; else if (k.slice(0, 2) === 'on') el.addEventListener(k.slice(2), v); else if (k === 'text') el.textContent = v; else el.setAttribute(k, v === true ? '' : v); }\n [].concat(kids == null ? [] : kids).forEach(function (c) { if (c == null || c === false) return; el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); });\n return el;\n}\nfunction api(path, body) {\n var opt = body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };\n return fetch(path, opt).then(function (r) { return r.text().then(function (t) { var j = {}; try { j = JSON.parse(t); } catch (e) { j = { error: t }; } if (!r.ok) throw new Error(j.error || r.statusText); return j; }); });\n}\nfunction toast(msg, bad) { var t = h('div', { class: 'toast' + (bad ? ' bad' : ''), text: msg }); document.body.appendChild(t); setTimeout(function () { t.remove(); }, bad ? 6000 : 2800); }\nfunction ago(ms) { var s = Math.max(1, Math.round((Date.now() - ms) / 1000)); if (s < 60) return 'just now'; if (s < 3600) return Math.round(s / 60) + ' min ago'; if (s < 86400) return Math.round(s / 3600) + ' h ago'; return Math.round(s / 86400) + ' d ago'; }\nfunction dur(sec) { var m = Math.floor(sec / 60), s = Math.round(sec - m * 60); return m ? m + ':' + (s < 10 ? '0' : '') + s : s + 's'; }\nfunction ask(title, value, ok) {\n var input = h('input', { value: value || '' }), m = h('div', { class: 'modal' }, h('div', null, [h('h2', { text: title }), input, h('div', { class: 'bar', style: 'margin:18px 0 0;justify-content:flex-end' }, [\n h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }), h('button', { class: 'btn', text: 'OK', onclick: function () { m.remove(); ok(input.value.trim()); } })])]));\n document.body.appendChild(m); input.focus(); input.select(); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { m.remove(); ok(input.value.trim()); } if (e.key === 'Escape') m.remove(); });\n}\nfunction confirmBox(text, ok) { var m = h('div', { class: 'modal' }, h('div', null, [h('p', { text: text, style: 'font-size:15px;line-height:1.5;margin:0 0 16px' }), h('div', { class: 'bar', style: 'justify-content:flex-end' }, [h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }), h('button', { class: 'btn', text: 'Yes, proceed', onclick: function () { m.remove(); ok(); } })])])); document.body.appendChild(m); }\n\n/* provider status polling in top bar */\nfunction updateStatusBadge() {\n api('/api/status').then(function (s) {\n var ptext = $('#pbadgetext'); if (!ptext) return;\n if (s.provider) ptext.textContent = (s.provider.id === 'groq' ? '\u26A1 Groq \u00B7 ' : s.provider.id === 'nvidia' ? '\uD83D\uDFE2 NVIDIA \u00B7 ' : '\u2728 ' + s.provider.id + ' \u00B7 ') + s.provider.model;\n else ptext.textContent = '\uD83D\uDCCB Paste JSON mode';\n }, function () {});\n}\n\n/* jobs: one event stream, a promise per job */\nvar jobs = {}, jobWaiters = {};\nfunction onJob(job) {\n jobs[job.id] = job; var running = Object.keys(jobs).map(function (k) { return jobs[k]; }).filter(function (j) { return j.status === 'running' || j.status === 'queued'; });\n $('#busy').classList.toggle('on', running.length > 0); var last = running[0]; $('#busytext').textContent = last ? (last.messages.length ? last.messages[last.messages.length - 1] : 'Working on ' + last.kind + '\u2026') : '';\n (jobWaiters[job.id] || []).forEach(function (w) { w.progress(job); if (job.status === 'done') w.ok(job); if (job.status === 'error') w.fail(new Error(job.error || 'Failed')); });\n if (job.status === 'done' || job.status === 'error') delete jobWaiters[job.id];\n}\nfunction connect() {\n var es = new EventSource('/api/events');\n es.addEventListener('snapshot', function (e) { JSON.parse(e.data).jobs.forEach(onJob); });\n es.addEventListener('job', function (e) { onJob(JSON.parse(e.data)); });\n}\nfunction waitJob(job, progress) {\n return new Promise(function (ok, fail) {\n var cur = jobs[job.id] || job; if (cur.status === 'done') return ok(cur); if (cur.status === 'error') return fail(new Error(cur.error));\n (jobWaiters[job.id] = jobWaiters[job.id] || []).push({ ok: ok, fail: fail, progress: progress || function () {} });\n var poll = setInterval(function () { if (!jobWaiters[job.id]) return clearInterval(poll); api('/api/job?id=' + job.id).then(function (r) { onJob(r.job); }, function () {}); }, 3500);\n });\n}\nfunction run(path, body, progress) { return api(path, body).then(function (r) { return waitJob(r.job, progress); }).then(function (j) { return j.result; }); }\n\n/* routes */\nvar app = $('#app'), cleanup = null;\nfunction route() {\n if (cleanup) { cleanup(); cleanup = null; }\n var hash = location.hash.replace('#', '') || '/', parts = hash.split('/').filter(Boolean), view = parts[0] || 'home';\n document.querySelectorAll('[data-nav]').forEach(function (a) { a.classList.toggle('on', a.getAttribute('data-nav') === (view === 'new' || view === 'p' ? 'home' : view)); });\n app.innerHTML = ''; app.scrollTop = 0;\n var go = { home: home, new: create, library: library, settings: settings }[view];\n if (view === 'p' && parts[1]) return editor(decodeURIComponent(parts[1]), parts[2]);\n if (view === 'making' && parts[1]) return making(decodeURIComponent(parts[1]), parts[2]);\n (go || home)();\n}\nwindow.addEventListener('hashchange', route);\nfunction fail(el, e) { el.appendChild(h('div', { class: 'err', text: e && e.message ? e.message : String(e) })); }\n\n/* projects dashboard */\nfunction home() {\n var page = h('div', { class: 'page' }); app.appendChild(page);\n var hero = h('div', { class: 'hero' }, [\n h('div', null, [h('h1', { text: 'Your Videos' }), h('p', { class: 'lead', text: 'All your video projects. Click any project to open the live editor, re-style it, or export MP4.' })]),\n h('div', { class: 'bar' }, [h('a', { class: 'btn', href: '#/new', text: '+ New video' })])\n ]);\n page.appendChild(hero);\n api('/api/projects').then(function (r) {\n if (!r.projects.length) {\n page.appendChild(h('div', { class: 'empty' }, [\n h('span', { class: 'empty-icon', text: '\uD83C\uDFAC' }),\n h('h2', { text: 'No video projects yet' }),\n h('p', { class: 'lead', style: 'margin-bottom:20px', text: 'Describe an idea with AI, or paste composition JSON generated in ChatGPT or Claude.' }),\n h('a', { class: 'btn', href: '#/new', text: 'Create your first video' })\n ]));\n return;\n }\n var grid = h('div', { class: 'grid' }); r.projects.forEach(function (p) { grid.appendChild(card(p)); }); page.appendChild(grid);\n [].reduce.call(grid.querySelectorAll('img'), function (chain, img) { return chain.then(function () { return new Promise(function (ok) { img.onload = img.onerror = ok; img.src = img.getAttribute('data-src'); }); }); }, Promise.resolve());\n }, function (e) { fail(page, e); });\n}\nfunction card(p) {\n var open = function () { location.hash = '#/p/' + encodeURIComponent(p.id); };\n var img = h('img', { 'data-src': '/api/thumb?id=' + encodeURIComponent(p.id), alt: '' });\n img.addEventListener('error', function () { img.replaceWith(h('div', { class: 'ph', text: '\u25B6 Preview' })); });\n var aspectLabel = p.width < p.height ? '9:16 vertical' : p.width === p.height ? '1:1 square' : '16:9 wide';\n return h('div', { class: 'card' }, [\n h('div', { class: 'thumb', onclick: open }, [\n img,\n h('span', { class: 'abadge', text: aspectLabel }),\n h('span', { class: 'tbadge', text: '\u25B6 ' + dur(p.seconds) })\n ]),\n h('div', { class: 'cardbody' }, [\n h('b', { text: p.name, title: p.name }),\n h('div', { class: 'meta', text: p.width + '\u00D7' + p.height + ' \u00B7 ' + p.scenes + ' scene(s)' }),\n h('div', { class: 'meta' }, ['Edited ' + ago(p.modified) + ' ', h('span', { class: 'tag' + (p.mode === 'template' ? ' ok' : ''), text: p.mode === 'template' ? 'editable text' : 'custom' })])\n ]),\n h('div', { class: 'cardfoot' }, [\n h('button', { class: 'btn sm', text: 'Open Studio', onclick: open }),\n h('button', { class: 'btn sec sm', text: 'Duplicate', onclick: function () { api('/api/project/duplicate', { id: p.id }).then(function () { toast('Duplicated project'); route(); }, function (e) { toast(e.message, true); }); } }),\n h('button', { class: 'btn sec sm danger', text: 'Delete', onclick: function () { confirmBox('Delete \"' + p.name + '\"? It moves to .trash folder.', function () { api('/api/project/delete', { id: p.id }).then(function () { toast('Deleted'); route(); }, function (e) { toast(e.message, true); }); }); } })\n ])\n ]);\n}\n\n/* create screen with dual mode */\nvar kinds = [\n ['reel', '\uD83D\uDCF1 Social Reel', 'Vertical 9:16, ~20s. For TikTok, Shorts, Reels'],\n ['product', '\u23F9\uFE0F Product Video', 'Square 1:1, ~18s. For Instagram / LinkedIn feeds'],\n ['promo', '\uD83D\uDCFA Promo Video', 'Wide 16:9, ~30s. For YouTube, landing pages'],\n ['project', '\uD83D\uDCBC Case Study', 'Wide 16:9, ~60s. For case studies, deep dives']\n];\nvar examples = [\n 'Vertical reel explaining panic attacks and how the 4-7-8 breathing technique stops the spiral',\n 'Launch video for a budgeting app called Pocket, highlighting 3 key features and download link',\n 'Product card for a luxury hand-poured soy candle with free shipping offer',\n 'Event invite for a tech and design meetup in Berlin with RSVP call-to-action'\n];\n\nfunction create() {\n var activeMode = 'ai'; // 'ai' or 'paste'\n var kind = 'reel', files = [], page = h('div', { class: 'page narrow' }); app.appendChild(page);\n\n var modeSwitch = h('div', { class: 'mode-switch' });\n var btnAi = h('button', { class: 'on', text: '\u2728 Generate with AI' });\n var btnPaste = h('button', { text: '\uD83D\uDCCB Paste JSON (ChatGPT / Claude)' });\n modeSwitch.append(btnAi, btnPaste);\n\n var contentAi = h('div');\n var contentPaste = h('div', { style: 'display:none' });\n\n btnAi.addEventListener('click', function () {\n activeMode = 'ai'; btnAi.classList.add('on'); btnPaste.classList.remove('on');\n contentAi.style.display = 'block'; contentPaste.style.display = 'none';\n });\n btnPaste.addEventListener('click', function () {\n activeMode = 'paste'; btnPaste.classList.add('on'); btnAi.classList.remove('on');\n contentAi.style.display = 'none'; contentPaste.style.display = 'block';\n });\n\n page.append(h('h1', { text: 'Create New Video' }), h('p', { class: 'lead', style: 'margin-bottom:20px', text: 'Build with your connected AI provider, or paste composition JSON generated in ChatGPT or Claude.' }), modeSwitch, contentAi, contentPaste);\n\n /* Tab 1: AI Generation */\n var brief = h('textarea', { placeholder: 'What is your video about? Who is it for? Key points or message?', style: 'min-height:120px' });\n var name = h('input', { placeholder: 'Optional. Auto-generated from your brief if left empty' });\n var tiles = h('div', { class: 'tiles' }), fileList = h('div', { class: 'files' }), errBox = h('div');\n var drawTiles = function () {\n tiles.innerHTML = '';\n kinds.forEach(function (k) {\n tiles.appendChild(h('button', { class: 'tile' + (kind === k[0] ? ' on' : ''), onclick: function () { kind = k[0]; drawTiles(); } }, [h('b', { text: k[1] }), h('span', { text: k[2] })]));\n });\n };\n drawTiles();\n\n var drawFiles = function () {\n fileList.innerHTML = '';\n files.forEach(function (f, i) {\n fileList.appendChild(h('span', { text: f.name + ' \u00D7', style: 'cursor:pointer', onclick: function () { files.splice(i, 1); drawFiles(); } }));\n });\n };\n var picker = h('input', { type: 'file', multiple: true, accept: 'image/*,video/*,audio/*', style: 'display:none', onchange: function () { [].forEach.call(picker.files, function (f) { files.push(f); }); drawFiles(); picker.value = ''; } });\n var drop = h('div', { class: 'drop' }, ['Drag & drop photos or clips here, or ', h('a', { href: '#', text: 'browse files', onclick: function (e) { e.preventDefault(); picker.click(); } }), picker]);\n drop.addEventListener('dragover', function (e) { e.preventDefault(); drop.classList.add('over'); });\n drop.addEventListener('dragleave', function () { drop.classList.remove('over'); });\n drop.addEventListener('drop', function (e) { e.preventDefault(); drop.classList.remove('over'); [].forEach.call(e.dataTransfer.files, function (f) { files.push(f); }); drawFiles(); });\n\n var duration = h('input', { type: 'number', min: '3', placeholder: 'Default for kind' });\n var aspect = h('select', null, [['', 'Default for format'], ['9:16', 'Vertical 9:16 (Shorts/Reels)'], ['1:1', 'Square 1:1 (Feed)'], ['16:9', 'Wide 16:9 (Landscape)'], ['4:5', 'Portrait 4:5']].map(function (a) { return h('option', { value: a[0], text: a[1] }); }));\n var voice = h('input', { type: 'checkbox' }), review = h('input', { type: 'checkbox' }), full = h('input', { type: 'checkbox' }), go = h('button', { class: 'btn', text: '\u2728 Generate Video' });\n var chips = h('div', { class: 'chips' }, examples.map(function (t) {\n return h('button', { class: 'chip', text: t.length > 56 ? t.slice(0, 54) + '\u2026' : t, title: t, onclick: function () { brief.value = t; } });\n }));\n\n contentAi.append(\n h('label', { class: 'f' }, [h('span', { text: 'Format & Aspect Ratio' }), tiles]),\n h('label', { class: 'f' }, [h('span', { text: 'Video Brief' }), brief]),\n chips,\n h('label', { class: 'f' }, [h('span', { text: 'Media Assets (optional)' }), drop, fileList]),\n h('details', null, [\n h('summary', { text: 'Advanced Settings' }),\n h('div', { style: 'margin-top:14px' }, [\n h('label', { class: 'f' }, [h('span', { text: 'Project Name' }), name]),\n h('div', { class: 'two' }, [h('label', { class: 'f' }, [h('span', { text: 'Duration in seconds' }), duration]), h('label', { class: 'f' }, [h('span', { text: 'Aspect Ratio Override' }), aspect])]),\n h('label', { class: 'f' }, [voice, ' Add voiceover narration and captions']),\n h('label', { class: 'f' }, [review, ' Automatic review and self-correction pass']),\n h('label', { class: 'f' }, [full, ' Write every layer from scratch (experimental, without templates)'])\n ])\n ]),\n errBox,\n h('div', { style: 'margin-top:16px' }, go)\n );\n\n go.addEventListener('click', function () {\n errBox.innerHTML = ''; var text = brief.value.trim(); if (!text) { fail(errBox, new Error('Please describe your video in the brief')); return; }\n go.disabled = true; go.textContent = 'Starting\u2026';\n var title = name.value.trim() || text.split(' ').slice(0, 6).join(' ');\n api('/api/project/new', { name: title, kind: kind }).then(function (made) {\n return files.reduce(function (p, f) {\n return p.then(function () {\n return fetch('/api/upload?project=' + encodeURIComponent(made.id) + '&name=' + encodeURIComponent(f.name), { method: 'POST', headers: { 'content-type': 'application/octet-stream' }, body: f }).then(function (r) { if (!r.ok) return r.json().then(function (j) { throw new Error(j.error || 'Upload failed'); }); });\n });\n }, Promise.resolve()).then(function () {\n var body = { brief: text, kind: kind, folder: made.id, narration: voice.checked, review: review.checked, full: full.checked };\n if (duration.value) body.duration = duration.value;\n if (aspect.value) body.aspect = aspect.value;\n return api('/api/compose', body);\n }).then(function (r) {\n location.hash = '#/making/' + encodeURIComponent(made.id) + '/' + r.job.id;\n });\n }).catch(function (e) { go.disabled = false; go.textContent = '\u2728 Generate Video'; fail(errBox, e); });\n });\n\n /* Tab 2: Paste JSON Workflow */\n var pasteBrief = h('textarea', { placeholder: 'Brief for AI prompt generator (e.g. 20-second vertical reel on panic attack recovery)...', style: 'min-height:75px' });\n var pasteKind = h('select', null, [['reel', 'Vertical Reel (9:16)'], ['product', 'Square (1:1)'], ['promo', 'Landscape Promo (16:9)'], ['project', 'Case Study (16:9)']].map(function (k) { return h('option', { value: k[0], text: k[1] }); }));\n var pastePromptBtn = h('button', { class: 'btn sec', text: '\uD83D\uDCCB Copy AI Prompt Cheatsheet' });\n var pasteInput = h('textarea', { placeholder: 'Paste valid composition JSON starting with { \"name\": ..., \"scenes\": [...] } here...', style: 'min-height:160px;font:12.5px/1.5 ui-monospace,Consolas,monospace' });\n var pasteFolder = h('input', { placeholder: 'Optional folder name (e.g. panic-attack-reel)' });\n var pasteGo = h('button', { class: 'btn', text: '\u26A1 Import & Launch Studio' });\n var pasteErr = h('div');\n\n pastePromptBtn.addEventListener('click', function () {\n pastePromptBtn.disabled = true; pastePromptBtn.textContent = 'Generating prompt\u2026';\n api('/api/prompt', { brief: pasteBrief.value.trim() || 'A modern promotional video', kind: pasteKind.value, schema: true }).then(function (r) {\n pastePromptBtn.disabled = false; pastePromptBtn.textContent = '\uD83D\uDCCB Copy AI Prompt Cheatsheet';\n if (navigator.clipboard) {\n navigator.clipboard.writeText(r.prompt).then(function () { toast('Copied prompt cheatsheet to clipboard!'); }, function () { toast('Prompt ready'); });\n } else {\n toast('Prompt ready');\n }\n }, function (e) {\n pastePromptBtn.disabled = false; pastePromptBtn.textContent = '\uD83D\uDCCB Copy AI Prompt Cheatsheet';\n fail(pasteErr, e);\n });\n });\n\n pasteGo.addEventListener('click', function () {\n pasteErr.innerHTML = ''; var raw = pasteInput.value.trim();\n if (!raw) { fail(pasteErr, new Error('Please paste your composition JSON first')); return; }\n pasteGo.disabled = true; pasteGo.textContent = 'Importing\u2026';\n api('/api/import', { json: raw, folder: pasteFolder.value.trim() || undefined }).then(function (r) {\n return waitJob(r.job, function (j) {\n if (j.messages.length) pasteGo.textContent = j.messages[j.messages.length - 1];\n });\n }).then(function (done) {\n var projId = (done.result && (done.result.id || done.result.project)) || (done.result && done.result.relative && done.result.relative.split('/')[0]) || pasteFolder.value.trim() || 'imported-video';\n toast('Imported video successfully!');\n location.hash = '#/p/' + encodeURIComponent(projId);\n }).catch(function (e) {\n pasteGo.disabled = false; pasteGo.textContent = '\u26A1 Import & Launch Studio';\n fail(pasteErr, e);\n });\n });\n\n contentPaste.append(\n h('div', { class: 'box-info', text: 'Zero-key workflow: Copy the official schema prompt into ChatGPT, Claude or Groq chat, then paste the returned JSON below.' }),\n h('h2', { style: 'margin-top:16px', text: 'Step 1: Get AI Chat Prompt' }),\n h('div', { class: 'two' }, [\n h('label', { class: 'f' }, [h('span', { text: 'Format' }), pasteKind]),\n h('label', { class: 'f' }, [h('span', { text: 'Video Topic' }), pasteBrief])\n ]),\n pastePromptBtn,\n h('h2', { style: 'margin-top:24px', text: 'Step 2: Paste JSON from AI' }),\n h('label', { class: 'f' }, [h('span', { text: 'Composition JSON' }), pasteInput]),\n h('label', { class: 'f' }, [h('span', { text: 'Project Folder Name (optional)' }), pasteFolder]),\n pasteErr,\n h('div', { style: 'margin-top:16px' }, pasteGo)\n );\n}\n\n/* live progress */\nfunction making(id, jobId) {\n var log = h('div', { class: 'log' }), title = h('h1', { text: 'Building your video\u2026' });\n var box = h('div', { class: 'center' }, [\n h('div', { class: 'spin' }),\n title,\n h('p', { class: 'lead', style: 'margin-bottom:18px', text: 'Writing scenes, positioning animations, and compiling design tokens. Ready in under a minute.' }),\n log\n ]);\n app.appendChild(box);\n waitJob({ id: jobId }, function (j) {\n log.textContent = j.messages.join(String.fromCharCode(10));\n log.scrollTop = log.scrollHeight;\n }).then(function () {\n location.hash = '#/p/' + encodeURIComponent(id);\n }, function (e) {\n var sp = box.querySelector('.spin'); if (sp) sp.remove();\n title.textContent = 'Could not finish video';\n fail(box, e);\n box.appendChild(h('div', { class: 'bar', style: 'justify-content:center;margin-top:20px' }, [\n h('a', { class: 'btn', href: '#/new', text: 'Try again' }),\n h('button', { class: 'btn sec', text: 'Delete empty folder', onclick: function () { api('/api/project/delete', { id: id }).then(function () { location.hash = '#/'; }); } })\n ]));\n });\n}\n\n/* full studio editor */\nfunction editor(id, tabHint) {\n var d, player = null, tab = tabHint || 'edit', box = h('div', { class: 'editor' }), stage = h('div', { class: 'stage' }), side = h('div', { class: 'side' });\n box.append(stage, side); app.appendChild(box); app.style.overflow = 'hidden';\n var destroyed = false;\n cleanup = function () {\n destroyed = true; document.removeEventListener('keydown', keys); app.style.overflow = '';\n if (player) { try { player.destroy(); } catch (e) {} }\n };\n var file = id + '/composition.json';\n var savedNote = h('span', { class: 'saved', text: '\u2713 Ready' });\n var screen = h('div', { class: 'screen' }, h('div', { id: 'stage-player' }));\n var sceneRow = h('div', { class: 'scenes' });\n var nameInput = h('input', { title: 'Click to rename project' });\n var pills = h('span', { class: 'tag', style: 'margin-left:4px' });\n\n var playBtn = h('button', { class: 'btn sm', text: '\u25B6 Play' });\n var scrub = h('input', { type: 'range', min: '0', max: '1', value: '0', step: '1' });\n var clock = h('span', { class: 't' });\n var xport = h('div', { class: 'xport' }, [playBtn, scrub, clock]);\n\n stage.append(\n h('div', { class: 'etop' }, [\n h('a', { class: 'back-link', href: '#/', text: '\u2190 Projects' }),\n nameInput,\n pills,\n h('div', { class: 'grow' }),\n savedNote,\n h('button', { class: 'btn sm sec', text: 'Export MP4', onclick: function () { tab = 'export'; drawTabs(); drawPane(); } })\n ]),\n screen,\n xport,\n sceneRow\n );\n\n function tick(f) {\n scrub.value = String(f);\n clock.textContent = dur(f / d.fps) + ' / ' + dur(d.durationFrames / d.fps) + ' \u00B7 f' + f;\n }\n playBtn.addEventListener('click', function () {\n if (!player) return;\n if (player.playing) player.pause(); else player.play();\n });\n scrub.addEventListener('input', function () {\n if (player) {\n if (player.playing) player.pause();\n player.seek(Number(scrub.value));\n tick(Number(scrub.value));\n }\n });\n document.addEventListener('keydown', keys);\n function keys(e) {\n if (e.key === ' ' && player && e.target === document.body) { e.preventDefault(); playBtn.click(); }\n }\n\n var tabs = h('div', { class: 'tabs' }), pane = h('div', { class: 'pane' }); side.append(tabs, pane);\n var names = [['edit', '\u270D\uFE0F Edit'], ['ai', '\u2728 Ask AI'], ['look', '\uD83C\uDFA8 Look'], ['history', '\uD83D\uDD52 History'], ['export', '\uD83D\uDE80 Export']];\n function drawTabs() {\n tabs.innerHTML = '';\n names.forEach(function (n) {\n tabs.appendChild(h('button', { class: tab === n[0] ? 'on' : '', text: n[1], onclick: function () { tab = n[0]; drawTabs(); drawPane(); } }));\n });\n }\n\n function playerStart(frame) {\n if (player) { try { player.destroy(); } catch (e) {} player = null; }\n $('#stage-player').innerHTML = '';\n return import('/player.js').then(function (m) {\n return m.createPlayer($('#stage-player'), {\n composition: d.source.raw,\n media: d.source.media,\n assetBase: location.origin + d.source.base,\n allowCode: false,\n controls: false\n });\n }).then(function (p) {\n if (destroyed) { p.destroy(); return; }\n player = p; scrub.max = String(Math.max(1, d.durationFrames - 1));\n tick(frame || 0); playBtn.textContent = '\u25B6 Play';\n p.on('frame', function (f) { tick(typeof f === 'number' ? f : p.frame); });\n p.on('play', function () { playBtn.textContent = '\u23F8 Pause'; });\n p.on('pause', function () { playBtn.textContent = '\u25B6 Play'; });\n p.on('ended', function () { playBtn.textContent = '\u25B6 Play'; });\n p.on('error', function (e) { savedNote.textContent = 'Preview error: ' + (e && e.message ? e.message : e); });\n if (frame) p.seek(frame);\n }).catch(function (e) {\n $('#stage-player').appendChild(h('div', { class: 'err', text: 'Preview player error: ' + e.message }));\n });\n }\n\n function load(keepFrame) {\n var frame = keepFrame && player ? player.frame : 0;\n return api('/api/project?id=' + encodeURIComponent(id)).then(function (r) {\n d = r; nameInput.value = d.name;\n pills.textContent = d.width + '\u00D7' + d.height + ' \u00B7 ' + d.fps + 'fps';\n drawScenes();\n return playerStart(frame);\n }).then(function () { drawPane(); }, function (e) { pane.innerHTML = ''; fail(pane, e); });\n }\n\n function drawScenes() {\n sceneRow.innerHTML = '';\n d.scenes.forEach(function (s, idx) {\n var btn = h('button', { onclick: function () { if (player) { player.seek(s.startFrame); tick(s.startFrame); } } }, [\n (idx + 1) + '. ' + s.name,\n h('small', { text: dur(s.durationFrames / d.fps) })\n ]);\n sceneRow.appendChild(btn);\n });\n }\n\n nameInput.addEventListener('change', function () {\n var v = nameInput.value.trim(); if (!v) { nameInput.value = d.name; return; }\n api('/api/project/rename', { id: id, name: v }).then(function () { d.name = v; savedNote.textContent = '\u2713 Renamed'; }, function (e) { toast(e.message, true); });\n });\n\n function save(path, body, note) {\n savedNote.textContent = 'Saving\u2026';\n return api(path, body).then(function () {\n savedNote.textContent = '\u2713 ' + (note || 'Saved');\n return load(true);\n }, function (e) { savedNote.textContent = 'Error'; toast(e.message, true); throw e; });\n }\n\n function drawPane() {\n pane.innerHTML = ''; if (!d) return;\n ({ edit: paneEdit, ai: paneAi, look: paneLook, history: paneHistory, export: paneExport }[tab])();\n }\n\n var themeNames = ['bg', 'surface', 'text', 'muted', 'accent', 'accent2', 'radius', 'headWeight'];\n\n function paneEdit() {\n var fields = [], apply = h('button', { class: 'btn', text: 'Save changes' }), box2 = h('div');\n if (d.mode === 'template') {\n d.variables.filter(function (v) { return themeNames.indexOf(v.name) < 0; }).forEach(function (v) {\n var input, label = v.description ? v.description : v.name.replace(/([A-Z])/g, ' $1').replace(/[-_]/g, ' ');\n if (v.type === 'boolean') { input = h('input', { type: 'checkbox' }); input.checked = !!v.value; }\n else if (v.type === 'color') input = h('input', { type: 'color', value: /^#[0-9a-fA-F]{6}$/.test(v.value) ? v.value : '#000000' });\n else if (v.type === 'number') input = h('input', { type: 'number', value: String(v.value) });\n else if (v.type === 'list') { input = h('textarea', { style: 'min-height:120px;font:12px ui-monospace,Consolas,monospace' }); input.value = JSON.stringify(v.value, null, 2); }\n else { input = h('textarea', { style: 'min-height:' + (String(v.value).length > 60 ? 90 : 42) + 'px' }); input.value = v.value == null ? '' : String(v.value); }\n fields.push({ v: v, input: input });\n box2.appendChild(h('label', { class: 'f' }, [h('span', { text: label }), input]));\n });\n if (!fields.length) box2.appendChild(h('p', { class: 'lead', text: 'This video has no exposed variable fields. Use Ask AI to adjust scene text.' }));\n apply.addEventListener('click', function () {\n var values = {};\n try {\n fields.forEach(function (f) {\n var v = f.v, x = f.input;\n values[v.name] = v.type === 'boolean' ? x.checked : v.type === 'number' ? Number(x.value) : v.type === 'list' ? JSON.parse(x.value) : x.value;\n });\n } catch (e) { toast('A list is not valid JSON: ' + e.message, true); return; }\n save('/api/variables', { id: id, values: values }, 'Saved');\n });\n } else {\n d.texts.forEach(function (t) {\n var input = h('textarea', { style: 'min-height:' + (t.content.length > 60 ? 80 : 40) + 'px' });\n input.value = t.content;\n fields.push({ t: t, input: input });\n box2.appendChild(h('label', { class: 'f' }, [h('span', { text: t.scene + ' \u00B7 ' + t.id }), input]));\n });\n if (!fields.length) box2.appendChild(h('p', { class: 'lead', text: 'No text layers found in this video.' }));\n apply.addEventListener('click', function () {\n var changed = fields.filter(function (f) { return f.input.value.trim() && f.input.value !== f.t.content; });\n changed.reduce(function (p, f) {\n return p.then(function () { return api('/api/text', { id: id, path: f.t.path, content: f.input.value }); });\n }, Promise.resolve()).then(function () {\n savedNote.textContent = changed.length ? '\u2713 Saved' : 'No changes';\n return load(true);\n }, function (e) { toast(e.message, true); });\n });\n }\n pane.append(\n h('h2', { text: 'Content & Text' }),\n h('p', { class: 'lead', style: 'font-size:13px;margin-bottom:16px', text: d.mode === 'template' ? 'Update headlines, copy, and variables. The visual layout adjusts dynamically.' : 'Every text line in the video in appearance order.' }),\n box2,\n fields.length ? apply : null\n );\n }\n\n function paneAi() {\n var text = h('textarea', { placeholder: 'e.g. Make it snappier, change the call to action, or add more energy', style: 'min-height:110px' });\n var go = h('button', { class: 'btn', text: '\u2728 Apply AI Change' }), errBox = h('div');\n var chips = h('div', { class: 'chips' }, [\n 'Make pace faster and punchier',\n 'Shorten all captions',\n 'Add a stronger call to action at the end',\n 'Calmer and more minimal',\n 'Use bolder, larger typography'\n ].map(function (t) { return h('button', { class: 'chip', text: t, onclick: function () { text.value = t; } }); }));\n\n pane.append(\n h('h2', { text: 'Ask AI to Refine Video' }),\n h('p', { class: 'lead', style: 'font-size:13px', text: 'Describe what you want to change in plain English. Every edit is saved to History so you can rollback anytime.' }),\n text,\n chips,\n d.mode === 'template' ? h('div', { class: 'note', text: 'Note: AI refinements unlock custom scene-by-scene editing.' }) : null,\n errBox,\n go\n );\n go.addEventListener('click', function () {\n errBox.innerHTML = ''; if (!text.value.trim()) return;\n go.disabled = true; go.textContent = 'Refining\u2026';\n run('/api/refine', { file: file, instruction: text.value.trim() }, function (j) {\n savedNote.textContent = j.messages.length ? j.messages[j.messages.length - 1] : 'Working\u2026';\n }).then(function (r) {\n savedNote.textContent = '\u2713 Updated';\n toast('Video refined successfully' + (r.warnings && r.warnings.length ? ' (with notes)' : ''));\n return load(false);\n }, function (e) {\n go.disabled = false; go.textContent = '\u2728 Apply AI Change';\n fail(errBox, e);\n });\n });\n }\n\n function paneLook() {\n pane.append(h('h2', { text: 'Look & Style' }));\n var themes = h('div', { class: 'themes' }), blocks = h('div', { class: 'blocklist' }), cat = h('select', { style: 'margin-bottom:12px' }), all = [];\n pane.append(\n h('p', { class: 'lead', style: 'font-size:13px', text: 'Pick a color palette to re-style every scene instantly.' }),\n themes,\n h('h2', { style: 'margin-top:28px', text: 'Add Scene Block' }),\n cat,\n blocks\n );\n api('/api/library').then(function (lib) {\n var canTheme = Object.keys(d.theme).length > 0;\n lib.themes.forEach(function (t) {\n var v = t.variables;\n var b = h('button', { class: 'theme', disabled: !canTheme, title: canTheme ? t.description : 'This video does not use theme tokens', onclick: function () { save('/api/theme', { id: id, theme: t.id }, 'Theme updated'); } }, [\n h('div', { class: 'sw' }, [v.bg, v.surface, v.accent, v.accent2].map(function (c) { return h('i', { style: 'background:' + c }); })),\n h('b', { text: t.name })\n ]);\n themes.appendChild(b);\n });\n all = lib.blocks;\n var cats = ['all'].concat(all.map(function (b) { return b.category; }).filter(function (c, i, a) { return a.indexOf(c) === i; }));\n cat.innerHTML = '';\n cats.forEach(function (c) { cat.appendChild(h('option', { value: c, text: c === 'all' ? 'All categories' : c })); });\n var draw = function () {\n blocks.innerHTML = '';\n all.filter(function (b) { return cat.value === 'all' || b.category === cat.value; }).forEach(function (b) {\n blocks.appendChild(h('div', { class: 'block' }, [\n h('div', null, [h('b', { text: b.name }), h('small', { text: b.description })]),\n h('button', { class: 'btn sec sm', text: '+ Add', onclick: function () { save('/api/add-block', { id: id, block: b.id }, 'Added ' + b.name).then(function () { toast('Added ' + b.name); }, function () {}); } })\n ]));\n });\n };\n cat.addEventListener('change', draw); draw();\n }, function (e) { fail(pane, e); });\n }\n\n function paneHistory() {\n pane.append(h('h2', { text: 'Version History' }), h('p', { class: 'lead', style: 'font-size:13px', text: 'Every change is versioned. Roll back anytime.' }));\n var list = h('div'); pane.appendChild(list);\n api('/api/history?file=' + encodeURIComponent(file)).then(function (r) {\n if (!r.revisions.length) { list.appendChild(h('p', { class: 'lead', text: 'No recorded versions yet.' })); return; }\n r.revisions.slice().reverse().forEach(function (rev, i) {\n list.appendChild(h('div', { class: 'hist' }, [\n h('div', null, [\n h('b', { text: '#' + rev.n + ' \u00B7 ' + rev.kind }),\n h('small', { text: rev.label }),\n h('small', { text: new Date(rev.at).toLocaleString() })\n ]),\n i === 0 ? h('span', { class: 'tag ok', text: 'current' }) : h('button', { class: 'btn sec sm', text: 'Rollback', onclick: function () {\n api('/api/restore', { file: file, n: rev.n }).then(function () { savedNote.textContent = 'Restored #' + rev.n; return load(false); }, function (e) { toast(e.message, true); });\n } })\n ]));\n });\n }, function (e) { fail(list, e); });\n }\n\n function paneExport() {\n var quality = h('select', null, [['1', 'Full HD (100% scale)'], ['0.75', 'Standard (75% scale)'], ['0.5', 'Fast Draft (50% scale)']].map(function (o) { return h('option', { value: o[0], text: o[1] }); }));\n var go = h('button', { class: 'btn', text: '\uD83C\uDFAC Render MP4 Video' }), out = h('div', { class: 'result' });\n var checkBtn = h('button', { class: 'btn sec', text: '\uD83D\uDD0D Check for issues' }), thumbBtn = h('button', { class: 'btn sec', text: '\uD83D\uDDBC\uFE0F Make Thumbnail' });\n var errBox = h('div');\n\n pane.append(\n h('h2', { text: 'Export & Render' }),\n h('p', { class: 'lead', style: 'font-size:13px', text: d.width + '\u00D7' + d.height + ' \u00B7 ' + dur(d.durationFrames / d.fps) + ' \u00B7 MP4' }),\n h('label', { class: 'f' }, [h('span', { text: 'Render Quality' }), quality]),\n go,\n h('div', { class: 'bar', style: 'margin:14px 0 0' }, [checkBtn, thumbBtn]),\n errBox,\n out\n );\n\n var guard = function (btn, label, fn) {\n btn.addEventListener('click', function () {\n errBox.innerHTML = ''; btn.disabled = true; btn.textContent = 'Working\u2026';\n fn().then(function () { btn.disabled = false; btn.textContent = label; }, function (e) { btn.disabled = false; btn.textContent = label; fail(errBox, e); });\n });\n };\n guard(go, '\uD83C\uDFAC Render MP4 Video', function () {\n return run('/api/render', { file: file, output: 'out.mp4', scale: Number(quality.value) }, function (j) {\n go.textContent = j.messages.length ? j.messages[j.messages.length - 1] : 'Rendering\u2026';\n }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: '\uD83C\uDF89 Your video is ready!' }),\n h('video', { src: r.url, controls: true }),\n h('div', { class: 'bar', style: 'margin:10px 0 0' }, [\n h('a', { class: 'btn sm', href: r.url, download: id + '.mp4', text: 'Download MP4' }),\n h('span', { class: 'saved', text: r.relative })\n ])\n );\n });\n });\n guard(checkBtn, '\uD83D\uDD0D Check for issues', function () {\n return run('/api/review', { file: file }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: r.issues.length ? r.issues.length + ' item(s) to review' : '\u2713 No issues found' }),\n h('ul', null, r.issues.slice(0, 12).map(function (i) { return h('li', { text: (i.message || String(i)) }); })),\n h('img', { src: r.url + '?t=' + Date.now() })\n );\n });\n });\n guard(thumbBtn, '\uD83D\uDDBC\uFE0F Make Thumbnail', function () {\n return run('/api/thumbnail', { file: file }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: 'Generated Thumbnail' }),\n h('img', { src: r.url + '?t=' + Date.now() }),\n h('div', { class: 'bar', style: 'margin:10px 0 0' }, [\n h('a', { class: 'btn sm', href: r.url, download: id + '.png', text: 'Download PNG' })\n ])\n );\n });\n });\n }\n\n drawTabs(); load(false);\n}\n\n/* library screen */\nfunction library() {\n var page = h('div', { class: 'page' }); app.appendChild(page);\n page.append(\n h('div', { class: 'hero' }, [\n h('div', null, [h('h1', { text: 'Design Library' }), h('p', { class: 'lead', text: 'Curated color palettes and modular scene blocks ready to inject into any video.' })])\n ])\n );\n api('/api/library').then(function (lib) {\n page.appendChild(h('h2', { text: 'Color Palettes' }));\n var row = h('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fill,minmax(200px,1fr));margin-bottom:36px' });\n lib.themes.forEach(function (t) {\n var v = t.variables;\n row.appendChild(h('div', { class: 'card', style: 'padding:14px' }, [\n h('div', { class: 'sw' }, [v.bg, v.surface, v.accent, v.accent2].map(function (c) { return h('i', { style: 'background:' + c }); })),\n h('b', { text: t.name, style: 'font-size:14.5px;color:#fff' }),\n h('div', { class: 'meta', style: 'margin-top:4px', text: t.description })\n ]));\n });\n page.appendChild(row);\n var cats = lib.blocks.map(function (b) { return b.category; }).filter(function (c, i, a) { return a.indexOf(c) === i; });\n cats.forEach(function (c) {\n page.appendChild(h('div', { class: 'cat', text: c }));\n var g = h('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fill,minmax(260px,1fr));margin-bottom:24px' });\n lib.blocks.filter(function (b) { return b.category === c; }).forEach(function (b) {\n g.appendChild(h('div', { class: 'card', style: 'padding:16px;gap:8px' }, [\n h('b', { text: b.name, style: 'font-size:14.5px;color:#fff' }),\n h('div', { class: 'meta', text: b.description }),\n h('div', { class: 'meta', text: (b.kind === 'overlay' ? 'Overlay \u00B7 ' : 'Scene \u00B7 ') + dur(b.durationFrames / 30) }),\n h('button', { class: 'btn sec sm', style: 'align-self:flex-start;margin-top:6px', text: '+ Add to video\u2026', onclick: function () { pick(b); } })\n ]));\n });\n page.appendChild(g);\n });\n }, function (e) { fail(page, e); });\n}\n\nfunction pick(block) {\n api('/api/projects').then(function (r) {\n if (!r.projects.length) { toast('Create a video project first', true); return; }\n var sel = h('select', null, r.projects.map(function (p) { return h('option', { value: p.id, text: p.name }); }));\n var m = h('div', { class: 'modal' }, h('div', null, [\n h('h2', { text: 'Add ' + block.name + ' to project:' }),\n sel,\n h('div', { class: 'bar', style: 'margin:18px 0 0;justify-content:flex-end' }, [\n h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }),\n h('button', { class: 'btn', text: 'Add Scene', onclick: function () {\n api('/api/add-block', { id: sel.value, block: block.id }).then(function () {\n m.remove(); location.hash = '#/p/' + encodeURIComponent(sel.value);\n }, function (e) { toast(e.message, true); });\n } })\n ])\n ]));\n document.body.appendChild(m);\n });\n}\n\n/* settings screen */\nfunction settings() {\n var page = h('div', { class: 'page narrow' }); app.appendChild(page);\n page.append(\n h('h1', { text: 'Studio Settings' }),\n h('p', { class: 'lead', style: 'margin-bottom:24px', text: 'Configuration, local storage path, and AI engine status.' })\n );\n api('/api/status').then(function (s) {\n var pinfo = s.provider ? s.provider.id.toUpperCase() + ' (' + s.provider.model + ')' : 'Not Connected';\n var rows = [\n ['Projects Directory', s.outDir],\n ['Active AI Provider', pinfo]\n ];\n rows.forEach(function (r) {\n page.appendChild(h('label', { class: 'f' }, [h('span', { text: r[0] }), h('input', { value: r[1], readonly: true })]));\n });\n if (s.error) {\n page.appendChild(h('div', { class: 'err', text: s.error + String.fromCharCode(10) + 'Tip: Add GROQ_API_KEY=... or GEMINI_API_KEY=... in your .env file, or use the zero-key Paste JSON workflow!' }));\n } else {\n page.appendChild(h('div', { class: 'note', text: 'Connected to ' + pinfo + '. AI generation and refinements will execute directly through this provider.' }));\n }\n page.appendChild(h('div', { class: 'box-info', text: 'Zero-API Key Workflow: You can also use ChatGPT or Claude directly by copying the prompt cheatsheet from \"+ New Video > Paste JSON\" and pasting the returned JSON.' }));\n page.appendChild(h('p', { class: 'meta', style: 'margin-top:20px', text: 'To change output directory, run: ove ai studio --out <custom-folder>' }));\n }, function (e) { fail(page, e); });\n}\n\nupdateStatusBadge();\nconnect();\nroute();\n</script></body></html>";
|
|
1
|
+
export declare const aiStudioPage = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>MotionRender | OVE AI studio</title>\n<style>\n:root{\n --bg:#07090e;\n --bg-gradient:radial-gradient(circle at 50% -10%,rgba(45,212,191,0.08) 0%,transparent 55%),radial-gradient(circle at 95% 15%,rgba(129,140,248,0.06) 0%,transparent 45%),#07090e;\n --panel:rgba(15,22,34,0.78);\n --panel-solid:#0f1622;\n --panel2:rgba(10,15,24,0.88);\n --panel2-solid:#0a0f18;\n --line:rgba(255,255,255,0.08);\n --line-hover:rgba(255,255,255,0.16);\n --text:#f8fafc;\n --muted:#94a3b8;\n --dim:#64748b;\n --accent:#2dd4bf;\n --accent-glow:rgba(45,212,191,0.25);\n --accent2:#818cf8;\n --ok:#34d399;\n --warn:#fbbf24;\n --err:#fb7185;\n --radius:12px;\n --radius-sm:8px;\n}\n*{box-sizing:border-box}html,body{height:100%}\nbody{margin:0;background:var(--bg-gradient);background-attachment:fixed;color:var(--text);font:14px/1.55 Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;letter-spacing:-.01em;display:flex;flex-direction:column;overflow:hidden;-webkit-font-smoothing:antialiased}\n::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:999px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.24)}\na{color:var(--accent);text-decoration:none;transition:color .15s}a:hover{filter:brightness(1.15)}\nheader.top{display:flex;align-items:center;gap:16px;padding:0 22px;height:58px;border-bottom:1px solid var(--line);background:rgba(9,13,20,0.85);backdrop-filter:blur(20px);flex:none;z-index:10}\n.brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em}\n.mark{font:800 11px/1 ui-monospace,Consolas,monospace;letter-spacing:.08em;color:#04211e;background:linear-gradient(135deg,#2dd4bf,#06b6d4);padding:6px 8px;border-radius:6px;box-shadow:0 0 12px var(--accent-glow)}\n.vpill{font-size:11px;font-weight:600;color:var(--muted);background:rgba(255,255,255,0.05);padding:2px 8px;border-radius:999px;border:1px solid var(--line)}\nnav.main{display:flex;gap:3px;background:rgba(255,255,255,0.03);padding:3px;border-radius:10px;border:1px solid var(--line)}\nnav.main a{color:var(--muted);padding:5px 13px;border-radius:7px;font-size:13px;font-weight:500;transition:all .15s}\nnav.main a:hover{color:var(--text);background:rgba(255,255,255,0.05)}\nnav.main a.on{color:#fff;background:rgba(255,255,255,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.3)}\n.grow{flex:1}\n.pbadge{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);background:rgba(255,255,255,0.03);border:1px solid var(--line);padding:4px 10px;border-radius:999px;text-decoration:none;transition:all .15s}\n.pbadge:hover{border-color:var(--line-hover);color:var(--text)}\n.pdot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok)}\n.busy{display:none;align-items:center;gap:9px;color:var(--muted);font-size:12.5px;max-width:320px;background:rgba(255,255,255,0.04);border:1px solid var(--line);padding:4px 11px;border-radius:999px}\n.busy.on{display:flex}.busy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.spin{width:12px;height:12px;border:2px solid rgba(255,255,255,0.15);border-top-color:var(--accent);border-radius:50%;animation:rot .8s linear infinite;flex:none}@keyframes rot{to{transform:rotate(360deg)}}\n#app{flex:1;min-height:0;overflow:auto}\n.page{max-width:1160px;margin:0 auto;padding:32px 24px 64px}\n.hero{margin-bottom:28px;display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}\nh1{margin:0 0 6px;font-size:26px;font-weight:700;letter-spacing:-.03em;color:#fff}\nh2{margin:0 0 10px;font-size:16px;font-weight:600;letter-spacing:-.01em}\n.lead{color:var(--muted);margin:0;font-size:14px}\nbutton{font:inherit;cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}\n.btn{background:linear-gradient(135deg,#2dd4bf 0%,#14b8a6 100%);color:#03211c;border:0;border-radius:8px;padding:8px 16px;font-weight:600;font-size:13.5px;box-shadow:0 2px 10px -2px rgba(45,212,191,0.4);transition:all .15s;display:inline-flex;align-items:center;justify-content:center;gap:7px}\n.btn:hover{filter:brightness(1.08);box-shadow:0 4px 16px -2px rgba(45,212,191,0.55);transform:translateY(-1px)}\n.btn:active{transform:translateY(0)}\n.btn.sec{background:rgba(255,255,255,0.05);color:var(--text);border:1px solid var(--line);font-weight:500;box-shadow:none}\n.btn.sec:hover{background:rgba(255,255,255,0.09);border-color:var(--line-hover);filter:none}\n.btn.sm{padding:5px 11px;font-size:12.5px;border-radius:6px}\n.btn.danger{color:#fda4af}.btn.danger:hover{background:rgba(244,63,94,0.15);border-color:rgba(244,63,94,0.3)}\ninput,textarea,select{width:100%;background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:9px 12px;font:inherit;transition:border-color .15s,box-shadow .15s}\ntextarea{resize:vertical;min-height:96px;font-size:13.5px}\ninput:focus,textarea:focus,select:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}\ninput[type=color]{padding:2px;height:36px;width:64px;border-radius:6px}input[type=checkbox]{width:auto}\nlabel.f{display:block;margin:0 0 16px}label.f>span{display:block;color:var(--muted);font-size:12.5px;font-weight:500;margin-bottom:6px}\n.bar{display:flex;align-items:center;gap:10px}\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:20px}\n.card{background:var(--panel);backdrop-filter:blur(16px);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column;transition:all .2s ease;box-shadow:0 4px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.04)}\n.card:hover{border-color:rgba(45,212,191,0.35);transform:translateY(-3px);box-shadow:0 12px 28px -6px rgba(0,0,0,0.6),0 0 0 1px rgba(45,212,191,0.25)}\n.thumb{aspect-ratio:16/10;background:#030508;display:flex;align-items:center;justify-content:center;overflow:hidden;cursor:pointer;position:relative}\n.thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}\n.card:hover .thumb img{transform:scale(1.03)}\n.thumb .ph{color:var(--dim);font-size:12px;display:flex;align-items:center;gap:6px}\n.tbadge{position:absolute;bottom:8px;right:8px;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);color:#fff;font-size:11px;font-weight:600;padding:2px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.1)}\n.abadge{position:absolute;top:8px;left:8px;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);color:var(--muted);font-size:10.5px;font-weight:500;padding:2px 7px;border-radius:5px;border:1px solid rgba(255,255,255,0.1)}\n.cardbody{padding:14px 16px 12px;display:flex;flex-direction:column;gap:5px;flex:1}\n.cardbody b{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff}\n.meta{color:var(--muted);font-size:12.5px;display:flex;align-items:center;gap:6px}\n.cardfoot{display:flex;gap:6px;padding:0 14px 14px;border-top:1px solid rgba(255,255,255,0.04);padding-top:10px}\n.tag{display:inline-block;font-size:11px;font-weight:500;background:rgba(255,255,255,0.04);border:1px solid var(--line);border-radius:999px;padding:1px 8px;color:var(--muted)}\n.tag.ok{color:var(--ok);border-color:rgba(52,211,153,0.3);background:rgba(52,211,153,0.06)}\n.empty{text-align:center;padding:70px 24px;border:1px dashed var(--line);border-radius:16px;background:rgba(15,22,34,0.4);color:var(--muted)}\n.empty h2{color:var(--text);font-size:20px;margin-bottom:8px}\n.empty-icon{font-size:36px;margin-bottom:12px;display:block}\n.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:18px}\n.tile{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:14px 12px;text-align:left;color:var(--text);transition:all .15s}\n.tile:hover{border-color:var(--line-hover);background:rgba(255,255,255,0.03)}\n.tile b{display:block;font-size:14px;margin-bottom:3px;color:#fff}\n.tile span{color:var(--muted);font-size:12px;line-height:1.4;display:block}\n.tile.on{border-color:var(--accent);background:rgba(45,212,191,0.08);box-shadow:0 0 14px rgba(45,212,191,0.15)}\n.tile.on b{color:var(--accent)}\n.chips{display:flex;gap:7px;flex-wrap:wrap;margin:10px 0 18px}\n.chip{background:rgba(255,255,255,0.03);border:1px solid var(--line);color:var(--muted);border-radius:999px;padding:5px 13px;font-size:12.5px;transition:all .15s}\n.chip:hover{color:var(--text);border-color:rgba(45,212,191,0.4);background:rgba(45,212,191,0.05);transform:translateY(-1px)}\n.drop{border:1px dashed rgba(255,255,255,0.16);border-radius:10px;padding:22px;text-align:center;color:var(--muted);background:var(--panel2);transition:all .15s}\n.drop:hover,.drop.over{border-color:var(--accent);color:var(--text);background:rgba(45,212,191,0.04)}\n.files{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}\n.files span{background:rgba(255,255,255,0.06);border:1px solid var(--line);border-radius:6px;padding:4px 10px;font-size:12.5px;display:inline-flex;align-items:center;gap:6px}\ndetails{margin:8px 0 20px;background:rgba(255,255,255,0.02);border:1px solid var(--line);border-radius:10px;padding:12px 14px}\ndetails summary{cursor:pointer;color:var(--muted);font-weight:500;user-select:none}\ndetails summary:hover{color:var(--text)}\n.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n.narrow{max-width:740px}\n.err{color:#fda4af;background:rgba(244,63,94,0.1);border:1px solid rgba(244,63,94,0.3);border-radius:8px;padding:12px 14px;white-space:pre-wrap;margin:12px 0;font-size:13px}\n.note{color:#fef08a;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.25);border-radius:8px;padding:10px 14px;font-size:13px;margin:12px 0}\n.box-info{color:#a5f3fc;background:rgba(6,182,212,0.08);border:1px solid rgba(6,182,212,0.25);border-radius:8px;padding:10px 14px;font-size:13px;margin:12px 0}\n.log{background:var(--panel2-solid);border:1px solid var(--line);border-radius:10px;padding:12px 14px;font:12.5px/1.6 ui-monospace,Consolas,monospace;color:#93c5fd;max-height:260px;overflow:auto;white-space:pre-wrap}\n.center{max-width:580px;margin:80px auto;text-align:center}\n.center .spin{width:40px;height:40px;border-width:3px;margin:0 auto 24px;border-top-color:var(--accent)}\n/* creation mode switch */\n.mode-switch{display:flex;gap:4px;background:rgba(255,255,255,0.03);border:1px solid var(--line);padding:4px;border-radius:10px;margin-bottom:24px}\n.mode-switch button{flex:1;background:transparent;border:0;color:var(--muted);padding:8px 14px;border-radius:8px;font-size:13.5px;font-weight:600;transition:all .15s}\n.mode-switch button:hover{color:var(--text)}\n.mode-switch button.on{color:#fff;background:rgba(255,255,255,0.1);box-shadow:0 2px 6px rgba(0,0,0,0.3)}\n/* editor */\n.editor{display:grid;grid-template-columns:minmax(0,1fr) 420px;height:100%;min-height:0}\n.stage{display:flex;flex-direction:column;min-width:0;min-height:0;padding:16px 22px;gap:12px;overflow:auto}\n.etop{display:flex;align-items:center;gap:12px;flex-wrap:wrap}\n.back-link{color:var(--muted);font-size:13px;font-weight:500;padding:5px 9px;border-radius:6px;background:rgba(255,255,255,0.03);border:1px solid var(--line)}\n.back-link:hover{color:var(--text);border-color:var(--line-hover)}\n.etop input{font-size:17px;font-weight:700;color:#fff;background:transparent;border:1px solid transparent;width:auto;min-width:220px;padding:4px 8px;border-radius:6px}\n.etop input:hover,.etop input:focus{border-color:var(--line);background:var(--panel2)}\n.saved{color:var(--muted);font-size:12.5px;font-weight:500}\n.screen{background:#000;border-radius:14px;overflow:hidden;display:flex;justify-content:center;border:1px solid var(--line);box-shadow:0 8px 30px rgba(0,0,0,0.6);position:relative}\n#stage-player{width:100%;max-width:880px}\n#stage-player .ove-player{margin:0 auto}\n#stage-player canvas{max-height:56vh;margin:0 auto;width:auto!important;max-width:100%;box-shadow:0 0 30px rgba(0,0,0,0.8)}\n.xport{display:flex;align-items:center;gap:12px;background:rgba(15,22,34,0.6);border:1px solid var(--line);border-radius:10px;padding:8px 14px}\n.xport input[type=range]{flex:1;padding:0;accent-color:var(--accent);height:6px;cursor:pointer}\n.xport .t{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12.5px;min-width:96px;text-align:right;font-family:ui-monospace,Consolas,monospace}\n.scenes{display:flex;gap:8px;overflow:auto;padding-bottom:4px}\n.scenes button{background:rgba(255,255,255,0.03);border:1px solid var(--line);color:var(--text);border-radius:8px;padding:7px 12px;white-space:nowrap;font-size:12.5px;font-weight:500;transition:all .15s}\n.scenes button:hover{border-color:rgba(45,212,191,0.4);background:rgba(45,212,191,0.05);transform:translateY(-1px)}\n.scenes small{color:var(--dim);margin-left:6px;font-family:ui-monospace,Consolas,monospace}\n.side{border-left:1px solid var(--line);background:var(--panel);backdrop-filter:blur(20px);display:flex;flex-direction:column;min-height:0}\n.tabs{display:flex;border-bottom:1px solid var(--line);padding:0 8px;gap:2px}\n.tabs button{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);padding:14px 12px;font-size:13px;font-weight:600;transition:all .15s}\n.tabs button:hover{color:var(--text)}\n.tabs button.on{color:var(--accent);border-bottom-color:var(--accent)}\n.pane{padding:20px;overflow:auto;min-height:0;flex:1}\n.hist{border-top:1px solid var(--line);padding:12px 0;display:flex;gap:10px;align-items:flex-start;justify-content:space-between}\n.hist b{font-size:13.5px;color:#fff}.hist small{display:block;color:var(--muted);margin-top:2px}\n.themes{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}\n.theme{border:1px solid var(--line);border-radius:10px;padding:10px;background:var(--panel2);color:var(--text);text-align:left;transition:all .15s}\n.theme:hover{border-color:rgba(45,212,191,0.4);transform:translateY(-1px)}\n.sw{display:flex;height:24px;border-radius:6px;overflow:hidden;margin-bottom:8px;box-shadow:0 1px 4px rgba(0,0,0,0.4)}\n.sw i{flex:1}\n.blocklist{display:flex;flex-direction:column;gap:8px}\n.block{display:flex;gap:10px;align-items:center;justify-content:space-between;border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:var(--panel2);transition:all .15s}\n.block:hover{border-color:var(--line-hover)}\n.block b{font-size:13.5px;color:#fff}.block small{display:block;color:var(--muted);margin-top:2px}\n.result video,.result img{width:100%;border-radius:10px;border:1px solid var(--line);background:#000;margin-top:12px;box-shadow:0 4px 20px rgba(0,0,0,0.5)}\n.cat{margin:28px 0 10px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;font-size:11.5px;font-weight:700}\n.toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:#131c2a;border:1px solid #2a3c54;color:#fff;border-radius:10px;padding:11px 18px;z-index:99;max-width:80vw;box-shadow:0 10px 30px rgba(0,0,0,0.6);font-size:13.5px;font-weight:500;animation:toastIn .2s ease}\n.toast.bad{background:#2a1117;border-color:#5c1d29;color:#fecdd3}\n@keyframes toastIn{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.modal{position:fixed;inset:0;background:rgba(2,4,8,0.75);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:80}.modal>div{background:var(--panel-solid);border:1px solid var(--line);border-radius:14px;padding:24px;width:min(460px,92vw);box-shadow:0 20px 40px rgba(0,0,0,0.8)}\n@media (max-width:960px){.editor{grid-template-columns:1fr;height:auto}.side{border-left:0;border-top:1px solid var(--line)}header.top{gap:10px;padding:0 12px}.busy{display:none!important}}\n</style></head><body>\n<header class=\"top\">\n<a class=\"brand\" href=\"#/\"><span class=\"mark\">MR</span> MotionRender <span class=\"vpill\">AI Studio</span></a>\n<nav class=\"main\"><a href=\"#/\" data-nav=\"home\">Projects</a><a href=\"#/library\" data-nav=\"library\">Library</a><a href=\"#/settings\" data-nav=\"settings\">Settings</a></nav>\n<div class=\"grow\"></div>\n<a class=\"pbadge\" id=\"pbadge\" href=\"#/settings\"><span class=\"pdot\"></span><span id=\"pbadgetext\">AI Ready</span></a>\n<div class=\"busy\" id=\"busy\"><div class=\"spin\"></div><span id=\"busytext\"></span></div>\n<a class=\"btn\" href=\"#/new\">+ New Video</a>\n</header>\n<div id=\"app\"></div>\n<script>\nvar $ = function (s, r) { return (r || document).querySelector(s); };\nfunction h(tag, attrs, kids) {\n var el = document.createElement(tag);\n for (var k in (attrs || {})) { var v = attrs[k]; if (v === false || v == null) continue; if (k === 'class') el.className = v; else if (k.slice(0, 2) === 'on') el.addEventListener(k.slice(2), v); else if (k === 'text') el.textContent = v; else el.setAttribute(k, v === true ? '' : v); }\n [].concat(kids == null ? [] : kids).forEach(function (c) { if (c == null || c === false) return; el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); });\n return el;\n}\nfunction api(path, body) {\n var opt = body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) };\n return fetch(path, opt).then(function (r) { return r.text().then(function (t) { var j = {}; try { j = JSON.parse(t); } catch (e) { j = { error: t }; } if (!r.ok) throw new Error(j.error || r.statusText); return j; }); });\n}\nfunction toast(msg, bad) { var t = h('div', { class: 'toast' + (bad ? ' bad' : ''), text: msg }); document.body.appendChild(t); setTimeout(function () { t.remove(); }, bad ? 6000 : 2800); }\nfunction ago(ms) { var s = Math.max(1, Math.round((Date.now() - ms) / 1000)); if (s < 60) return 'just now'; if (s < 3600) return Math.round(s / 60) + ' min ago'; if (s < 86400) return Math.round(s / 3600) + ' h ago'; return Math.round(s / 86400) + ' d ago'; }\nfunction dur(sec) { var m = Math.floor(sec / 60), s = Math.round(sec - m * 60); return m ? m + ':' + (s < 10 ? '0' : '') + s : s + 's'; }\nfunction ask(title, value, ok) {\n var input = h('input', { value: value || '' }), m = h('div', { class: 'modal' }, h('div', null, [h('h2', { text: title }), input, h('div', { class: 'bar', style: 'margin:18px 0 0;justify-content:flex-end' }, [\n h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }), h('button', { class: 'btn', text: 'OK', onclick: function () { m.remove(); ok(input.value.trim()); } })])]));\n document.body.appendChild(m); input.focus(); input.select(); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { m.remove(); ok(input.value.trim()); } if (e.key === 'Escape') m.remove(); });\n}\nfunction confirmBox(text, ok) { var m = h('div', { class: 'modal' }, h('div', null, [h('p', { text: text, style: 'font-size:15px;line-height:1.5;margin:0 0 16px' }), h('div', { class: 'bar', style: 'justify-content:flex-end' }, [h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }), h('button', { class: 'btn', text: 'Yes, proceed', onclick: function () { m.remove(); ok(); } })])])); document.body.appendChild(m); }\n\n/* provider status polling in top bar */\nfunction updateStatusBadge() {\n api('/api/status').then(function (s) {\n var ptext = $('#pbadgetext'); if (!ptext) return;\n if (s.provider) ptext.textContent = (s.provider.id === 'freellmapi' ? '\uD83D\uDE80 FreeLLMAPI \u00B7 ' : '\u2728 ' + s.provider.id + ' \u00B7 ') + s.provider.model;\n else ptext.textContent = '\uD83D\uDCCB Paste JSON mode';\n }, function () {});\n}\n\n/* jobs: one event stream, a promise per job */\nvar jobs = {}, jobWaiters = {};\nfunction onJob(job) {\n jobs[job.id] = job; var running = Object.keys(jobs).map(function (k) { return jobs[k]; }).filter(function (j) { return j.status === 'running' || j.status === 'queued'; });\n $('#busy').classList.toggle('on', running.length > 0); var last = running[0]; $('#busytext').textContent = last ? (last.messages.length ? last.messages[last.messages.length - 1] : 'Working on ' + last.kind + '\u2026') : '';\n (jobWaiters[job.id] || []).forEach(function (w) { w.progress(job); if (job.status === 'done') w.ok(job); if (job.status === 'error') w.fail(new Error(job.error || 'Failed')); });\n if (job.status === 'done' || job.status === 'error') delete jobWaiters[job.id];\n}\nfunction connect() {\n var es = new EventSource('/api/events');\n es.addEventListener('snapshot', function (e) { JSON.parse(e.data).jobs.forEach(onJob); });\n es.addEventListener('job', function (e) { onJob(JSON.parse(e.data)); });\n}\nfunction waitJob(job, progress) {\n return new Promise(function (ok, fail) {\n var cur = jobs[job.id] || job; if (cur.status === 'done') return ok(cur); if (cur.status === 'error') return fail(new Error(cur.error));\n (jobWaiters[job.id] = jobWaiters[job.id] || []).push({ ok: ok, fail: fail, progress: progress || function () {} });\n var poll = setInterval(function () { if (!jobWaiters[job.id]) return clearInterval(poll); api('/api/job?id=' + job.id).then(function (r) { onJob(r.job); }, function () {}); }, 3500);\n });\n}\nfunction run(path, body, progress) { return api(path, body).then(function (r) { return waitJob(r.job, progress); }).then(function (j) { return j.result; }); }\n\n/* routes */\nvar app = $('#app'), cleanup = null;\nfunction route() {\n if (cleanup) { cleanup(); cleanup = null; }\n var hash = location.hash.replace('#', '') || '/', parts = hash.split('/').filter(Boolean), view = parts[0] || 'home';\n document.querySelectorAll('[data-nav]').forEach(function (a) { a.classList.toggle('on', a.getAttribute('data-nav') === (view === 'new' || view === 'p' ? 'home' : view)); });\n app.innerHTML = ''; app.scrollTop = 0;\n var go = { home: home, new: create, library: library, settings: settings }[view];\n if (view === 'p' && parts[1]) return editor(decodeURIComponent(parts[1]), parts[2]);\n if (view === 'making' && parts[1]) return making(decodeURIComponent(parts[1]), parts[2]);\n (go || home)();\n}\nwindow.addEventListener('hashchange', route);\nfunction fail(el, e) { el.appendChild(h('div', { class: 'err', text: e && e.message ? e.message : String(e) })); }\n\n/* projects dashboard */\nfunction home() {\n var page = h('div', { class: 'page' }); app.appendChild(page);\n var hero = h('div', { class: 'hero' }, [\n h('div', null, [h('h1', { text: 'Your Videos' }), h('p', { class: 'lead', text: 'All your video projects. Click any project to open the live editor, re-style it, or export MP4.' })]),\n h('div', { class: 'bar' }, [h('a', { class: 'btn', href: '#/new', text: '+ New video' })])\n ]);\n page.appendChild(hero);\n api('/api/projects').then(function (r) {\n if (!r.projects.length) {\n page.appendChild(h('div', { class: 'empty' }, [\n h('span', { class: 'empty-icon', text: '\uD83C\uDFAC' }),\n h('h2', { text: 'No video projects yet' }),\n h('p', { class: 'lead', style: 'margin-bottom:20px', text: 'Describe an idea with AI, or paste composition JSON generated in ChatGPT or Claude.' }),\n h('a', { class: 'btn', href: '#/new', text: 'Create your first video' })\n ]));\n return;\n }\n var grid = h('div', { class: 'grid' }); r.projects.forEach(function (p) { grid.appendChild(card(p)); }); page.appendChild(grid);\n [].reduce.call(grid.querySelectorAll('img'), function (chain, img) { return chain.then(function () { return new Promise(function (ok) { img.onload = img.onerror = ok; img.src = img.getAttribute('data-src'); }); }); }, Promise.resolve());\n }, function (e) { fail(page, e); });\n}\nfunction card(p) {\n var open = function () { location.hash = '#/p/' + encodeURIComponent(p.id); };\n var img = h('img', { 'data-src': '/api/thumb?id=' + encodeURIComponent(p.id), alt: '' });\n img.addEventListener('error', function () { img.replaceWith(h('div', { class: 'ph', text: '\u25B6 Preview' })); });\n var aspectLabel = p.width < p.height ? '9:16 vertical' : p.width === p.height ? '1:1 square' : '16:9 wide';\n return h('div', { class: 'card' }, [\n h('div', { class: 'thumb', onclick: open }, [\n img,\n h('span', { class: 'abadge', text: aspectLabel }),\n h('span', { class: 'tbadge', text: '\u25B6 ' + dur(p.seconds) })\n ]),\n h('div', { class: 'cardbody' }, [\n h('b', { text: p.name, title: p.name }),\n h('div', { class: 'meta', text: p.width + '\u00D7' + p.height + ' \u00B7 ' + p.scenes + ' scene(s)' }),\n h('div', { class: 'meta' }, ['Edited ' + ago(p.modified) + ' ', h('span', { class: 'tag' + (p.mode === 'template' ? ' ok' : ''), text: p.mode === 'template' ? 'editable text' : 'custom' })])\n ]),\n h('div', { class: 'cardfoot' }, [\n h('button', { class: 'btn sm', text: 'Open Studio', onclick: open }),\n h('button', { class: 'btn sec sm', text: 'Duplicate', onclick: function () { api('/api/project/duplicate', { id: p.id }).then(function () { toast('Duplicated project'); route(); }, function (e) { toast(e.message, true); }); } }),\n h('button', { class: 'btn sec sm danger', text: 'Delete', onclick: function () { confirmBox('Delete \"' + p.name + '\"? It moves to .trash folder.', function () { api('/api/project/delete', { id: p.id }).then(function () { toast('Deleted'); route(); }, function (e) { toast(e.message, true); }); }); } })\n ])\n ]);\n}\n\n/* create screen with dual mode */\nvar kinds = [\n ['reel', '\uD83D\uDCF1 Social Reel', 'Vertical 9:16, ~20s. For TikTok, Shorts, Reels'],\n ['product', '\u23F9\uFE0F Product Video', 'Square 1:1, ~18s. For Instagram / LinkedIn feeds'],\n ['promo', '\uD83D\uDCFA Promo Video', 'Wide 16:9, ~30s. For YouTube, landing pages'],\n ['project', '\uD83D\uDCBC Case Study', 'Wide 16:9, ~60s. For case studies, deep dives']\n];\nvar examples = [\n 'Vertical reel explaining panic attacks and how the 4-7-8 breathing technique stops the spiral',\n 'Launch video for a budgeting app called Pocket, highlighting 3 key features and download link',\n 'Product card for a luxury hand-poured soy candle with free shipping offer',\n 'Event invite for a tech and design meetup in Berlin with RSVP call-to-action'\n];\n\nfunction create() {\n var activeMode = 'ai'; // 'ai' or 'paste'\n var kind = 'reel', files = [], page = h('div', { class: 'page narrow' }); app.appendChild(page);\n\n var modeSwitch = h('div', { class: 'mode-switch' });\n var btnAi = h('button', { class: 'on', text: '\u2728 Generate with AI' });\n var btnPaste = h('button', { text: '\uD83D\uDCCB Paste JSON (ChatGPT / Claude)' });\n modeSwitch.append(btnAi, btnPaste);\n\n var contentAi = h('div');\n var contentPaste = h('div', { style: 'display:none' });\n\n btnAi.addEventListener('click', function () {\n activeMode = 'ai'; btnAi.classList.add('on'); btnPaste.classList.remove('on');\n contentAi.style.display = 'block'; contentPaste.style.display = 'none';\n });\n btnPaste.addEventListener('click', function () {\n activeMode = 'paste'; btnPaste.classList.add('on'); btnAi.classList.remove('on');\n contentAi.style.display = 'none'; contentPaste.style.display = 'block';\n });\n\n page.append(h('h1', { text: 'Create New Video' }), h('p', { class: 'lead', style: 'margin-bottom:20px', text: 'Build with your connected AI provider, or paste composition JSON generated in ChatGPT or Claude.' }), modeSwitch, contentAi, contentPaste);\n\n /* Tab 1: AI Generation */\n var brief = h('textarea', { placeholder: 'What is your video about? Who is it for? Key points or message?', style: 'min-height:120px' });\n var name = h('input', { placeholder: 'Optional. Auto-generated from your brief if left empty' });\n var tiles = h('div', { class: 'tiles' }), fileList = h('div', { class: 'files' }), errBox = h('div');\n var drawTiles = function () {\n tiles.innerHTML = '';\n kinds.forEach(function (k) {\n tiles.appendChild(h('button', { class: 'tile' + (kind === k[0] ? ' on' : ''), onclick: function () { kind = k[0]; drawTiles(); } }, [h('b', { text: k[1] }), h('span', { text: k[2] })]));\n });\n };\n drawTiles();\n\n var drawFiles = function () {\n fileList.innerHTML = '';\n files.forEach(function (f, i) {\n fileList.appendChild(h('span', { text: f.name + ' \u00D7', style: 'cursor:pointer', onclick: function () { files.splice(i, 1); drawFiles(); } }));\n });\n };\n var picker = h('input', { type: 'file', multiple: true, accept: 'image/*,video/*,audio/*', style: 'display:none', onchange: function () { [].forEach.call(picker.files, function (f) { files.push(f); }); drawFiles(); picker.value = ''; } });\n var drop = h('div', { class: 'drop' }, ['Drag & drop photos or clips here, or ', h('a', { href: '#', text: 'browse files', onclick: function (e) { e.preventDefault(); picker.click(); } }), picker]);\n drop.addEventListener('dragover', function (e) { e.preventDefault(); drop.classList.add('over'); });\n drop.addEventListener('dragleave', function () { drop.classList.remove('over'); });\n drop.addEventListener('drop', function (e) { e.preventDefault(); drop.classList.remove('over'); [].forEach.call(e.dataTransfer.files, function (f) { files.push(f); }); drawFiles(); });\n\n var duration = h('input', { type: 'number', min: '3', placeholder: 'Default for kind' });\n var aspect = h('select', null, [['', 'Default for format'], ['9:16', 'Vertical 9:16 (Shorts/Reels)'], ['1:1', 'Square 1:1 (Feed)'], ['16:9', 'Wide 16:9 (Landscape)'], ['4:5', 'Portrait 4:5']].map(function (a) { return h('option', { value: a[0], text: a[1] }); }));\n var voice = h('input', { type: 'checkbox' }), review = h('input', { type: 'checkbox' }), full = h('input', { type: 'checkbox' }), go = h('button', { class: 'btn', text: '\u2728 Generate Video' });\n var chips = h('div', { class: 'chips' }, examples.map(function (t) {\n return h('button', { class: 'chip', text: t.length > 56 ? t.slice(0, 54) + '\u2026' : t, title: t, onclick: function () { brief.value = t; } });\n }));\n\n contentAi.append(\n h('label', { class: 'f' }, [h('span', { text: 'Format & Aspect Ratio' }), tiles]),\n h('label', { class: 'f' }, [h('span', { text: 'Video Brief' }), brief]),\n chips,\n h('label', { class: 'f' }, [h('span', { text: 'Media Assets (optional)' }), drop, fileList]),\n h('details', null, [\n h('summary', { text: 'Advanced Settings' }),\n h('div', { style: 'margin-top:14px' }, [\n h('label', { class: 'f' }, [h('span', { text: 'Project Name' }), name]),\n h('div', { class: 'two' }, [h('label', { class: 'f' }, [h('span', { text: 'Duration in seconds' }), duration]), h('label', { class: 'f' }, [h('span', { text: 'Aspect Ratio Override' }), aspect])]),\n h('label', { class: 'f' }, [voice, ' Add voiceover narration and captions']),\n h('label', { class: 'f' }, [review, ' Automatic review and self-correction pass']),\n h('label', { class: 'f' }, [full, ' Write every layer from scratch (experimental, without templates)'])\n ])\n ]),\n errBox,\n h('div', { style: 'margin-top:16px' }, go)\n );\n\n go.addEventListener('click', function () {\n errBox.innerHTML = ''; var text = brief.value.trim(); if (!text) { fail(errBox, new Error('Please describe your video in the brief')); return; }\n go.disabled = true; go.textContent = 'Starting\u2026';\n var title = name.value.trim() || text.split(' ').slice(0, 6).join(' ');\n api('/api/project/new', { name: title, kind: kind }).then(function (made) {\n return files.reduce(function (p, f) {\n return p.then(function () {\n return fetch('/api/upload?project=' + encodeURIComponent(made.id) + '&name=' + encodeURIComponent(f.name), { method: 'POST', headers: { 'content-type': 'application/octet-stream' }, body: f }).then(function (r) { if (!r.ok) return r.json().then(function (j) { throw new Error(j.error || 'Upload failed'); }); });\n });\n }, Promise.resolve()).then(function () {\n var body = { brief: text, kind: kind, folder: made.id, narration: voice.checked, review: review.checked, full: full.checked };\n if (duration.value) body.duration = duration.value;\n if (aspect.value) body.aspect = aspect.value;\n return api('/api/compose', body);\n }).then(function (r) {\n location.hash = '#/making/' + encodeURIComponent(made.id) + '/' + r.job.id;\n });\n }).catch(function (e) { go.disabled = false; go.textContent = '\u2728 Generate Video'; fail(errBox, e); });\n });\n\n /* Tab 2: Paste JSON Workflow */\n var pasteBrief = h('textarea', { placeholder: 'Brief for AI prompt generator (e.g. 20-second vertical reel on panic attack recovery)...', style: 'min-height:75px' });\n var pasteKind = h('select', null, [['reel', 'Vertical Reel (9:16)'], ['product', 'Square (1:1)'], ['promo', 'Landscape Promo (16:9)'], ['project', 'Case Study (16:9)']].map(function (k) { return h('option', { value: k[0], text: k[1] }); }));\n var pastePromptBtn = h('button', { class: 'btn sec', text: '\uD83D\uDCCB Copy AI Prompt Cheatsheet' });\n var pasteInput = h('textarea', { placeholder: 'Paste valid composition JSON starting with { \"name\": ..., \"scenes\": [...] } here...', style: 'min-height:160px;font:12.5px/1.5 ui-monospace,Consolas,monospace' });\n var pasteFolder = h('input', { placeholder: 'Optional folder name (e.g. panic-attack-reel)' });\n var pasteGo = h('button', { class: 'btn', text: '\u26A1 Import & Launch Studio' });\n var pasteErr = h('div');\n\n pastePromptBtn.addEventListener('click', function () {\n pastePromptBtn.disabled = true; pastePromptBtn.textContent = 'Generating prompt\u2026';\n api('/api/prompt', { brief: pasteBrief.value.trim() || 'A modern promotional video', kind: pasteKind.value, schema: true }).then(function (r) {\n pastePromptBtn.disabled = false; pastePromptBtn.textContent = '\uD83D\uDCCB Copy AI Prompt Cheatsheet';\n if (navigator.clipboard) {\n navigator.clipboard.writeText(r.prompt).then(function () { toast('Copied prompt cheatsheet to clipboard!'); }, function () { toast('Prompt ready'); });\n } else {\n toast('Prompt ready');\n }\n }, function (e) {\n pastePromptBtn.disabled = false; pastePromptBtn.textContent = '\uD83D\uDCCB Copy AI Prompt Cheatsheet';\n fail(pasteErr, e);\n });\n });\n\n pasteGo.addEventListener('click', function () {\n pasteErr.innerHTML = ''; var raw = pasteInput.value.trim();\n if (!raw) { fail(pasteErr, new Error('Please paste your composition JSON first')); return; }\n pasteGo.disabled = true; pasteGo.textContent = 'Importing\u2026';\n api('/api/import', { json: raw, folder: pasteFolder.value.trim() || undefined }).then(function (r) {\n return waitJob(r.job, function (j) {\n if (j.messages.length) pasteGo.textContent = j.messages[j.messages.length - 1];\n });\n }).then(function (done) {\n var projId = (done.result && (done.result.id || done.result.project)) || (done.result && done.result.relative && done.result.relative.split('/')[0]) || pasteFolder.value.trim() || 'imported-video';\n toast('Imported video successfully!');\n location.hash = '#/p/' + encodeURIComponent(projId);\n }).catch(function (e) {\n pasteGo.disabled = false; pasteGo.textContent = '\u26A1 Import & Launch Studio';\n fail(pasteErr, e);\n });\n });\n\n contentPaste.append(\n h('div', { class: 'box-info', text: 'Zero-key workflow: Copy the official schema prompt into ChatGPT, Claude or FreeLLMAPI chat, then paste the returned JSON below.' }),\n h('h2', { style: 'margin-top:16px', text: 'Step 1: Get AI Chat Prompt' }),\n h('div', { class: 'two' }, [\n h('label', { class: 'f' }, [h('span', { text: 'Format' }), pasteKind]),\n h('label', { class: 'f' }, [h('span', { text: 'Video Topic' }), pasteBrief])\n ]),\n pastePromptBtn,\n h('h2', { style: 'margin-top:24px', text: 'Step 2: Paste JSON from AI' }),\n h('label', { class: 'f' }, [h('span', { text: 'Composition JSON' }), pasteInput]),\n h('label', { class: 'f' }, [h('span', { text: 'Project Folder Name (optional)' }), pasteFolder]),\n pasteErr,\n h('div', { style: 'margin-top:16px' }, pasteGo)\n );\n}\n\n/* live progress */\nfunction making(id, jobId) {\n var log = h('div', { class: 'log' }), title = h('h1', { text: 'Building your video\u2026' });\n var box = h('div', { class: 'center' }, [\n h('div', { class: 'spin' }),\n title,\n h('p', { class: 'lead', style: 'margin-bottom:18px', text: 'Writing scenes, positioning animations, and compiling design tokens. Ready in under a minute.' }),\n log\n ]);\n app.appendChild(box);\n waitJob({ id: jobId }, function (j) {\n log.textContent = j.messages.join(String.fromCharCode(10));\n log.scrollTop = log.scrollHeight;\n }).then(function () {\n location.hash = '#/p/' + encodeURIComponent(id);\n }, function (e) {\n var sp = box.querySelector('.spin'); if (sp) sp.remove();\n title.textContent = 'Could not finish video';\n fail(box, e);\n box.appendChild(h('div', { class: 'bar', style: 'justify-content:center;margin-top:20px' }, [\n h('a', { class: 'btn', href: '#/new', text: 'Try again' }),\n h('button', { class: 'btn sec', text: 'Delete empty folder', onclick: function () { api('/api/project/delete', { id: id }).then(function () { location.hash = '#/'; }); } })\n ]));\n });\n}\n\n/* full studio editor */\nfunction editor(id, tabHint) {\n var d, player = null, tab = tabHint || 'edit', box = h('div', { class: 'editor' }), stage = h('div', { class: 'stage' }), side = h('div', { class: 'side' });\n box.append(stage, side); app.appendChild(box); app.style.overflow = 'hidden';\n var destroyed = false;\n cleanup = function () {\n destroyed = true; document.removeEventListener('keydown', keys); app.style.overflow = '';\n if (player) { try { player.destroy(); } catch (e) {} }\n };\n var file = id + '/composition.json';\n var savedNote = h('span', { class: 'saved', text: '\u2713 Ready' });\n var screen = h('div', { class: 'screen' }, h('div', { id: 'stage-player' }));\n var sceneRow = h('div', { class: 'scenes' });\n var nameInput = h('input', { title: 'Click to rename project' });\n var pills = h('span', { class: 'tag', style: 'margin-left:4px' });\n\n var playBtn = h('button', { class: 'btn sm', text: '\u25B6 Play' });\n var scrub = h('input', { type: 'range', min: '0', max: '1', value: '0', step: '1' });\n var clock = h('span', { class: 't' });\n var xport = h('div', { class: 'xport' }, [playBtn, scrub, clock]);\n\n stage.append(\n h('div', { class: 'etop' }, [\n h('a', { class: 'back-link', href: '#/', text: '\u2190 Projects' }),\n nameInput,\n pills,\n h('div', { class: 'grow' }),\n savedNote,\n h('button', { class: 'btn sm sec', text: 'Export MP4', onclick: function () { tab = 'export'; drawTabs(); drawPane(); } })\n ]),\n screen,\n xport,\n sceneRow\n );\n\n function tick(f) {\n scrub.value = String(f);\n clock.textContent = dur(f / d.fps) + ' / ' + dur(d.durationFrames / d.fps) + ' \u00B7 f' + f;\n }\n playBtn.addEventListener('click', function () {\n if (!player) return;\n if (player.playing) player.pause(); else player.play();\n });\n scrub.addEventListener('input', function () {\n if (player) {\n if (player.playing) player.pause();\n player.seek(Number(scrub.value));\n tick(Number(scrub.value));\n }\n });\n document.addEventListener('keydown', keys);\n function keys(e) {\n if (e.key === ' ' && player && e.target === document.body) { e.preventDefault(); playBtn.click(); }\n }\n\n var tabs = h('div', { class: 'tabs' }), pane = h('div', { class: 'pane' }); side.append(tabs, pane);\n var names = [['edit', '\u270D\uFE0F Edit'], ['ai', '\u2728 Ask AI'], ['look', '\uD83C\uDFA8 Look'], ['history', '\uD83D\uDD52 History'], ['export', '\uD83D\uDE80 Export']];\n function drawTabs() {\n tabs.innerHTML = '';\n names.forEach(function (n) {\n tabs.appendChild(h('button', { class: tab === n[0] ? 'on' : '', text: n[1], onclick: function () { tab = n[0]; drawTabs(); drawPane(); } }));\n });\n }\n\n function playerStart(frame) {\n if (player) { try { player.destroy(); } catch (e) {} player = null; }\n $('#stage-player').innerHTML = '';\n return import('/player.js').then(function (m) {\n return m.createPlayer($('#stage-player'), {\n composition: d.source.raw,\n media: d.source.media,\n assetBase: location.origin + d.source.base,\n allowCode: false,\n controls: false\n });\n }).then(function (p) {\n if (destroyed) { p.destroy(); return; }\n player = p; scrub.max = String(Math.max(1, d.durationFrames - 1));\n tick(frame || 0); playBtn.textContent = '\u25B6 Play';\n p.on('frame', function (f) { tick(typeof f === 'number' ? f : p.frame); });\n p.on('play', function () { playBtn.textContent = '\u23F8 Pause'; });\n p.on('pause', function () { playBtn.textContent = '\u25B6 Play'; });\n p.on('ended', function () { playBtn.textContent = '\u25B6 Play'; });\n p.on('error', function (e) { savedNote.textContent = 'Preview error: ' + (e && e.message ? e.message : e); });\n if (frame) p.seek(frame);\n }).catch(function (e) {\n $('#stage-player').appendChild(h('div', { class: 'err', text: 'Preview player error: ' + e.message }));\n });\n }\n\n function load(keepFrame) {\n var frame = keepFrame && player ? player.frame : 0;\n return api('/api/project?id=' + encodeURIComponent(id)).then(function (r) {\n d = r; nameInput.value = d.name;\n pills.textContent = d.width + '\u00D7' + d.height + ' \u00B7 ' + d.fps + 'fps';\n drawScenes();\n return playerStart(frame);\n }).then(function () { drawPane(); }, function (e) { pane.innerHTML = ''; fail(pane, e); });\n }\n\n function drawScenes() {\n sceneRow.innerHTML = '';\n d.scenes.forEach(function (s, idx) {\n var btn = h('button', { onclick: function () { if (player) { player.seek(s.startFrame); tick(s.startFrame); } } }, [\n (idx + 1) + '. ' + s.name,\n h('small', { text: dur(s.durationFrames / d.fps) })\n ]);\n sceneRow.appendChild(btn);\n });\n }\n\n nameInput.addEventListener('change', function () {\n var v = nameInput.value.trim(); if (!v) { nameInput.value = d.name; return; }\n api('/api/project/rename', { id: id, name: v }).then(function () { d.name = v; savedNote.textContent = '\u2713 Renamed'; }, function (e) { toast(e.message, true); });\n });\n\n function save(path, body, note) {\n savedNote.textContent = 'Saving\u2026';\n return api(path, body).then(function () {\n savedNote.textContent = '\u2713 ' + (note || 'Saved');\n return load(true);\n }, function (e) { savedNote.textContent = 'Error'; toast(e.message, true); throw e; });\n }\n\n function drawPane() {\n pane.innerHTML = ''; if (!d) return;\n ({ edit: paneEdit, ai: paneAi, look: paneLook, history: paneHistory, export: paneExport }[tab])();\n }\n\n var themeNames = ['bg', 'surface', 'text', 'muted', 'accent', 'accent2', 'radius', 'headWeight'];\n\n function paneEdit() {\n var fields = [], apply = h('button', { class: 'btn', text: 'Save changes' }), box2 = h('div');\n if (d.mode === 'template') {\n d.variables.filter(function (v) { return themeNames.indexOf(v.name) < 0; }).forEach(function (v) {\n var input, label = v.description ? v.description : v.name.replace(/([A-Z])/g, ' $1').replace(/[-_]/g, ' ');\n if (v.type === 'boolean') { input = h('input', { type: 'checkbox' }); input.checked = !!v.value; }\n else if (v.type === 'color') input = h('input', { type: 'color', value: /^#[0-9a-fA-F]{6}$/.test(v.value) ? v.value : '#000000' });\n else if (v.type === 'number') input = h('input', { type: 'number', value: String(v.value) });\n else if (v.type === 'list') { input = h('textarea', { style: 'min-height:120px;font:12px ui-monospace,Consolas,monospace' }); input.value = JSON.stringify(v.value, null, 2); }\n else { input = h('textarea', { style: 'min-height:' + (String(v.value).length > 60 ? 90 : 42) + 'px' }); input.value = v.value == null ? '' : String(v.value); }\n fields.push({ v: v, input: input });\n box2.appendChild(h('label', { class: 'f' }, [h('span', { text: label }), input]));\n });\n if (!fields.length) box2.appendChild(h('p', { class: 'lead', text: 'This video has no exposed variable fields. Use Ask AI to adjust scene text.' }));\n apply.addEventListener('click', function () {\n var values = {};\n try {\n fields.forEach(function (f) {\n var v = f.v, x = f.input;\n values[v.name] = v.type === 'boolean' ? x.checked : v.type === 'number' ? Number(x.value) : v.type === 'list' ? JSON.parse(x.value) : x.value;\n });\n } catch (e) { toast('A list is not valid JSON: ' + e.message, true); return; }\n save('/api/variables', { id: id, values: values }, 'Saved');\n });\n } else {\n d.texts.forEach(function (t) {\n var input = h('textarea', { style: 'min-height:' + (t.content.length > 60 ? 80 : 40) + 'px' });\n input.value = t.content;\n fields.push({ t: t, input: input });\n box2.appendChild(h('label', { class: 'f' }, [h('span', { text: t.scene + ' \u00B7 ' + t.id }), input]));\n });\n if (!fields.length) box2.appendChild(h('p', { class: 'lead', text: 'No text layers found in this video.' }));\n apply.addEventListener('click', function () {\n var changed = fields.filter(function (f) { return f.input.value.trim() && f.input.value !== f.t.content; });\n changed.reduce(function (p, f) {\n return p.then(function () { return api('/api/text', { id: id, path: f.t.path, content: f.input.value }); });\n }, Promise.resolve()).then(function () {\n savedNote.textContent = changed.length ? '\u2713 Saved' : 'No changes';\n return load(true);\n }, function (e) { toast(e.message, true); });\n });\n }\n pane.append(\n h('h2', { text: 'Content & Text' }),\n h('p', { class: 'lead', style: 'font-size:13px;margin-bottom:16px', text: d.mode === 'template' ? 'Update headlines, copy, and variables. The visual layout adjusts dynamically.' : 'Every text line in the video in appearance order.' }),\n box2,\n fields.length ? apply : null\n );\n }\n\n function paneAi() {\n var text = h('textarea', { placeholder: 'e.g. Make it snappier, change the call to action, or add more energy', style: 'min-height:110px' });\n var go = h('button', { class: 'btn', text: '\u2728 Apply AI Change' }), errBox = h('div');\n var chips = h('div', { class: 'chips' }, [\n 'Make pace faster and punchier',\n 'Shorten all captions',\n 'Add a stronger call to action at the end',\n 'Calmer and more minimal',\n 'Use bolder, larger typography'\n ].map(function (t) { return h('button', { class: 'chip', text: t, onclick: function () { text.value = t; } }); }));\n\n pane.append(\n h('h2', { text: 'Ask AI to Refine Video' }),\n h('p', { class: 'lead', style: 'font-size:13px', text: 'Describe what you want to change in plain English. Every edit is saved to History so you can rollback anytime.' }),\n text,\n chips,\n d.mode === 'template' ? h('div', { class: 'note', text: 'Note: AI refinements unlock custom scene-by-scene editing.' }) : null,\n errBox,\n go\n );\n go.addEventListener('click', function () {\n errBox.innerHTML = ''; if (!text.value.trim()) return;\n go.disabled = true; go.textContent = 'Refining\u2026';\n run('/api/refine', { file: file, instruction: text.value.trim() }, function (j) {\n savedNote.textContent = j.messages.length ? j.messages[j.messages.length - 1] : 'Working\u2026';\n }).then(function (r) {\n savedNote.textContent = '\u2713 Updated';\n toast('Video refined successfully' + (r.warnings && r.warnings.length ? ' (with notes)' : ''));\n return load(false);\n }, function (e) {\n go.disabled = false; go.textContent = '\u2728 Apply AI Change';\n fail(errBox, e);\n });\n });\n }\n\n function paneLook() {\n pane.append(h('h2', { text: 'Look & Style' }));\n var themes = h('div', { class: 'themes' }), blocks = h('div', { class: 'blocklist' }), cat = h('select', { style: 'margin-bottom:12px' }), all = [];\n pane.append(\n h('p', { class: 'lead', style: 'font-size:13px', text: 'Pick a color palette to re-style every scene instantly.' }),\n themes,\n h('h2', { style: 'margin-top:28px', text: 'Add Scene Block' }),\n cat,\n blocks\n );\n api('/api/library').then(function (lib) {\n var canTheme = Object.keys(d.theme).length > 0;\n lib.themes.forEach(function (t) {\n var v = t.variables;\n var b = h('button', { class: 'theme', disabled: !canTheme, title: canTheme ? t.description : 'This video does not use theme tokens', onclick: function () { save('/api/theme', { id: id, theme: t.id }, 'Theme updated'); } }, [\n h('div', { class: 'sw' }, [v.bg, v.surface, v.accent, v.accent2].map(function (c) { return h('i', { style: 'background:' + c }); })),\n h('b', { text: t.name })\n ]);\n themes.appendChild(b);\n });\n all = lib.blocks;\n var cats = ['all'].concat(all.map(function (b) { return b.category; }).filter(function (c, i, a) { return a.indexOf(c) === i; }));\n cat.innerHTML = '';\n cats.forEach(function (c) { cat.appendChild(h('option', { value: c, text: c === 'all' ? 'All categories' : c })); });\n var draw = function () {\n blocks.innerHTML = '';\n all.filter(function (b) { return cat.value === 'all' || b.category === cat.value; }).forEach(function (b) {\n blocks.appendChild(h('div', { class: 'block' }, [\n h('div', null, [h('b', { text: b.name }), h('small', { text: b.description })]),\n h('button', { class: 'btn sec sm', text: '+ Add', onclick: function () { save('/api/add-block', { id: id, block: b.id }, 'Added ' + b.name).then(function () { toast('Added ' + b.name); }, function () {}); } })\n ]));\n });\n };\n cat.addEventListener('change', draw); draw();\n }, function (e) { fail(pane, e); });\n }\n\n function paneHistory() {\n pane.append(h('h2', { text: 'Version History' }), h('p', { class: 'lead', style: 'font-size:13px', text: 'Every change is versioned. Roll back anytime.' }));\n var list = h('div'); pane.appendChild(list);\n api('/api/history?file=' + encodeURIComponent(file)).then(function (r) {\n if (!r.revisions.length) { list.appendChild(h('p', { class: 'lead', text: 'No recorded versions yet.' })); return; }\n r.revisions.slice().reverse().forEach(function (rev, i) {\n list.appendChild(h('div', { class: 'hist' }, [\n h('div', null, [\n h('b', { text: '#' + rev.n + ' \u00B7 ' + rev.kind }),\n h('small', { text: rev.label }),\n h('small', { text: new Date(rev.at).toLocaleString() })\n ]),\n i === 0 ? h('span', { class: 'tag ok', text: 'current' }) : h('button', { class: 'btn sec sm', text: 'Rollback', onclick: function () {\n api('/api/restore', { file: file, n: rev.n }).then(function () { savedNote.textContent = 'Restored #' + rev.n; return load(false); }, function (e) { toast(e.message, true); });\n } })\n ]));\n });\n }, function (e) { fail(list, e); });\n }\n\n function paneExport() {\n var quality = h('select', null, [['1', 'Full HD (100% scale)'], ['0.75', 'Standard (75% scale)'], ['0.5', 'Fast Draft (50% scale)']].map(function (o) { return h('option', { value: o[0], text: o[1] }); }));\n var go = h('button', { class: 'btn', text: '\uD83C\uDFAC Render MP4 Video' }), out = h('div', { class: 'result' });\n var checkBtn = h('button', { class: 'btn sec', text: '\uD83D\uDD0D Check for issues' }), thumbBtn = h('button', { class: 'btn sec', text: '\uD83D\uDDBC\uFE0F Make Thumbnail' });\n var errBox = h('div');\n\n pane.append(\n h('h2', { text: 'Export & Render' }),\n h('p', { class: 'lead', style: 'font-size:13px', text: d.width + '\u00D7' + d.height + ' \u00B7 ' + dur(d.durationFrames / d.fps) + ' \u00B7 MP4' }),\n h('label', { class: 'f' }, [h('span', { text: 'Render Quality' }), quality]),\n go,\n h('div', { class: 'bar', style: 'margin:14px 0 0' }, [checkBtn, thumbBtn]),\n errBox,\n out\n );\n\n var guard = function (btn, label, fn) {\n btn.addEventListener('click', function () {\n errBox.innerHTML = ''; btn.disabled = true; btn.textContent = 'Working\u2026';\n fn().then(function () { btn.disabled = false; btn.textContent = label; }, function (e) { btn.disabled = false; btn.textContent = label; fail(errBox, e); });\n });\n };\n guard(go, '\uD83C\uDFAC Render MP4 Video', function () {\n return run('/api/render', { file: file, output: 'out.mp4', scale: Number(quality.value) }, function (j) {\n go.textContent = j.messages.length ? j.messages[j.messages.length - 1] : 'Rendering\u2026';\n }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: '\uD83C\uDF89 Your video is ready!' }),\n h('video', { src: r.url, controls: true }),\n h('div', { class: 'bar', style: 'margin:10px 0 0' }, [\n h('a', { class: 'btn sm', href: r.url, download: id + '.mp4', text: 'Download MP4' }),\n h('span', { class: 'saved', text: r.relative })\n ])\n );\n });\n });\n guard(checkBtn, '\uD83D\uDD0D Check for issues', function () {\n return run('/api/review', { file: file }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: r.issues.length ? r.issues.length + ' item(s) to review' : '\u2713 No issues found' }),\n h('ul', null, r.issues.slice(0, 12).map(function (i) { return h('li', { text: (i.message || String(i)) }); })),\n h('img', { src: r.url + '?t=' + Date.now() })\n );\n });\n });\n guard(thumbBtn, '\uD83D\uDDBC\uFE0F Make Thumbnail', function () {\n return run('/api/thumbnail', { file: file }).then(function (r) {\n out.innerHTML = '';\n out.append(\n h('b', { text: 'Generated Thumbnail' }),\n h('img', { src: r.url + '?t=' + Date.now() }),\n h('div', { class: 'bar', style: 'margin:10px 0 0' }, [\n h('a', { class: 'btn sm', href: r.url, download: id + '.png', text: 'Download PNG' })\n ])\n );\n });\n });\n }\n\n drawTabs(); load(false);\n}\n\n/* library screen */\nfunction library() {\n var page = h('div', { class: 'page' }); app.appendChild(page);\n page.append(\n h('div', { class: 'hero' }, [\n h('div', null, [h('h1', { text: 'Design Library' }), h('p', { class: 'lead', text: 'Curated color palettes and modular scene blocks ready to inject into any video.' })])\n ])\n );\n api('/api/library').then(function (lib) {\n page.appendChild(h('h2', { text: 'Color Palettes' }));\n var row = h('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fill,minmax(200px,1fr));margin-bottom:36px' });\n lib.themes.forEach(function (t) {\n var v = t.variables;\n row.appendChild(h('div', { class: 'card', style: 'padding:14px' }, [\n h('div', { class: 'sw' }, [v.bg, v.surface, v.accent, v.accent2].map(function (c) { return h('i', { style: 'background:' + c }); })),\n h('b', { text: t.name, style: 'font-size:14.5px;color:#fff' }),\n h('div', { class: 'meta', style: 'margin-top:4px', text: t.description })\n ]));\n });\n page.appendChild(row);\n var cats = lib.blocks.map(function (b) { return b.category; }).filter(function (c, i, a) { return a.indexOf(c) === i; });\n cats.forEach(function (c) {\n page.appendChild(h('div', { class: 'cat', text: c }));\n var g = h('div', { class: 'grid', style: 'grid-template-columns:repeat(auto-fill,minmax(260px,1fr));margin-bottom:24px' });\n lib.blocks.filter(function (b) { return b.category === c; }).forEach(function (b) {\n g.appendChild(h('div', { class: 'card', style: 'padding:16px;gap:8px' }, [\n h('b', { text: b.name, style: 'font-size:14.5px;color:#fff' }),\n h('div', { class: 'meta', text: b.description }),\n h('div', { class: 'meta', text: (b.kind === 'overlay' ? 'Overlay \u00B7 ' : 'Scene \u00B7 ') + dur(b.durationFrames / 30) }),\n h('button', { class: 'btn sec sm', style: 'align-self:flex-start;margin-top:6px', text: '+ Add to video\u2026', onclick: function () { pick(b); } })\n ]));\n });\n page.appendChild(g);\n });\n }, function (e) { fail(page, e); });\n}\n\nfunction pick(block) {\n api('/api/projects').then(function (r) {\n if (!r.projects.length) { toast('Create a video project first', true); return; }\n var sel = h('select', null, r.projects.map(function (p) { return h('option', { value: p.id, text: p.name }); }));\n var m = h('div', { class: 'modal' }, h('div', null, [\n h('h2', { text: 'Add ' + block.name + ' to project:' }),\n sel,\n h('div', { class: 'bar', style: 'margin:18px 0 0;justify-content:flex-end' }, [\n h('button', { class: 'btn sec', text: 'Cancel', onclick: function () { m.remove(); } }),\n h('button', { class: 'btn', text: 'Add Scene', onclick: function () {\n api('/api/add-block', { id: sel.value, block: block.id }).then(function () {\n m.remove(); location.hash = '#/p/' + encodeURIComponent(sel.value);\n }, function (e) { toast(e.message, true); });\n } })\n ])\n ]));\n document.body.appendChild(m);\n });\n}\n\n/* settings screen */\nfunction settings() {\n var page = h('div', { class: 'page narrow' }); app.appendChild(page);\n page.append(\n h('h1', { text: 'Studio Settings' }),\n h('p', { class: 'lead', style: 'margin-bottom:24px', text: 'Configuration, local storage path, and AI engine status.' })\n );\n api('/api/status').then(function (s) {\n var pinfo = s.provider ? s.provider.id.toUpperCase() + ' (' + s.provider.model + ')' : 'Not Connected';\n var rows = [\n ['Projects Directory', s.outDir],\n ['Active AI Provider', pinfo]\n ];\n rows.forEach(function (r) {\n page.appendChild(h('label', { class: 'f' }, [h('span', { text: r[0] }), h('input', { value: r[1], readonly: true })]));\n });\n if (s.error) {\n page.appendChild(h('div', { class: 'err', text: s.error + String.fromCharCode(10) + 'Tip: Add FREELLMAPI_API_KEY=... in your .env file, or use the zero-key Paste JSON workflow!' }));\n } else {\n page.appendChild(h('div', { class: 'note', text: 'Connected to ' + pinfo + '. AI generation and refinements will execute directly through this provider.' }));\n }\n page.appendChild(h('div', { class: 'box-info', text: 'Zero-API Key Workflow: You can also use ChatGPT or Claude directly by copying the prompt cheatsheet from \"+ New Video > Paste JSON\" and pasting the returned JSON.' }));\n page.appendChild(h('p', { class: 'meta', style: 'margin-top:20px', text: 'To change output directory, run: ove ai studio --out <custom-folder>' }));\n }, function (e) { fail(page, e); });\n}\n\nupdateStatusBadge();\nconnect();\nroute();\n</script></body></html>";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const usage = "Usage:\n ove ai doctor show the provider, model and capabilities\n ove ai capabilities [--json] list every registered capability the AI may use\n ove ai history <composition.json> list the kept revisions of an AI-made video\n ove ai restore <composition.json> <n> bring revision n back (nothing is lost: the restore is a revision too)\n ove ai compose <dir> --brief \"...\" [--kind reel|product|promo|project] [--aspect 9:16] [--duration 20] [--fps 30] [--full]\n [--narration] [--review] [--render] [--force] [--dry-run] [--asset id=path] [--fields name[:type],...]\n [--no-plan] skip the planner: template-first routing, then a straight composition\n ove ai refine <composition.json> --instruction \"...\" [--review] [--dry-run]\n ove ai narrate <script.txt> [--composition video.json] [--out dir] [--voice Puck] [--language en] [--tail 0.8]\n ove ai narrate --brief \"...\" [--duration 20] [--composition video.json]\n ove ai transcribe <audio> [--composition video.json] [--out dir] [--language en] [--start 0]\n ove ai edit <clip.mp4> --out reel-dir [--max-silence 0.5] [--keep-fillers] [--width 1080] [--height 1920]\n [--fps 30] [--no-captions] [--language en] [--force]\n transcribe the clip, cut silences and filler words, and write an edited reel project\n ove ai shorts <long.mp4> --out dir [--count 3] [--min 15] [--max 60] [--no-cut] [--no-captions]\n [--width 1080] [--height 1920] [--fps 30] [--language en]\n transcribe once, score candidate clips, and write the best ones as vertical reel projects\n ove ai image \"<prompt>\" --out assets/bg.png [--size square|portrait|landscape|WxH] [--composition video.json --id bg]\n ove ai batch <rows.json> --brief \"...\" --output \"out/{{id}}.mp4\" [--template video.json] [--out-dir dir] [--limit n]\n [--duration 18] [--fps 30] [render options]\n ove ai studio [--port 3100] [--out ai-videos] open a local page for the AI commands in a browser\n\nProvider options (all commands): --provider
|
|
1
|
+
declare const usage = "Usage:\n ove ai doctor show the provider, model and capabilities\n ove ai capabilities [--json] list every registered capability the AI may use\n ove ai history <composition.json> list the kept revisions of an AI-made video\n ove ai restore <composition.json> <n> bring revision n back (nothing is lost: the restore is a revision too)\n ove ai compose <dir> --brief \"...\" [--kind reel|product|promo|project] [--aspect 9:16] [--duration 20] [--fps 30] [--full]\n [--narration] [--review] [--render] [--force] [--dry-run] [--asset id=path] [--fields name[:type],...]\n [--no-plan] skip the planner: template-first routing, then a straight composition\n ove ai refine <composition.json> --instruction \"...\" [--review] [--dry-run]\n ove ai narrate <script.txt> [--composition video.json] [--out dir] [--voice Puck] [--language en] [--tail 0.8]\n ove ai narrate --brief \"...\" [--duration 20] [--composition video.json]\n ove ai transcribe <audio> [--composition video.json] [--out dir] [--language en] [--start 0]\n ove ai edit <clip.mp4> --out reel-dir [--max-silence 0.5] [--keep-fillers] [--width 1080] [--height 1920]\n [--fps 30] [--no-captions] [--language en] [--force]\n transcribe the clip, cut silences and filler words, and write an edited reel project\n ove ai shorts <long.mp4> --out dir [--count 3] [--min 15] [--max 60] [--no-cut] [--no-captions]\n [--width 1080] [--height 1920] [--fps 30] [--language en]\n transcribe once, score candidate clips, and write the best ones as vertical reel projects\n ove ai image \"<prompt>\" --out assets/bg.png [--size square|portrait|landscape|WxH] [--composition video.json --id bg]\n ove ai batch <rows.json> --brief \"...\" --output \"out/{{id}}.mp4\" [--template video.json] [--out-dir dir] [--limit n]\n [--duration 18] [--fps 30] [render options]\n ove ai studio [--port 3100] [--out ai-videos] open a local page for the AI commands in a browser\n\nProvider options (all commands): --provider freellmapi|mock, --model name, --base-url URL\nEnvironment: OVE_AI_PROVIDER, OVE_AI_MODEL, OVE_AI_API_KEY, FREELLMAPI_API_KEY, and FREELLMAPI_BASE_URL.\nRender options: --crf, --bitrate, --audio-bitrate, --scale, --loudness, --concurrency, --preset, --codec, --format";
|
|
2
2
|
export declare function runAi(argv: string[]): Promise<number>;
|
|
3
3
|
export { usage as aiUsage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "motionrender",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "JSON-first, frame-deterministic motion graphics video engine: render compositions to MP4, WebM, ProRes, GIF or PNG frames from Node or the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/skills/ove/SKILL.md
CHANGED
|
@@ -243,7 +243,7 @@ ove ai edit ./raw/take.mp4 --out reel --max-silence 0.5
|
|
|
243
243
|
|
|
244
244
|
- Turns a raw clip into an edited project: the clip's audio is transcribed with word timings, pauses longer than `--max-silence` seconds (default 0.5) and filler words (um, uh, erm…) plus immediate repeats become a cut list, and the project is written as `composition.json` + `composition.schema.json` + `assets/`. The result is one `video` layer per kept stretch (`trimStart` skips the removed source, the clip's own sound carried through, hard jump cuts between stretches) and a word-timed captions layer above the platform UI band.
|
|
245
245
|
- `--width`/`--height` set the canvas (default 1080×1920, the clip centre-cropped with `objectFit: cover` — the standard landscape-talking-head-to-reel move); `--keep-fillers` leaves the ums in; `--no-captions` skips the captions layer; `--force` writes into a directory that already has files.
|
|
246
|
-
- The output is an ordinary project: review it (`ove review reel/composition.json --output review.png`), tune it (the cut list is visible as the scene boundaries — delete a `clip-N` scene to restore a moment, or re-run with a different `--max-silence`), then render (`ove render reel/composition.json --output out.mp4`). Transcription needs a provider with `transcribe` (`--provider
|
|
246
|
+
- The output is an ordinary project: review it (`ove review reel/composition.json --output review.png`), tune it (the cut list is visible as the scene boundaries — delete a `clip-N` scene to restore a moment, or re-run with a different `--max-silence`), then render (`ove render reel/composition.json --output out.mp4`). Transcription needs a provider with `transcribe` (`--provider freellmapi`, or any adapter that implements it); everything else is local.
|
|
247
247
|
|
|
248
248
|
## Splitting a long recording into shorts
|
|
249
249
|
|
|
@@ -31,7 +31,7 @@ ove agent "<brief>" [dir] [options]
|
|
|
31
31
|
| `--fps <number>` | Frames per second | `30` |
|
|
32
32
|
| `--no-render` | Generate and compile React code only; skip MP4 rendering | Off |
|
|
33
33
|
| `--no-plan` | Skip the route planner and always run the React agent loop | Off |
|
|
34
|
-
| `--provider <name>` | AI provider: `
|
|
34
|
+
| `--provider <name>` | AI provider: `freellmapi`, `mock` | Auto-detected |
|
|
35
35
|
| `--model <id>` | Model ID override (passed to the selected provider) | Provider default |
|
|
36
36
|
|
|
37
37
|
## Brand Pack API
|