@motion-studio/cli 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +299 -2
- package/dist/main.js +56971 -0
- package/dist/mcp-studio.mjs +213 -0
- package/dist/runtime/clock.js +1 -0
- package/dist/runtime/index.html +14 -0
- package/dist/runtime/layer.js +1 -0
- package/dist/runtime/preview.js +62 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-CCBNWqgR.css +1 -0
- package/dist/web/assets/index-DCwEfJxp.js +34 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/logo-Dd9VMLzB.svg +3 -0
- package/dist/web/assets/logo-dark-Yi8yb5WJ.svg +3 -0
- package/dist/web/assets/logo-horizontal-Blb7HKgj.svg +3 -0
- package/dist/web/assets/logo-horizontal-dark-B16it8f0.svg +3 -0
- package/dist/web/assets/react-VlNyIE0d.js +9 -0
- package/dist/web/assets/schibsted-grotesk-latin-400-normal-BkiRe4WW.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-400-normal-DPhJBilQ.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-500-normal-Ba39e-CX.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-500-normal-rf9C4Thp.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-600-normal-BieE9YjJ.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-600-normal-Czv9Obfv.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-700-normal-BkH0uJ1o.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-700-normal-Dz-okVa0.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-400-normal-DHVTfbSM.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-400-normal-DUPvg9bQ.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-500-normal-BjmYCtCC.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-500-normal-Ch1izu81.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-600-normal-C5pQPdUJ.woff2 +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-600-normal-DQsnXf5q.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-700-normal-DDW2aNyx.woff +0 -0
- package/dist/web/assets/schibsted-grotesk-latin-ext-700-normal-o210KhU4.woff2 +0 -0
- package/dist/web/assets/shared-D-Exbs4c.js +29 -0
- package/dist/web/assets/vendor-CfFIcVU3.css +1 -0
- package/dist/web/assets/vendor-IQO2I6nB.js +57 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +3 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512-maskable.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +22 -0
- package/dist/web/site.webmanifest +15 -0
- package/package.json +49 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Motion Studio MCP server: a dependency-free stdio bridge. Every tool call is forwarded to the Motion Studio core
|
|
3
|
+
// on loopback with the per-job token; the core holds the API keys and does the work.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { request } from 'node:http';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
|
|
8
|
+
const URL_BASE = (process.env.MOTION_STUDIO_BRIDGE_URL ?? '').replace(/\/+$/, '');
|
|
9
|
+
// The launcher hands the token over in a private file (never in argv or the config); the env var is a fallback.
|
|
10
|
+
const TOKEN = (() => {
|
|
11
|
+
const file = process.env.MOTION_STUDIO_BRIDGE_TOKEN_FILE;
|
|
12
|
+
if (file) {
|
|
13
|
+
try { return readFileSync(file, 'utf8').trim(); } catch { return ''; }
|
|
14
|
+
}
|
|
15
|
+
return (process.env.MOTION_STUDIO_BRIDGE_TOKEN ?? '').trim();
|
|
16
|
+
})();
|
|
17
|
+
// Just above the agent's MCP_TOOL_TIMEOUT (15 min): an approval waits for the user with no response headers, so no shorter timeout may apply.
|
|
18
|
+
const CAP_MS = Number(process.env.MOTION_STUDIO_BRIDGE_TIMEOUT_MS) || 16 * 60 * 1000;
|
|
19
|
+
const ENABLED = new Set((process.env.MOTION_STUDIO_TOOLS ?? '').split(',').map((s) => s.trim()).filter(Boolean));
|
|
20
|
+
|
|
21
|
+
const str = (description, extra = {}) => ({ type: 'string', description, ...extra });
|
|
22
|
+
/** A variant id: a, b … z, then aa, ab … (decision 978; lower case). */
|
|
23
|
+
const VARIANT_ID = { type: 'string', pattern: '^[a-z]{1,3}$', description: 'Variant id, lower case: a, b … z, then aa, ab …' };
|
|
24
|
+
const TOOLS = [
|
|
25
|
+
{ name: 'approve', description: 'For Motion Studio internal use only (permission requests).', inputSchema: { type: 'object', properties: { tool_name: str('Tool'), input: { type: 'object' }, tool_use_id: str('Id') }, additionalProperties: true } },
|
|
26
|
+
{ name: 'report_progress', description: 'Tell the user where you are (one short sentence).', inputSchema: { type: 'object', properties: { message: str('Short sentence', { maxLength: 300 }), phase: { enum: ['reading', 'fonts', 'colors', 'logos', 'tone', 'writing'], description: 'Brand analysis only: the step you are in' } }, required: ['message'] } },
|
|
27
|
+
// Phase 3 of the single agent: the coordinator of a variant group (the agent the user talks to). It hands each variant's work
|
|
28
|
+
// to that variant's own turn; the core checks every id and refuses a variant the user did not point to.
|
|
29
|
+
{ name: 'list_variants', description: 'Lists the variants of this creative: id, folder, direction, status (active or discarded), whether it is working now, its latest version per format with status and problems, and the export picks (★).', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
30
|
+
{ name: 'work_on_variants', description: 'Records the work of this turn: one item per variant, with your own instruction for that variant\'s helper (its own agent turn, started when your run ends). Only for variants the user pointed to (pinned comments, named in the message, or picked answering your AskUserQuestion); otherwise it is refused: ask first. Calling it again adds items or replaces the item of the same variant. Returns { ok, plan }. Then end your run with one short sentence.', inputSchema: { type: 'object', properties: {
|
|
31
|
+
items: { type: 'array', minItems: 1, items: { type: 'object', properties: {
|
|
32
|
+
variant: VARIANT_ID,
|
|
33
|
+
instruction: str('What this variant\'s helper must do, concrete and complete', { minLength: 1, maxLength: 4000 }),
|
|
34
|
+
formats: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 100, description: 'Only these formats of the variant (default: all)' },
|
|
35
|
+
}, required: ['variant', 'instruction'], additionalProperties: false } },
|
|
36
|
+
}, required: ['items'], additionalProperties: false } },
|
|
37
|
+
{ name: 'add_variants', description: 'Creates new variants of this creative (no limit on their number), each with its creative direction; their first generation runs in this turn. Once per turn: a request for one new variant makes one variant. Optional baseFrom: start from a saved version of an existing variant.', inputSchema: { type: 'object', properties: {
|
|
38
|
+
items: { type: 'array', minItems: 1, maxItems: 26, items: { type: 'object', properties: {
|
|
39
|
+
direction: str('One line, 3 to 140 characters, clearly different from the other directions', { minLength: 3, maxLength: 140 }),
|
|
40
|
+
baseFrom: { type: 'object', properties: { variant: VARIANT_ID, version: { type: 'integer', minimum: 1 } }, required: ['variant', 'version'], additionalProperties: false },
|
|
41
|
+
}, required: ['direction'], additionalProperties: false } },
|
|
42
|
+
}, required: ['items'], additionalProperties: false } },
|
|
43
|
+
{ name: 'validate_composition', description: 'Checks the compositions of the current version (all formats, or one) against the contract: schema, assets, duration, fonts, html layer checks. Returns problems (to fix) and warnings (advisory).', inputSchema: { type: 'object', properties: { format: str('Format id (optional: all formats when omitted)') } } },
|
|
44
|
+
{ name: 'preview_frames', description: 'Renders up to 6 frames of a format\'s composition (PNG, at most 540 px wide) and returns them as images, so you can look at your work. Limited calls per turn. In a translation turn, give the language too: the frames show the translation submitted so far.', inputSchema: { type: 'object', properties: { format: str('Format id'), times: { type: 'array', items: { type: 'number', minimum: 0 }, minItems: 1, maxItems: 6, description: 'Times in seconds from the start of the composition' }, locale: str('Translation turns only: the language tag, e.g. "de"') }, required: ['format', 'times'] } },
|
|
45
|
+
{ name: 'submit_localization', description: 'Translation turns only: submits the translation of one language and one format (merged with what was submitted before). Returns { ok: true, problems } (fix them and submit again: a shorter text, a smaller fit, a shorter voice script) or { ok: false, errors } (nothing was saved).', inputSchema: { type: 'object', properties: {
|
|
46
|
+
locale: str('Language tag, e.g. "de"'), instanceId: str('Format id, as listed in the request'),
|
|
47
|
+
texts: { type: 'object', additionalProperties: { type: 'string' }, description: 'Translated text per text clip id' },
|
|
48
|
+
params: { type: 'object', additionalProperties: { type: 'string' }, description: 'Translated text per html layer parameter, keyed "<clipId>.<param>"' },
|
|
49
|
+
voiceScripts: { type: 'object', additionalProperties: { type: 'string' }, description: 'Translated voice-over script per voice clip id (Motion Studio generates the audio)' },
|
|
50
|
+
fit: { type: 'object', additionalProperties: { type: 'number', minimum: 0.7, maximum: 1 }, description: 'Optional text scale per text clip id, 0.7 to 1 (omit to let Motion Studio choose)' },
|
|
51
|
+
}, required: ['locale', 'instanceId'], additionalProperties: false } },
|
|
52
|
+
{ name: 'submit_variant_directions', description: 'Variant directions turns only: submits one creative direction for each variant that has none, in the order asked. Each direction is one line of 3 to 140 characters, clearly different from the others and from the directions already given. Returns { ok: true } (saved) or { ok: false, errors } (nothing was saved: fix them and submit again).', inputSchema: { type: 'object', properties: {
|
|
53
|
+
directions: { type: 'array', items: { type: 'string', minLength: 3, maxLength: 140 }, minItems: 1, description: 'One direction per variant asked, in the order asked' },
|
|
54
|
+
}, required: ['directions'], additionalProperties: false } },
|
|
55
|
+
{ name: 'read_brand_kit', description: 'Reads the project brand kit and guidelines.', inputSchema: { type: 'object', properties: {} } },
|
|
56
|
+
// Decision 1421: a brief session's own tools. The agent writes the user's New creative form; nothing is created.
|
|
57
|
+
{ name: 'list_formats', description: 'Brief sessions only: lists the formats of the catalog the user can pick (id, label, channel, kind, size, max video length). Use only these ids in fill_brief.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
58
|
+
// Report 70: "the original format" of an attached video or image, as a custom format of its own size.
|
|
59
|
+
{ name: 'source_format', description: 'Brief sessions only: the format of an attached video or image at its own size, for "the original format": a custom format of exactly that size (as it is shown, rotation applied; a video\'s odd side one pixel less, at most 4096 px; a custom format of that size without limits is reused, else one is added to the user\'s custom formats). Returns { format, name, width, height, source, scaled, added }; put format in fill_brief\'s formats; when scaled is true the source was larger than a format can be: tell the user the size it got. Refused (with the reason) when the asset is not attached, is not a video or an image, its size is not known or it is too small.', inputSchema: { type: 'object', properties: {
|
|
60
|
+
asset: str('Library path of an attached asset, e.g. "assets/clip.mp4"', { minLength: 1, maxLength: 1024 }),
|
|
61
|
+
}, required: ['asset'], additionalProperties: false } },
|
|
62
|
+
{ name: 'fill_brief', description: 'Brief sessions only: writes fields of the user\'s New creative form (give only what changes; call it again to change more). Fields the user edited are kept and listed in `kept`: tell the user. Use `reclaim` only for fields the user\'s latest message explicitly asks you to rewrite. Nothing is created: the user reviews the form and presses Generate.', inputSchema: { type: 'object', properties: {
|
|
63
|
+
goal: { type: 'string', minLength: 1, maxLength: 100000, description: 'What the user wants to make' },
|
|
64
|
+
message: { type: 'string', maxLength: 100000, description: 'The key message shown on screen' },
|
|
65
|
+
notes: { type: 'string', maxLength: 100000, description: 'Notes for the agent that will make the creative' },
|
|
66
|
+
durationSec: { type: ['integer', 'null'], minimum: 1, maximum: 600, description: 'Video length in seconds' },
|
|
67
|
+
assets: { type: 'array', items: { type: 'string' }, description: 'Project library paths from the attached assets' },
|
|
68
|
+
linkedCodebases: { type: 'array', items: { type: 'object', properties: { path: { type: 'string' }, note: { type: 'string', description: 'What the folder is for' } }, required: ['path'], additionalProperties: false }, description: 'Folders linked to the session, each with a note' },
|
|
69
|
+
formats: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Preset ids from list_formats; N copies of one format (different images or videos on the same brief): the id, then <id>@2 … <id>@N (N at most 20, none left out)' },
|
|
70
|
+
variants: { type: 'integer', minimum: 1, maximum: 5, description: 'Only when the user asks for variants' },
|
|
71
|
+
reclaim: { type: 'array', items: { enum: ['goal', 'message', 'durationSec', 'assets', 'linkedCodebases', 'notes', 'formats', 'variants'] }, description: 'Fields the user edited that their latest message explicitly asks you to rewrite; never on your own initiative' },
|
|
72
|
+
}, additionalProperties: false, minProperties: 1 } },
|
|
73
|
+
// Decision 1468: a video analysis run writes its description of the frames it was given; the core checks the times and frames.
|
|
74
|
+
{ name: 'write_video_analysis', description: 'Video analysis jobs only: writes the analysis of the frames you were given. Give exactly the fields your instructions ask for; times are seconds from the start of the video. Calling it again replaces the previous write.', inputSchema: { type: 'object', properties: {
|
|
75
|
+
summary: str('3 to 5 lines: what the video is, its style, what it is good for in a motion graphic', { minLength: 1, maxLength: 2000 }),
|
|
76
|
+
timeline: { type: 'array', maxItems: 200, description: 'The scenes in order', items: { type: 'object', properties: {
|
|
77
|
+
from: { type: 'number', minimum: 0 }, to: { type: 'number', minimum: 0 },
|
|
78
|
+
onScreen: str('What is on screen', { minLength: 1, maxLength: 1000 }),
|
|
79
|
+
onScreenText: { type: 'array', maxItems: 20, items: { type: 'string', maxLength: 300 }, description: 'Text visible on screen, verbatim' },
|
|
80
|
+
frame: str('The file name of the frame that shows the scene best, e.g. f-00012000.jpg'),
|
|
81
|
+
}, required: ['from', 'to', 'onScreen'], additionalProperties: false } },
|
|
82
|
+
usableMoments: { type: 'array', maxItems: 30, description: 'Clips worth reusing, with exact in/out times', items: { type: 'object', properties: {
|
|
83
|
+
from: { type: 'number', minimum: 0 }, to: { type: 'number', minimum: 0 }, why: str('Why it is worth reusing', { minLength: 1, maxLength: 300 }),
|
|
84
|
+
}, required: ['from', 'to', 'why'], additionalProperties: false } },
|
|
85
|
+
look: { type: 'object', properties: {
|
|
86
|
+
colours: { type: 'array', maxItems: 12, items: { type: 'string', pattern: '^#[0-9a-fA-F]{6}$' }, description: 'Dominant colours as #rrggbb' },
|
|
87
|
+
typography: str('Typography seen', { maxLength: 300 }), framing: str('Framing: screen recording, talking head, b-roll…', { maxLength: 300 }), audio: str('Audio character: voice, music, silence', { maxLength: 300 }),
|
|
88
|
+
}, required: ['colours', 'typography', 'framing', 'audio'], additionalProperties: false },
|
|
89
|
+
tags: { type: 'array', maxItems: 30, items: { type: 'string', minLength: 1, maxLength: 40 } },
|
|
90
|
+
}, additionalProperties: false, minProperties: 1 } },
|
|
91
|
+
// Task 23: a subtitle translation run hands over its lines; the core checks the ids, the length and the lines on screen.
|
|
92
|
+
{ name: 'submit_subtitle_translation', description: 'Subtitle translation jobs only: submits translated lines by id (only the ids you were given). Each text is one plain line of 1 to 200 characters that fits two lines on screen (Motion Studio wraps it). Several calls add up (at most 5). Returns { ok: true, remaining } (the ids still missing); an error names the lines to fix, and nothing of that call is kept.', inputSchema: { type: 'object', properties: {
|
|
93
|
+
cues: { type: 'array', minItems: 1, maxItems: 2000, items: { type: 'object', properties: {
|
|
94
|
+
id: str('Line id, as listed', { minLength: 1, maxLength: 64 }), text: str('The translated line', { minLength: 1, maxLength: 200 }),
|
|
95
|
+
}, required: ['id', 'text'], additionalProperties: false } },
|
|
96
|
+
}, required: ['cues'], additionalProperties: false } },
|
|
97
|
+
{ name: 'generate_image', description: 'Generates or edits an image with gpt-image-2 (paid; may ask the user for confirmation). Returns the path of the file saved in the assets.', inputSchema: { type: 'object', properties: { prompt: str('Description'), width: { type: 'integer' }, height: { type: 'integer' }, quality: { enum: ['low', 'medium', 'high', 'auto'] }, background: { enum: ['transparent', 'opaque', 'auto'] }, references: { type: 'array', items: { type: 'string' }, maxItems: 16, description: 'Project images to use as references' }, name: str('File name (optional)') }, required: ['prompt', 'width', 'height'] } },
|
|
98
|
+
{ name: 'tts', description: 'Generates a voice-over (paid; may ask for confirmation). Returns the path of the audio file.', inputSchema: { type: 'object', properties: { text: str('Text to read'), provider: { enum: ['openai', 'elevenlabs'] }, voice: str('Voice: an OpenAI voice name, or an ElevenLabs voice id from list_voices (then provider \'elevenlabs\')'), instructions: str('Tone and style (OpenAI only)'), format: { enum: ['mp3', 'wav'] }, name: str('File name (optional)') }, required: ['text'] } },
|
|
99
|
+
{ name: 'sound_effect', description: 'Generates a sound effect with ElevenLabs (paid; may ask for confirmation). Returns the path of the audio file saved in the assets. Leave the duration out to let the service choose.', inputSchema: { type: 'object', properties: { prompt: str('Description of the sound (up to 1000 characters)'), durationSec: { type: 'number', minimum: 0.5, maximum: 30 }, loop: { type: 'boolean', description: 'Seamless loop' }, promptInfluence: { type: 'number', minimum: 0, maximum: 1 }, name: str('File name (optional)') }, required: ['prompt'] } },
|
|
100
|
+
{ name: 'music', description: 'Generates a music track with ElevenLabs (paid; may ask for confirmation). Returns the path of the audio file saved in the assets.', inputSchema: { type: 'object', properties: { prompt: str('Description of the music (up to 1000 characters)'), durationSec: { type: 'number', minimum: 10, maximum: 300 }, instrumental: { type: 'boolean', description: 'No vocals' }, name: str('File name (optional)') }, required: ['prompt', 'durationSec'] } },
|
|
101
|
+
{ name: 'list_voices', description: 'Lists the ElevenLabs voices of the account (free): id, name, labels, preview URL. To use one, call tts with provider \'elevenlabs\' and the id as voice (an ElevenLabs id never works with OpenAI).', inputSchema: { type: 'object', properties: { search: str('Search text (optional)'), limit: { type: 'integer', minimum: 1, maximum: 100 } } } },
|
|
102
|
+
{ name: 'stock_search', description: 'Searches photos or videos on Pexels or Unsplash.', inputSchema: { type: 'object', properties: { provider: { enum: ['pexels', 'unsplash'] }, query: str('Search query (works best in English)'), kind: { enum: ['photo', 'video'] }, orientation: { enum: ['landscape', 'portrait', 'square'] }, limit: { type: 'integer', minimum: 1, maximum: 20 } }, required: ['provider', 'query'] } },
|
|
103
|
+
{ name: 'stock_download', description: 'Downloads a stock result into the project assets, with the required attribution.', inputSchema: { type: 'object', properties: { provider: { enum: ['pexels', 'unsplash'] }, id: str('Result id'), kind: { enum: ['photo', 'video'] } }, required: ['provider', 'id'] } },
|
|
104
|
+
{ name: 'fonts_fetch', description: 'Downloads a font from Google Fonts into the project assets.', inputSchema: { type: 'object', properties: { family: str('Family, e.g. "Manrope"'), weights: { type: 'array', items: { type: 'integer' } }, italic: { type: 'boolean' } }, required: ['family'] } },
|
|
105
|
+
{ name: 'inspect_site_fonts', description: 'Reads a website\'s HTML and CSS (within limits) and lists its fonts: family, roles (heading, body, button, other), weights, italic, source (google, self-hosted, adobe, system) and font file URLs, plus warnings about what was not read.', inputSchema: { type: 'object', properties: { url: str('http(s) URL of the website page') }, required: ['url'] } },
|
|
106
|
+
{ name: 'download_file', description: 'Downloads a file (logo, image or font) from a website into the project assets: give the URL and the destination in assets/brand/ or assets/fonts/', inputSchema: { type: 'object', properties: { url: str('http(s) URL of the file to download'), dest: str('Destination, e.g. "assets/brand/logo.svg" or "assets/fonts/Inter-400.woff2"') }, required: ['url', 'dest'] } },
|
|
107
|
+
{ name: 'import_codebase_file', description: 'Brand analysis of a codebase: copies a logo, icon or font file from one of the analysis codebases into the project assets (read-only copy; secret files, files outside the codebase and files over 50 MB are refused; at most 40 calls). Give the source path (relative to the codebase folder, or absolute inside it) and the destination in assets/brand/ or assets/fonts/ with the same extension.', inputSchema: { type: 'object', properties: { source: str('Path of the file in the codebase, e.g. "public/logo.svg"'), dest: str('Destination, e.g. "assets/brand/logo.svg" or "assets/fonts/Inter-400.woff2"') }, required: ['source', 'dest'] } },
|
|
108
|
+
// Decision 917: the agent's actions on the app (creative turns only), with no confirmation card; the chat shows each one.
|
|
109
|
+
// The core checks every input again and acts only on the turn's own creative, its variant group and its project.
|
|
110
|
+
{ name: 'update_brief', description: 'Changes the brief of this creative: duration (durationSec), goal, key message, notes, services. Give only what changes. The brief changes when your turn ends; a new durationSec applies to this turn\'s output checks at once. Use it when the user asks, or when the brief clearly needs it (e.g. its text asks for 60 s but its duration says 6).', inputSchema: { type: 'object', properties: {
|
|
111
|
+
durationSec: { type: ['integer', 'null'], minimum: 1, maximum: 600, description: 'Video duration in seconds (null: none)' },
|
|
112
|
+
goal: str('Goal', { minLength: 1, maxLength: 2000 }), message: str('Key message', { maxLength: 500 }), notes: str('Notes', { maxLength: 10000 }),
|
|
113
|
+
services: { type: 'object', properties: { images: { enum: ['auto', 'openai', 'stock', 'none'] }, voice: { enum: ['auto', 'openai', 'elevenlabs', 'none'] }, music: { enum: ['auto', 'elevenlabs', 'none'] }, sfx: { enum: ['auto', 'elevenlabs', 'none'] } }, additionalProperties: false, description: 'The services to change (the others stay)' },
|
|
114
|
+
}, additionalProperties: false } },
|
|
115
|
+
{ name: 'add_format', description: 'Adds a format of the catalog to the brief (by preset id, e.g. "youtube-shorts-9x16"). Motion Studio makes it in a turn right after this one: do not write its composition now.', inputSchema: { type: 'object', properties: { format: str('Preset id'), label: str('Optional label', { minLength: 1, maxLength: 60 }) }, required: ['format'], additionalProperties: false } },
|
|
116
|
+
{ name: 'duplicate_format', description: 'Duplicates a format of the brief as a second instance (e.g. youtube-16x9 → youtube-16x9@2), from the version you are making, when your turn ends.', inputSchema: { type: 'object', properties: { format: str('Format id of the brief'), label: str('Optional label', { minLength: 1, maxLength: 60 }) }, required: ['format'], additionalProperties: false } },
|
|
117
|
+
{ name: 'name_formats', description: 'Gives formats of the brief their short speaking names (what each one shows), shown to the user in the formats list, on the canvas and in the exports. A store screenshot keeps its number in front ("1 · name"). Formats the user named are refused: never rename them. The names are set when your turn ends.', inputSchema: { type: 'object', properties: { names: { type: 'object', additionalProperties: str('Name, one line', { minLength: 1, maxLength: 60 }), minProperties: 1, maxProperties: 100, description: 'Format id → name' } }, required: ['names'], additionalProperties: false } },
|
|
118
|
+
{ name: 'add_language', description: 'Adds a language variant: the language goes into the brief and its translation starts when your turn ends. On a brief without languages give the primary language (the one the creative is in) too.', inputSchema: { type: 'object', properties: { locale: str('Language tag, e.g. "de"'), primary: str('Primary language tag, e.g. "it" (only when the brief has no languages yet)'), formats: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 100, description: 'Formats to translate (default: all)' } }, required: ['locale'], additionalProperties: false } },
|
|
119
|
+
{ name: 'add_variant', description: 'Adds a variant of this creative with its creative direction and starts its first generation (its own agent turn). A creative without variants becomes variant A of a group of two. There is no limit on the number of variants.', inputSchema: { type: 'object', properties: { direction: str('One line, 3 to 140 characters', { minLength: 3, maxLength: 140 }) }, required: ['direction'], additionalProperties: false } },
|
|
120
|
+
{ name: 'discard_variant', description: 'Discards a variant of the group (hidden, nothing deleted, restorable). A variant that is working cannot be discarded.', inputSchema: { type: 'object', properties: { variant: VARIANT_ID }, required: ['variant'], additionalProperties: false } },
|
|
121
|
+
{ name: 'restore_variant', description: 'Restores a discarded variant of the group.', inputSchema: { type: 'object', properties: { variant: VARIANT_ID }, required: ['variant'], additionalProperties: false } },
|
|
122
|
+
{ name: 'set_pick', description: 'Sets the ★ of a format: the version exported. version: a saved version number, or "current" (the version you are making, set when your turn ends). In a variant group, variant names whose version it is (default: yours; the agent of the group always names it, with a saved version).', inputSchema: { type: 'object', properties: { format: str('Format id'), version: { anyOf: [{ type: 'integer', minimum: 1 }, { const: 'current' }] }, variant: VARIANT_ID }, required: ['format', 'version'], additionalProperties: false } },
|
|
123
|
+
{ name: 'render_version', description: 'Renders a version\'s video and image files on the user\'s computer: a saved version number now, or "current" (the version you are making) when your turn ends. Optional formats (default: all). The agent of a variant group names the variant and a saved version.', inputSchema: { type: 'object', properties: { version: { anyOf: [{ type: 'integer', minimum: 1 }, { const: 'current' }] }, formats: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 100 }, variant: VARIANT_ID }, required: ['version'], additionalProperties: false } },
|
|
124
|
+
{ name: 'prepare_export', description: 'When your turn ends, renders what the ★ export needs. The user then exports (chooses where the files go): you never write outside the project.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
125
|
+
// Decision 1640 (U9) and Task 19: the subtitles job of this creative, which runs on its own, drawn into the video or files
|
|
126
|
+
// only (phase 3 restored `drawn`, decision 1710); and the agent's corrections of their text. The core checks the input again.
|
|
127
|
+
{ name: 'start_subtitles', description: 'Makes the subtitles of this creative from the version you are making in this turn: when your turn ends, its audio is transcribed and cut into subtitle lines, on its own (it may take minutes; the chat tells the user when they are ready, with the editor and the .srt/.vtt downloads). Once per turn. drawn: true when the user wants them drawn into the video (when they are ready, Motion Studio asks you in a new turn to add the subtitles track with their style), false for files or a track in the MP4 only. If the user did not say which, ask with AskUserQuestion first. Never write subtitles as text clips.', inputSchema: { type: 'object', properties: {
|
|
128
|
+
drawn: { type: 'boolean', description: 'true: drawn into the video; false: only as files (.srt/.vtt) or a track in the MP4' },
|
|
129
|
+
lang: str('Only when the user names the spoken language: its tag (e.g. "it", "en", "zh-Hans"). Leave it out to detect it'),
|
|
130
|
+
}, required: ['drawn'], additionalProperties: false } },
|
|
131
|
+
{ name: 'edit_subtitles', description: 'Corrects the text of this creative\'s subtitles in one language now; their times stay. replace: a literal text found in every line (case-insensitive unless caseSensitive), e.g. "motion studio" → "Motion Studio"; cues: the whole text of lines by id, each with was, its text as you read it (the ids and texts are in creatives/<slug>/subtitles/<lang>.json: read it just before, never write it); if a line changed since (the user corrected it), nothing is changed: read it again. The lines you change count as the user\'s corrections (a new transcription keeps them). An unknown id changes nothing. Returns how many lines changed.', inputSchema: { type: 'object', properties: {
|
|
132
|
+
lang: str('Language tag of the subtitles, e.g. "it"'),
|
|
133
|
+
replace: { type: 'array', maxItems: 20, items: { type: 'object', properties: {
|
|
134
|
+
find: str('Text to find (literal)', { minLength: 1, maxLength: 200 }), with: str('Replacement', { maxLength: 200 }), caseSensitive: { type: 'boolean', description: 'Match the case exactly (default: no)' },
|
|
135
|
+
}, required: ['find', 'with'], additionalProperties: false } },
|
|
136
|
+
cues: { type: 'array', maxItems: 200, items: { type: 'object', properties: {
|
|
137
|
+
id: str('Line id', { minLength: 1, maxLength: 32 }), was: str('The line\'s text as you read it', { maxLength: 200 }), text: str('The line\'s new text, at most two lines (\\n between them)', { minLength: 1, maxLength: 200 }),
|
|
138
|
+
}, required: ['id', 'was', 'text'], additionalProperties: false } },
|
|
139
|
+
}, required: ['lang'], additionalProperties: false } },
|
|
140
|
+
// Task 25 (spec §6.5, U8): one more subtitle language, made from its own voices or translated. The core checks it again.
|
|
141
|
+
{ name: 'add_subtitle_language', description: 'Adds subtitles in another language to this creative: from that language\'s own voices when its localized video has them (transcribed when your turn ends), else translated now from the subtitles made from the video\'s speech (start_subtitles first when there are none). Never together with start_subtitles for the same language. It runs on its own; the chat tells the user when they are ready. Subtitles only: the video is not translated and the brief\'s languages do not change: for subtitles use this tool, not add_language (which translates the video itself). Refused when the language already has subtitles.', inputSchema: { type: 'object', properties: {
|
|
142
|
+
lang: str('Language tag of the new subtitles, e.g. "en"'),
|
|
143
|
+
}, required: ['lang'], additionalProperties: false } },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const send = (m) => process.stdout.write(`${JSON.stringify(m)}\n`);
|
|
147
|
+
const listed = () => TOOLS.filter((t) => t.name === 'approve' || ENABLED.has(t.name));
|
|
148
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
149
|
+
|
|
150
|
+
// node:http instead of fetch: undici cuts a request without response headers after 5 minutes.
|
|
151
|
+
function post(path, payload) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const req = request(`${URL_BASE}${path}`, { method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload), 'x-motion-studio-bridge': TOKEN } }, (res) => {
|
|
154
|
+
const chunks = [];
|
|
155
|
+
res.on('data', (c) => chunks.push(c));
|
|
156
|
+
res.on('end', () => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
let body = {};
|
|
159
|
+
try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { /* not JSON */ }
|
|
160
|
+
resolve({ status: res.statusCode ?? 0, body });
|
|
161
|
+
});
|
|
162
|
+
res.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
163
|
+
});
|
|
164
|
+
const timer = setTimeout(() => req.destroy(new Error('no response within the maximum time')), CAP_MS);
|
|
165
|
+
req.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
166
|
+
req.end(payload);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function call(name, args) {
|
|
171
|
+
if (!listed().some((t) => t.name === name)) return fail(`Tool not available: ${name}`);
|
|
172
|
+
if (!TOKEN) return fail('Motion Studio is unreachable: token missing');
|
|
173
|
+
try {
|
|
174
|
+
const { status, body } = await post(`/api/bridge/${encodeURIComponent(name)}`, JSON.stringify(args ?? {}));
|
|
175
|
+
if (status < 200 || status > 299) return fail(body?.error ?? `Error ${status}`);
|
|
176
|
+
// A result with `images` (preview_frames): the images become image content, the rest stays text.
|
|
177
|
+
if (body && typeof body === 'object' && Array.isArray(body.images)) {
|
|
178
|
+
const { images, ...rest } = body;
|
|
179
|
+
const pictures = images.filter((i) => i && typeof i.data === 'string' && /^image\/(png|jpeg)$/.test(i.mimeType ?? '')).slice(0, 6);
|
|
180
|
+
return { content: [{ type: 'text', text: JSON.stringify(rest) }, ...pictures.map((i) => ({ type: 'image', data: i.data, mimeType: i.mimeType }))] };
|
|
181
|
+
}
|
|
182
|
+
return { content: [{ type: 'text', text: JSON.stringify(body) }] };
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return fail(`Motion Studio is unreachable: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let inflight = 0;
|
|
189
|
+
let closed = false;
|
|
190
|
+
const maybeExit = () => { if (closed && inflight === 0) process.exit(0); };
|
|
191
|
+
createInterface({ input: process.stdin }).on('line', async (line) => {
|
|
192
|
+
let msg;
|
|
193
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
194
|
+
if (!msg || typeof msg !== 'object') return;
|
|
195
|
+
if (msg.id === undefined) return; // notification
|
|
196
|
+
inflight++;
|
|
197
|
+
try { await handle(msg); } finally { inflight--; maybeExit(); }
|
|
198
|
+
}).on('close', () => { closed = true; maybeExit(); });
|
|
199
|
+
|
|
200
|
+
async function handle(msg) {
|
|
201
|
+
switch (msg.method) {
|
|
202
|
+
case 'initialize':
|
|
203
|
+
return send({ jsonrpc: '2.0', id: msg.id, result: { protocolVersion: msg.params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'studio', version: '0.4.0' } } });
|
|
204
|
+
case 'ping':
|
|
205
|
+
return send({ jsonrpc: '2.0', id: msg.id, result: {} });
|
|
206
|
+
case 'tools/list':
|
|
207
|
+
return send({ jsonrpc: '2.0', id: msg.id, result: { tools: listed() } });
|
|
208
|
+
case 'tools/call':
|
|
209
|
+
return send({ jsonrpc: '2.0', id: msg.id, result: await call(msg.params?.name, msg.params?.arguments) });
|
|
210
|
+
default:
|
|
211
|
+
return send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Unsupported method: ${msg.method}` } });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{function _(n){let s=typeof n.requestAnimationFrame=="function"?n.requestAnimationFrame.bind(n):null,f=n.setTimeout.bind(n);return{setTimeout:(u,o)=>f(u,o),clearTimeout:n.clearTimeout.bind(n),requestAnimationFrame:s??(u=>f(()=>u(0),16)),performanceNow:n.performance.now.bind(n.performance)}}function y(n){let s=n;if(s.__msClock)return s.__msClock;let f=_(n),u=n.Date,o=0,b=0,l=new Map,m=new Map,k=(e,t)=>{if(typeof e=="function")try{e(...t)}catch(a){n.console?.error(a)}};function c(...e){return new.target?e.length?new u(...e):new u(17672256e5+o):new u(17672256e5+o).toString()}c.prototype=u.prototype,c.now=()=>17672256e5+o,c.UTC=u.UTC,c.parse=u.parse,n.Date=c,Object.defineProperty(n.performance,"now",{value:()=>o,configurable:!0,writable:!0});let p=(e,t,a,r)=>{let d=++b,i=Math.max(r?1:0,Number(t)||0);return l.set(d,{due:o+i,fn:e,args:a,...r?{interval:i}:{}}),d},w=e=>{l.delete(Number(e))};n.setTimeout=((e,t,...a)=>p(e,t,a,!1)),n.setInterval=((e,t,...a)=>p(e,t,a,!0)),n.clearTimeout=w,n.clearInterval=w,n.requestAnimationFrame=e=>{let t=++b;return m.set(t,e),t},n.cancelAnimationFrame=e=>{m.delete(e)};let v=e=>{let t=0;for(;t<1e5;t++){let a=-1,r;for(let[d,i]of l)i.due<=e&&(!r||i.due<r.due)&&(a=d,r=i);if(!r)break;r.due>o&&(o=r.due),r.interval?r.due+=r.interval:l.delete(a),k(r.fn,r.args)}return t},C=()=>{let e=m;m=new Map;for(let t of e.values())k(t,[o]);return e.size},T={get now(){return o},native:f,advanceTo(e){Number.isFinite(e)&&(v(e),o=e,C())},runDueTimers:()=>v(o),runFrame:C,nextTimerDue(){let e=null;for(let t of l.values())(e===null||t.due<e)&&(e=t.due);return e},pendingFrames:()=>m.size};return Object.defineProperty(s,"__msClock",{value:T,configurable:!1,enumerable:!1,writable:!1}),T}y(window);})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Motion Studio composition</title>
|
|
7
|
+
<style>html,body{margin:0;padding:0;overflow:hidden;background:transparent;color-scheme:normal}#ms-root{position:fixed;inset:0;overflow:hidden}</style>
|
|
8
|
+
<!--ms-composition-->
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="ms-root"></div>
|
|
12
|
+
<script src="runtime.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{function X(e){let t=typeof e.requestAnimationFrame=="function"?e.requestAnimationFrame.bind(e):null,i=e.setTimeout.bind(e);return{setTimeout:(o,s)=>i(o,s),clearTimeout:e.clearTimeout.bind(e),requestAnimationFrame:t??(o=>i(()=>o(0),16)),performanceNow:e.performance.now.bind(e.performance)}}function V(e){let t=e;if(t.__msClock)return t.__msClock;let i=X(e),o=e.Date,s=0,n=0,u=new Map,a=new Map,y=(f,b)=>{if(typeof f=="function")try{f(...b)}catch(x){e.console?.error(x)}};function d(...f){return new.target?f.length?new o(...f):new o(17672256e5+s):new o(17672256e5+s).toString()}d.prototype=o.prototype,d.now=()=>17672256e5+s,d.UTC=o.UTC,d.parse=o.parse,e.Date=d,Object.defineProperty(e.performance,"now",{value:()=>s,configurable:!0,writable:!0});let w=(f,b,x,k)=>{let E=++n,C=Math.max(k?1:0,Number(b)||0);return u.set(E,{due:s+C,fn:f,args:x,...k?{interval:C}:{}}),E},h=f=>{u.delete(Number(f))};e.setTimeout=((f,b,...x)=>w(f,b,x,!1)),e.setInterval=((f,b,...x)=>w(f,b,x,!0)),e.clearTimeout=h,e.clearInterval=h,e.requestAnimationFrame=f=>{let b=++n;return a.set(b,f),b},e.cancelAnimationFrame=f=>{a.delete(f)};let S=f=>{let b=0;for(;b<1e5;b++){let x=-1,k;for(let[E,C]of u)C.due<=f&&(!k||C.due<k.due)&&(x=E,k=C);if(!k)break;k.due>s&&(s=k.due),k.interval?k.due+=k.interval:u.delete(x),y(k.fn,k.args)}return b},T=()=>{let f=a;a=new Map;for(let b of f.values())y(b,[s]);return f.size},R={get now(){return s},native:i,advanceTo(f){Number.isFinite(f)&&(S(f),s=f,T())},runDueTimers:()=>S(s),runFrame:T,nextTimerDue(){let f=null;for(let b of u.values())(f===null||b.due<f)&&(f=b.due);return f},pendingFrames:()=>a.size};return Object.defineProperty(t,"__msClock",{value:R,configurable:!1,enumerable:!1,writable:!1}),R}var M=class extends Error{code="frameTimeout";constructor(t){super(`no animation frame within ${t} ms`),this.name="FrameTimeoutError"}},_={strict:!1,timeoutMs:100};function O(e,t=_){return new Promise((i,o)=>{let s=!1,n=e.setTimeout(()=>{s||(s=!0,t.strict?o(new M(t.timeoutMs)):i())},t.timeoutMs);e.requestAnimationFrame(()=>{s||(s=!0,e.clearTimeout(n),i())})})}async function $(e,t=_){await O(e,t),await O(e,t)}var H={cues:2e3,words:60,text:200,wordText:100,lang:35,ms:36e6},D=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),j=(e,t)=>Object.keys(e).every(i=>t.includes(i)),W=e=>typeof e=="number"&&Number.isFinite(e)&&e>=0&&e<=H.ms,J=/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/;function Y(e){let t=H;if(!D(e)||!j(e,["lang","cues"]))return{ok:!1,reason:"not a subtitles block"};if(typeof e.lang!="string"||e.lang.length>t.lang||!J.test(e.lang))return{ok:!1,reason:"bad language"};if(!Array.isArray(e.cues)||e.cues.length>t.cues)return{ok:!1,reason:`cues must be an array of at most ${t.cues}`};let i=[],o=0;for(let s=0;s<e.cues.length;s++){let n=e.cues[s];if(!D(n)||!j(n,["start","end","text","words"]))return{ok:!1,reason:`cue ${s} is malformed`};if(!W(n.start)||!W(n.end)||!(n.end>n.start))return{ok:!1,reason:`cue ${s} has bad times`};if(n.start<o)return{ok:!1,reason:`cue ${s} is out of order`};if(o=n.start,typeof n.text!="string"||n.text.length>t.text)return{ok:!1,reason:`cue ${s} has a bad text`};let u;if(n.words!==void 0){if(!Array.isArray(n.words)||n.words.length>t.words)return{ok:!1,reason:`cue ${s} has too many words`};u=[];for(let a of n.words){if(!D(a)||!j(a,["start","end","text"])||!W(a.start)||!W(a.end)||a.end<a.start||typeof a.text!="string"||a.text.length===0||a.text.length>t.wordText)return{ok:!1,reason:`cue ${s} has a bad word`};u.push({start:a.start,end:a.end,text:a.text})}}i.push({start:n.start,end:n.end,text:n.text,...u?{words:u}:{}})}return{ok:!0,subtitles:{lang:e.lang,cues:Object.freeze(i)}}}function q(e,t){let i=0,o=e.length-1,s=-1;for(;i<=o;){let a=i+o>>1;e[a].start<=t?(s=a,i=a+1):o=a-1}if(s<0)return-1;if(t<e[s].end)return s;let n=Q(e),u=(a,y,d)=>{if(y>s||!(n.max[a]>t))return-1;if(y===d)return y;let w=y+d>>1,h=u(a*2+1,w+1,d);return h>=0?h:u(a*2,y,w)};return u(1,0,n.size-1)}var U=new WeakMap;function Q(e){let t=U.get(e);if(t&&t.n===e.length)return t;let i=1;for(;i<e.length;)i*=2;let o=new Float64Array(i*2).fill(Number.NEGATIVE_INFINITY);for(let n=0;n<e.length;n++)o[i+n]=e[n].end;for(let n=i-1;n>=1;n--)o[n]=Math.max(o[n*2],o[n*2+1]);let s={n:e.length,size:i,max:o};return U.set(e,s),s}var G="ms-layer:",F=1;function K(e){if(typeof e!="string"||!e.startsWith(G))return null;try{let t=JSON.parse(e.slice(G.length));if(t.v!==F||t.mode!=="render"&&t.mode!=="preview"||typeof t.clipId!="string")return null;let i=typeof t.params=="object"&&t.params!==null?t.params:{},o=Array.isArray(t.fonts)?t.fonts.filter(u=>u&&typeof u.family=="string"&&typeof u.url=="string"):[],s=typeof t.frameTimeoutMs=="number"&&t.frameTimeoutMs>0?t.frameTimeoutMs:void 0,n=t.subtitles!==void 0?Y(t.subtitles):null;return{v:F,mode:t.mode,clipId:t.clipId,params:i,fonts:o,...s?{frameTimeoutMs:s}:{},...n?.ok?{subtitles:n.subtitles}:{}}}catch{return null}}var Z=2e4,ee=50,te=5e3,N=class extends Error{constructor(t,i,o){super(`${t} waited ${i/1e3} s for a timer (due in ${Math.round(o)} ms) that the render clock never fires: time does not pass while a frame is rendered, so msReady and msSeek must not wait on setTimeout, setInterval, Date or performance.now`),this.name="ClockStallError"}},I=class extends Error{};async function ne(e,t,i=Z,o){if(!e||typeof e.then!="function")return e;let s=!1,n=Promise.resolve(e).finally(()=>{s=!0});n.catch(()=>{});let u=!1,a=0,y=null;for(let d=0;!s&&d<i&&(await new Promise(S=>t.native.setTimeout(S,0)),!s);d++){if(u||a>=ee){t.runFrame()>0&&t.pendingFrames()===0&&(y=null),u=!1,a=0;continue}if(u=t.runDueTimers()===0,a=u?0:a+1,!o)continue;let w=t.nextTimerDue();if(!u||w===null||w<=t.now){y=null;continue}let h=t.native.performanceNow();if(y===null)y=h;else if(h-y>=o.ms)throw new N(o.what,o.ms,w-t.now)}return n}function re(e){let t=new Set(["transform","scale","rotate","translate"]),i=e.defaultView,o=i?i.getComputedStyle.bind(i):null,s=i?i.MutationObserver:null,n=new Map,u="data-ms-u",a=`@layer ms-unfreeze { [${u}] { translate: 0px; } }`,y=i?.CSSStyleSheet,d=null;try{y&&typeof y.prototype.replaceSync=="function"&&Array.isArray(e.adoptedStyleSheets)&&(d=new y,d.replaceSync(a))}catch{d=null}let w=()=>{if(!d)return;d.cssRules.length!==1&&d.replaceSync(a);let c=e.adoptedStyleSheets;c.includes(d)||(e.adoptedStyleSheets=[...c,d])};w();let h=new Set,S=!0,T=c=>{for(let m=c;m;m=m.parentNode){let p=m.nodeName.toLowerCase();if(p==="style"||p==="link")return!0}return!1},R=c=>c.split(",").some(m=>t.has(m.trim())),f=c=>c.style,b=c=>{if(!S)for(let m of c)if(m.type==="attributes"){if(m.attributeName===u){n.has(m.target)&&h.add(m.target);continue}if(T(m.target)||m.attributeName==="class"||m.attributeName==="id"){S=!0;return}let p=m.target;(m.attributeName!=="style"||n.has(p)||R(f(p)?.getPropertyValue("will-change")??""))&&h.add(p)}else if(m.type==="characterData"){if(T(m.target)){S=!0;return}}else{if(T(m.target)){S=!0;return}for(let p of m.addedNodes){if(p.nodeType!==1)continue;let A=p;if(T(A)||A.querySelector("style, link")){S=!0;return}h.add(A);for(let r of A.querySelectorAll("*"))h.add(r)}}},x=s?new s(b):null,k=null,E=()=>{let c=new Set(["class","id","style","media","disabled","href","rel","type"]),m=c,p=r=>{for(let l of r){let g=l.selectorText;if(g)for(let L of g.matchAll(/\[\s*(?:[-\w*]*\|)?([-\w]+)/g))c.add(L[1].toLowerCase());let v=l.cssRules;v&&p(v)}};for(let r of e.styleSheets)try{p(r.cssRules)}catch{c=null;break}m.add(u);let A=c?[...c].sort().join(" "):"*";!x||A===k||(k=A,x.disconnect(),x.observe(e,{subtree:!0,childList:!0,characterData:!0,attributes:!0,...c?{attributeFilter:[...c]}:{}}))};x?.observe(e,{subtree:!0,childList:!0,attributes:!0,characterData:!0});let C=c=>{let m=n.get(c),p=f(c);n.delete(c),c.removeAttribute(u),!(!m||!p)&&p.getPropertyValue("will-change")===m.wc&&p.getPropertyPriority("will-change")==="important"&&(m.was?p.setProperty("will-change",m.was,m.wasPriority):p.removeProperty("will-change"))},P=c=>{let m=f(c);if(!m||!o||!c.isConnected)return;let p=n.get(c);if(!p&&c.hasAttribute(u)&&c.removeAttribute(u),p){if(m.getPropertyValue("will-change")===p.wc&&m.getPropertyPriority("will-change")==="important"){c.hasAttribute(u)||c.setAttribute(u,"");return}n.delete(c),c.removeAttribute(u)}let r=o(c).willChange;if(!r||r==="auto"||!R(r))return;let l=m.getPropertyValue("will-change"),g=m.getPropertyPriority("will-change"),v=r.split(",").map(L=>L.trim()).filter(L=>L&&!t.has(L));m.setProperty("will-change",v.length?v.join(", "):"auto","important"),c.setAttribute(u,""),n.set(c,{wc:m.getPropertyValue("will-change"),was:l,wasPriority:g})};return{run(c=1/0){x&&b(x.takeRecords()),w();for(let p of[...n.keys()])p.isConnected||C(p);if(S){S=!1,h.clear(),E();for(let p of[...n.keys()])C(p);for(let p of e.querySelectorAll(`[${u}]`))p.removeAttribute(u);for(let p of e.querySelectorAll("*"))h.add(p)}let m=0;for(let p of h){if(m++>=c)break;h.delete(p),P(p)}x?.takeRecords()},tracked:()=>n.size}}function oe(e,t){let i=[];for(let o of e.getAnimations())try{o.playState!=="paused"&&i.push(o),o.pause(),o.currentTime=t*1e3}catch{}return i}async function se(e,t,i){let o=t*1e3,s=[];for(let n of e)try{if(n.playState==="idle")continue;let u=n.effect?.getComputedTiming();if(!u)continue;let a=Number(u.delay??0),y=a+Number(u.activeDuration??0),d=o+1<y?o+1:o-1;if(o<a||o>=y||d<a)continue;n.currentTime=d,s.push(n)}catch{}if(s.length===0)return!1;await i();for(let n of s)try{n.playState!=="idle"&&(n.currentTime=o)}catch{}return!0}var ie=2e3;function ae(e,t){let i=Object.freeze(t.cues.map(a=>Object.freeze({start:a.start,end:a.end,text:a.text,...a.words?{words:Object.freeze(a.words.map(y=>Object.freeze({...y})))}:{}}))),o=Object.freeze({lang:t.lang,cues:i}),s=Object.freeze([]),n=a=>{if(typeof a!="number"||!Number.isFinite(a))return null;let y=Math.round(a*1e3)/1e3,d=q(i,y);if(d<0)return null;let w=i[d];return Object.freeze({index:d,text:w.text,words:w.words??s,progress:Math.min(1,Math.max(0,(y-w.start)/(w.end-w.start)))})},u={writable:!1,configurable:!1,enumerable:!0};Object.defineProperty(e,"msSubtitles",{value:o,...u}),Object.defineProperty(e,"msActiveCue",{value:n,...u})}var z=class extends M{constructor(t){super(t),this.message=`a canvas of the layer lost its GPU context and did not get it back within ${t} ms`}};function B(e,t={}){let i=K(e.name);if(!i)return;let o=V(e),s=i.mode==="render",n=s?{strict:!0,timeoutMs:i.frameTimeoutMs??3e4}:_,u=e,a=re(e.document);Object.defineProperty(u,"msParams",{value:Object.freeze({...i.params}),writable:!1,configurable:!1,enumerable:!0}),i.subtitles&&ae(u,i.subtitles);let y=[];for(let r of i.fonts)try{let l=new e.FontFace(r.family,`url("${r.url}")`,{...r.weight?{weight:r.weight}:{},...r.style?{style:r.style}:{}});e.document.fonts.add(l),y.push(l)}catch{}let d=null,w=!1,h=0,S=0,T=new Set,R=null,f=t.trusted??(r=>r.isTrusted);e.addEventListener("contextlost",r=>{f(r)&&(h++,S++,r.target&&T.add(r.target),d?.postMessage({type:"canvasLost",canvasLosses:h}),R?.())},!0),e.addEventListener("contextrestored",r=>{f(r)&&(S++,r.target&&T.delete(r.target),R?.())},!0);let b=()=>{for(let r of T)r.isConnected||T.delete(r);return T.size>0},x=async()=>{let r=o.native.performanceNow()+n.timeoutMs;for(;b();){let l=r-o.native.performanceNow();if(l<=0)throw new z(n.timeoutMs);await new Promise(g=>{let v=o.native.setTimeout(()=>{R=null,g()},Math.min(50,l));R=()=>{R=null,o.native.clearTimeout(v),g()}})}},k=r=>oe(e.document,r),E=r=>{o.advanceTo(r*1e3);let l=k(r),g=u.msTimeline;return g&&typeof g=="object"&&(typeof g.pause=="function"&&g.pause(),typeof g.seek=="function"?g.seek(r,!1):"currentTime"in g&&(g.currentTime=r*1e3)),l},C=(r,l)=>ne(r,o,Z,i.mode==="render"?{ms:t.stallMs??te,what:l}:void 0),P=null,c=async r=>{if(typeof u.msSeek=="function")try{await C(u.msSeek(r),"msSeek")}catch(l){throw l instanceof N&&(P=l.message),l}return k(r)};async function m(){let r=[],l=new Set([...y,...e.document.fonts]);return await Promise.all([...l].map(g=>g.load().catch(()=>{r.push(`font ${g.family} failed to load`)}))),await C(u.msReady,"msReady"),r}async function p(r){let l=E(r);if(P!==null)throw new I(`msSeek not called again: ${P}`);l.push(...await c(r)),a.run(),await $(o.native,n),l.length&&await se(l,r,()=>O(o.native,n))&&await $(o.native,n)}async function A(r){if(r.op==="tick"){if(!Number.isFinite(r.t))return;E(r.t);let l=()=>{try{a.run(ie)}catch{}};if(!w&&typeof u.msSeek=="function"){w=!0;try{await c(r.t)}catch{}finally{w=!1,l()}}else l();return}try{if(r.op==="ready"){let l=await m();a.run(),d?.postMessage({id:r.id,ok:!0,warnings:l})}else if(r.op==="seek"){let l=S;for(await p(r.t);s&&(S!==l||b());)await x(),l=S,await p(r.t);d?.postMessage({id:r.id,ok:!0,canvasLosses:h})}}catch(l){let g=l instanceof M?"frameTimeout":l instanceof N||l instanceof I?"layerStalled":void 0;d?.postMessage({id:r.id,ok:!1,error:l instanceof Error?l.message:String(l),...g?{code:g}:{}})}}e.addEventListener("message",r=>{if(r.source!==e.parent||r.ports.length!==1)return;let l=r.data;!l||l.type!=="ms-layer-connect"||l.v!==F||(d?.close(),d=r.ports[0],d.onmessage=g=>{let v=g.data;v&&typeof v=="object"&&(v.op==="ready"||v.op==="seek"||v.op==="tick")&&A(v)},d.postMessage({type:"hello",v:F}))})}B(window);})();
|