motionrender 0.2.1 → 0.2.3
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 +2872 -582
- package/dist/index.js +270 -43
- package/dist/player.js +13 -13
- package/dist/react.js +704 -9
- package/dist/types/packages/ai/src/agent.d.ts +57 -0
- package/dist/types/packages/ai/src/brand-scraper.d.ts +35 -0
- package/dist/types/packages/ai/src/captions.d.ts +11 -0
- package/dist/types/packages/ai/src/compose.d.ts +2 -0
- package/dist/types/packages/ai/src/edit.d.ts +69 -0
- package/dist/types/packages/ai/src/index.d.ts +8 -1
- package/dist/types/packages/ai/src/plan.d.ts +42 -0
- package/dist/types/packages/ai/src/prompt.d.ts +18 -0
- package/dist/types/packages/ai/src/providers.d.ts +35 -2
- package/dist/types/packages/ai/src/registry.d.ts +81 -0
- package/dist/types/packages/ai/src/retime.d.ts +7 -0
- package/dist/types/packages/ai/src/shorts.d.ts +75 -0
- package/dist/types/packages/cli/src/agent-cli.d.ts +15 -0
- 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/dist/types/packages/cli/src/build.d.ts +2 -0
- package/dist/types/packages/cli/src/templates.d.ts +13 -4
- package/dist/types/packages/contracts/src/index.d.ts +12 -12
- package/dist/types/packages/core/src/index.d.ts +2 -2
- package/dist/types/packages/core/src/review.d.ts +24 -0
- package/dist/types/packages/ffmpeg/src/auto-install.d.ts +12 -0
- package/dist/types/packages/ffmpeg/src/index.d.ts +2 -0
- package/dist/types/packages/jobs/src/queue.d.ts +2 -2
- package/dist/types/packages/jobs/src/redis.d.ts +2 -2
- package/dist/types/packages/react/src/components.d.ts +166 -0
- package/dist/types/packages/react/src/index.d.ts +54 -2
- package/dist/types/packages/react/src/springs.d.ts +25 -0
- package/dist/types/packages/renderer/src/index.d.ts +3 -0
- package/dist/types/packages/schema/src/captions.d.ts +2 -0
- package/dist/types/packages/schema/src/index.d.ts +66 -66
- package/dist/types/scripts/eval-briefs.d.ts +67 -0
- package/package.json +1 -1
- package/skills/ove/SKILL.md +40 -2
- package/skills/ove-agent/SKILL.md +144 -0
- package/skills/ove-code-video/SKILL.md +17 -9
- package/skills/ove-motion-graphics/SKILL.md +113 -0
- package/skills/ove-react-video/SKILL.md +212 -0
- package/skills/ove-remotion/SKILL.md +122 -0
- package/studio/assets/index-DmZj9wTZ.css +1 -0
- package/studio/assets/index-n3DJI_GV.js +10 -0
- package/studio/index.html +2 -2
- package/templates/case-study/composition.json +25 -10
- package/templates/event-invite/composition.json +12 -2
- package/templates/explainer-square/composition.json +1 -1
- package/templates/keynote-wide/composition.json +2 -2
- package/templates/kinetic-reel/composition.json +73 -0
- package/templates/kinetic-reel/composition.schema.json +9295 -0
- package/templates/kinetic-reel/template.json +17 -0
- package/templates/kinetic-reel/video.bundle.js +72 -0
- package/templates/kinetic-reel/video.tsx +283 -0
- package/templates/product-card/composition.json +7 -2
- package/templates/product-wide/composition.json +1 -1
- package/templates/quote-card/composition.json +6 -1
- package/templates/saas-launch/composition.json +70 -0
- package/templates/saas-launch/composition.schema.json +9295 -0
- package/templates/saas-launch/template.json +16 -0
- package/templates/saas-launch/video.bundle.js +72 -0
- package/templates/saas-launch/video.tsx +217 -0
- package/templates/sale-promo/composition.json +16 -1
- package/templates/showcase-tall/composition.json +4 -4
- package/templates/slideshow/composition.json +1 -1
- package/templates/social-reel/composition.json +19 -4
- package/templates/story-tall/composition.json +1 -1
- package/studio/assets/index-CzOvWv9w.css +0 -1
- package/studio/assets/index-J_fvKgng.js +0 -11
|
@@ -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 ' : '\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' }), 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 ])\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 };\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 === '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,3 +1,3 @@
|
|
|
1
|
-
declare const usage = "Usage:\n ove ai doctor show the provider, model and capabilities\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 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 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 groq|mock, --model name, --base-url URL\nEnvironment: OVE_AI_PROVIDER, OVE_AI_MODEL, OVE_AI_API_KEY, and GROQ_API_KEY.\nRender options: --crf, --bitrate, --audio-bitrate, --scale, --loudness, --concurrency, --preset, --codec, --format";
|
|
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 anthropic|groq|mock, --model name, --base-url URL\nEnvironment: OVE_AI_PROVIDER, OVE_AI_MODEL, OVE_AI_API_KEY, ANTHROPIC_API_KEY, and GROQ_API_KEY.\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 };
|
|
@@ -1 +1,3 @@
|
|
|
1
|
+
/** Bundles a React video: the component for the browser (the render function the `react` layer calls) and, separately, its exports for Node. */
|
|
2
|
+
export declare function buildReact(source: string, output: string): Promise<void>;
|
|
1
3
|
export declare function runBuild(argv: string[]): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NarrateOptions, type NarrateResult, type AiProvider, type ComposeOptions, type ComposeResult, type TemplateChoice } from '../../ai/src/index.js';
|
|
1
|
+
import { type NarrateOptions, type NarrateResult, type AiProvider, type ComposeOptions, type ComposeResult, type Registry, type TemplateChoice } from '../../ai/src/index.js';
|
|
2
2
|
export declare const templatesDir: string;
|
|
3
3
|
export type TemplateVariable = {
|
|
4
4
|
name: string;
|
|
@@ -41,12 +41,21 @@ export declare function listTemplates(root?: string): Promise<TemplateInfo[]>;
|
|
|
41
41
|
/** Every template with its composition, in the shape `composeFromTemplate` reads. */
|
|
42
42
|
export declare function templateChoices(root?: string): Promise<TemplateChoice[]>;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
44
|
+
* The capability registry built from this package's own catalogues: templates, themes and blocks plus the built-in
|
|
45
|
+
* effects, sounds and the React kit that @ove/ai adds by itself. Read by the planner and `ove ai capabilities`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function cliRegistry(): Promise<Registry>;
|
|
48
|
+
/**
|
|
49
|
+
* Compose by filling a ready-made template — the primary path, chosen whenever a template's kind and aspect fit the
|
|
50
|
+
* brief — and write the whole composition only when that cannot work: `full` was asked for, assets or batch fields
|
|
51
|
+
* were supplied (templates use neither), no template has the wanted shape, or the model finds none that suits the
|
|
52
|
+
* brief. Before either path, the planner (plan.ts) decides the route and, for a JSON video, supplies the beats the
|
|
53
|
+
* composition must follow. Planning failure or `--no-plan` keeps the same template-first routing.
|
|
47
54
|
*/
|
|
48
55
|
export declare function composeSmart(provider: AiProvider, options: ComposeOptions, mode?: {
|
|
49
56
|
full?: boolean;
|
|
57
|
+
noPlan?: boolean;
|
|
58
|
+
registry?: Registry;
|
|
50
59
|
}): Promise<ComposeResult & {
|
|
51
60
|
template?: string;
|
|
52
61
|
source?: Record<string, unknown>;
|
|
@@ -135,10 +135,10 @@ export declare const revisionSchema: z.ZodObject<{
|
|
|
135
135
|
file: z.ZodString;
|
|
136
136
|
at: z.ZodString;
|
|
137
137
|
kind: z.ZodEnum<{
|
|
138
|
-
baseline: "baseline";
|
|
139
138
|
compose: "compose";
|
|
140
|
-
refine: "refine";
|
|
141
139
|
edit: "edit";
|
|
140
|
+
baseline: "baseline";
|
|
141
|
+
refine: "refine";
|
|
142
142
|
import: "import";
|
|
143
143
|
restore: "restore";
|
|
144
144
|
}>;
|
|
@@ -159,10 +159,10 @@ export declare const historySchema: z.ZodObject<{
|
|
|
159
159
|
file: z.ZodString;
|
|
160
160
|
at: z.ZodString;
|
|
161
161
|
kind: z.ZodEnum<{
|
|
162
|
-
baseline: "baseline";
|
|
163
162
|
compose: "compose";
|
|
164
|
-
refine: "refine";
|
|
165
163
|
edit: "edit";
|
|
164
|
+
baseline: "baseline";
|
|
165
|
+
refine: "refine";
|
|
166
166
|
import: "import";
|
|
167
167
|
restore: "restore";
|
|
168
168
|
}>;
|
|
@@ -222,8 +222,9 @@ export declare const librarySchema: z.ZodObject<{
|
|
|
222
222
|
}, z.core.$strip>;
|
|
223
223
|
export type Library = z.infer<typeof librarySchema>;
|
|
224
224
|
export declare const jobKindSchema: z.ZodEnum<{
|
|
225
|
-
image: "image";
|
|
226
225
|
compose: "compose";
|
|
226
|
+
batch: "batch";
|
|
227
|
+
image: "image";
|
|
227
228
|
refine: "refine";
|
|
228
229
|
import: "import";
|
|
229
230
|
thumbnail: "thumbnail";
|
|
@@ -231,14 +232,14 @@ export declare const jobKindSchema: z.ZodEnum<{
|
|
|
231
232
|
review: "review";
|
|
232
233
|
narrate: "narrate";
|
|
233
234
|
transcribe: "transcribe";
|
|
234
|
-
batch: "batch";
|
|
235
235
|
}>;
|
|
236
236
|
export type JobKind = z.infer<typeof jobKindSchema>;
|
|
237
237
|
export declare const jobSchema: z.ZodObject<{
|
|
238
238
|
id: z.ZodString;
|
|
239
239
|
kind: z.ZodEnum<{
|
|
240
|
-
image: "image";
|
|
241
240
|
compose: "compose";
|
|
241
|
+
batch: "batch";
|
|
242
|
+
image: "image";
|
|
242
243
|
refine: "refine";
|
|
243
244
|
import: "import";
|
|
244
245
|
thumbnail: "thumbnail";
|
|
@@ -246,7 +247,6 @@ export declare const jobSchema: z.ZodObject<{
|
|
|
246
247
|
review: "review";
|
|
247
248
|
narrate: "narrate";
|
|
248
249
|
transcribe: "transcribe";
|
|
249
|
-
batch: "batch";
|
|
250
250
|
}>;
|
|
251
251
|
status: z.ZodEnum<{
|
|
252
252
|
error: "error";
|
|
@@ -265,8 +265,9 @@ export declare const jobEnvelopeSchema: z.ZodObject<{
|
|
|
265
265
|
job: z.ZodObject<{
|
|
266
266
|
id: z.ZodString;
|
|
267
267
|
kind: z.ZodEnum<{
|
|
268
|
-
image: "image";
|
|
269
268
|
compose: "compose";
|
|
269
|
+
batch: "batch";
|
|
270
|
+
image: "image";
|
|
270
271
|
refine: "refine";
|
|
271
272
|
import: "import";
|
|
272
273
|
thumbnail: "thumbnail";
|
|
@@ -274,7 +275,6 @@ export declare const jobEnvelopeSchema: z.ZodObject<{
|
|
|
274
275
|
review: "review";
|
|
275
276
|
narrate: "narrate";
|
|
276
277
|
transcribe: "transcribe";
|
|
277
|
-
batch: "batch";
|
|
278
278
|
}>;
|
|
279
279
|
status: z.ZodEnum<{
|
|
280
280
|
error: "error";
|
|
@@ -294,8 +294,9 @@ export declare const statusSchema: z.ZodObject<{
|
|
|
294
294
|
jobs: z.ZodArray<z.ZodObject<{
|
|
295
295
|
id: z.ZodString;
|
|
296
296
|
kind: z.ZodEnum<{
|
|
297
|
-
image: "image";
|
|
298
297
|
compose: "compose";
|
|
298
|
+
batch: "batch";
|
|
299
|
+
image: "image";
|
|
299
300
|
refine: "refine";
|
|
300
301
|
import: "import";
|
|
301
302
|
thumbnail: "thumbnail";
|
|
@@ -303,7 +304,6 @@ export declare const statusSchema: z.ZodObject<{
|
|
|
303
304
|
review: "review";
|
|
304
305
|
narrate: "narrate";
|
|
305
306
|
transcribe: "transcribe";
|
|
306
|
-
batch: "batch";
|
|
307
307
|
}>;
|
|
308
308
|
status: z.ZodEnum<{
|
|
309
309
|
error: "error";
|
|
@@ -8,8 +8,8 @@ export { cacheRoot, cacheUsage, cleanCache, type CacheUsage } from '../../assets
|
|
|
8
8
|
export { reviewComposition, type ReviewIssue, type ReviewResult, type ReviewOptions } from './review.js';
|
|
9
9
|
export { measureMedia, attachCaptions, prepareComposition, captionTrack } from './media.js';
|
|
10
10
|
export * from './distributed.js';
|
|
11
|
-
export { createRenderer, renderFrames, defaultConcurrency, launchBrowser, describeBrowser, browserInstallHint, type Renderer } from '../../renderer/src/index.js';
|
|
12
|
-
export { x264Presets, defaultPreset, checkFfmpeg, requiredEncoders, ffmpegPath, ffprobePath, type X264Preset } from '../../ffmpeg/src/index.js';
|
|
11
|
+
export { createRenderer, renderFrames, defaultConcurrency, launchBrowser, describeBrowser, ensureChromium, browserInstallHint, type Renderer } from '../../renderer/src/index.js';
|
|
12
|
+
export { x264Presets, defaultPreset, checkFfmpeg, requiredEncoders, ffmpegPath, ffprobePath, ensureFfmpeg, type X264Preset } from '../../ffmpeg/src/index.js';
|
|
13
13
|
export type OutputFormat = typeof outputFormats[number];
|
|
14
14
|
export type LoadedComposition = {
|
|
15
15
|
composition: Composition;
|
|
@@ -16,6 +16,12 @@ export type ReviewResult = {
|
|
|
16
16
|
truePeak: number;
|
|
17
17
|
};
|
|
18
18
|
};
|
|
19
|
+
export type SafeZone = {
|
|
20
|
+
top?: number;
|
|
21
|
+
bottom?: number;
|
|
22
|
+
left?: number;
|
|
23
|
+
right?: number;
|
|
24
|
+
};
|
|
19
25
|
export type ReviewOptions = {
|
|
20
26
|
allowCode?: boolean;
|
|
21
27
|
baseDir?: string;
|
|
@@ -24,6 +30,24 @@ export type ReviewOptions = {
|
|
|
24
30
|
columns?: number;
|
|
25
31
|
concurrency?: number;
|
|
26
32
|
audio?: boolean;
|
|
33
|
+
safeZone?: SafeZone;
|
|
27
34
|
onProgress?: (done: number, total: number) => void;
|
|
28
35
|
};
|
|
36
|
+
export type SafeZoneBand = {
|
|
37
|
+
name: string;
|
|
38
|
+
hint: string;
|
|
39
|
+
x0: number;
|
|
40
|
+
y0: number;
|
|
41
|
+
x1: number;
|
|
42
|
+
y1: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The rectangles where a platform's own UI covers the picture. The defaults assume the video plays in a 9:16 feed app —
|
|
46
|
+
* Instagram Reels, TikTok and YouTube Shorts draw the same chrome: caption, audio tag and action controls over the bottom
|
|
47
|
+
* of the screen, action buttons down the right edge, search and camera over the top. A tall canvas wider than 9:16 (4:5,
|
|
48
|
+
* square) is letterboxed in that player, so the chrome lands lower on the picture: the insets are where each screen band
|
|
49
|
+
* actually overlaps the video, and bands that land on the letterbox drop out. Insets are fractions of the frame; callers
|
|
50
|
+
* can override any of them (0 turns a band off). A landscape canvas has no app chrome, so there are no bands by default.
|
|
51
|
+
*/
|
|
52
|
+
export declare function safeZoneBands(width: number, height: number, override?: SafeZone): SafeZoneBand[];
|
|
29
53
|
export declare function reviewComposition(composition: Composition, options?: ReviewOptions): Promise<ReviewResult>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const localFfmpegPath: string;
|
|
2
|
+
export declare const localFfprobePath: string;
|
|
3
|
+
export declare function hasSystemFfmpeg(): boolean;
|
|
4
|
+
export declare function hasSystemFfprobe(): boolean;
|
|
5
|
+
export declare const getFfmpegPath: () => string;
|
|
6
|
+
export declare const getFfprobePath: () => string;
|
|
7
|
+
export declare function ensureFfmpeg(options?: {
|
|
8
|
+
onProgress?: (msg: string) => void;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
ffmpeg: string;
|
|
11
|
+
ffprobe: string;
|
|
12
|
+
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ensureFfmpeg, hasSystemFfmpeg, hasSystemFfprobe, localFfmpegPath, localFfprobePath } from './auto-install.js';
|
|
1
2
|
export declare const x264Presets: readonly ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
|
|
2
3
|
export type X264Preset = typeof x264Presets[number];
|
|
3
4
|
export type EncodeOptions = {
|
|
@@ -66,3 +67,4 @@ export declare function checkFfmpeg(): Promise<{
|
|
|
66
67
|
}>;
|
|
67
68
|
export declare function hasAudioStream(path: string): Promise<boolean>;
|
|
68
69
|
export declare function probe(path: string): Promise<string>;
|
|
70
|
+
export declare function extractAudio(source: string, output: string): Promise<string>;
|
|
@@ -47,7 +47,7 @@ export declare class InProcessQueue implements JobQueue {
|
|
|
47
47
|
get(id: string, who: Principal): Promise<{
|
|
48
48
|
messages: string[];
|
|
49
49
|
id: string;
|
|
50
|
-
kind: "
|
|
50
|
+
kind: "compose" | "batch" | "image" | "refine" | "import" | "thumbnail" | "render" | "review" | "narrate" | "transcribe";
|
|
51
51
|
status: "error" | "done" | "queued" | "running";
|
|
52
52
|
result?: unknown;
|
|
53
53
|
error?: string | undefined;
|
|
@@ -57,7 +57,7 @@ export declare class InProcessQueue implements JobQueue {
|
|
|
57
57
|
recent(who: Principal): Promise<{
|
|
58
58
|
messages: string[];
|
|
59
59
|
id: string;
|
|
60
|
-
kind: "
|
|
60
|
+
kind: "compose" | "batch" | "image" | "refine" | "import" | "thumbnail" | "render" | "review" | "narrate" | "transcribe";
|
|
61
61
|
status: "error" | "done" | "queued" | "running";
|
|
62
62
|
result?: unknown;
|
|
63
63
|
error?: string | undefined;
|
|
@@ -13,7 +13,7 @@ export declare class RedisQueue implements JobQueue {
|
|
|
13
13
|
enqueue(request: JobRequest): Promise<Job>;
|
|
14
14
|
get(id: string, who: Principal): Promise<{
|
|
15
15
|
id: string;
|
|
16
|
-
kind: "
|
|
16
|
+
kind: "compose" | "batch" | "image" | "refine" | "import" | "thumbnail" | "render" | "review" | "narrate" | "transcribe";
|
|
17
17
|
status: "error" | "done" | "queued" | "running";
|
|
18
18
|
messages: string[];
|
|
19
19
|
result?: unknown;
|
|
@@ -23,7 +23,7 @@ export declare class RedisQueue implements JobQueue {
|
|
|
23
23
|
} | undefined>;
|
|
24
24
|
recent(who: Principal): Promise<{
|
|
25
25
|
id: string;
|
|
26
|
-
kind: "
|
|
26
|
+
kind: "compose" | "batch" | "image" | "refine" | "import" | "thumbnail" | "render" | "review" | "narrate" | "transcribe";
|
|
27
27
|
status: "error" | "done" | "queued" | "running";
|
|
28
28
|
messages: string[];
|
|
29
29
|
result?: unknown;
|