From a6d0ac7f1ce47911bea8b779b9f1d65565d260b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 04:15:13 +0000 Subject: [PATCH] feat: video layers and user files referenced by id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Format: "video" layer (props.asset = id of the user's video, props.from = second of the file where the clip starts; the clip lasts end - start), accepted by create_hcmotion's Studio check and listed by get_capabilities. - get_capabilities explains how to reference the files the user keeps in Studio («Tus materiales»): by id only, never in assets or as URLs. Studio fills them in when the project opens; nothing is uploaded here. - validate_json / create_hcmotion: a video layer needs an id (not a URL), from must be 0 or more; warns when the user's video is put in assets. - Skill: composition contract and truthfulness rule cover user files and video clips. Contract snapshot synced with Studio (schema + presets). - Version 0.7.0. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01JVFrDoRJqaLyTwScsFCYtX --- README.md | 2 +- api/mcp.js | 4 +-- package.json | 2 +- server.json | 2 +- skills/hypercodemotion/SKILL.md | 2 +- .../hypercodemotion/references/composition.md | 16 +++++++++- .../hypercodemotion/references/html-scenes.md | 2 +- src/contract.js | 4 +++ src/hcmotion.js | 2 +- src/studio-contract.json | 29 +++++++++++++++++-- src/validator.js | 19 +++++++++++- tests/validator.test.js | 19 ++++++++++++ 12 files changed, 90 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 35dda1a..ad60a96 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Both links expire **24 hours** after they are created. Save the project from Stu | Tool | What it does | Annotations | | --- | --- | --- | -| `get_capabilities` | Lists the formats, layer types, layer properties and effect presets that Studio supports, with the Studio revision they come from. | read-only | +| `get_capabilities` | Lists the formats, layer types, layer properties and effect presets that Studio supports, how to reference files the user keeps in Studio (photos, logo, video clips) by id, and the Studio revision they come from. | read-only | | `create_hcmotion` | Validates a finished composition and packages it as a `.hcmotion` file. Invalid input returns `created: false` with the errors to fix and stores nothing. Valid input returns `open` and `download` links (24 h). | write (creates a temporary file), not destructive | | `get_template` | Returns the full design of a Studio gallery template by its id and format (brief, scenes, HTML scene documents and layers), to use as a design reference. | read-only | | `validate_json` | Checks a composition, design or improvement patch and returns errors and warnings. Preliminary: it does not prove a correct render. | read-only | diff --git a/api/mcp.js b/api/mcp.js index abecbe7..8909fe7 100644 --- a/api/mcp.js +++ b/api/mcp.js @@ -18,7 +18,7 @@ async function createHcmotion(args){ return {created:true,download:file.download,open:file.open,expiresAt:file.expiresAt,bytes:bytes.length,errors:[],warnings:report.warnings}; } const tools=[ -{name:"get_capabilities",title:"HyperCodeMotion Capabilities",description:"List the HyperCodeMotion JSON formats, layer types and properties, and effect presets that HyperCodeMotion Studio supports, with the Studio contract revision they come from.",annotations:{title:"HyperCodeMotion Capabilities",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{},additionalProperties:false},outputSchema:{type:"object",properties:{contractVersion:{type:"string"},source:{type:"string"},sourceSchemaSha:{type:"string"},sourcePresetsSha:{type:"string"},formats:{type:"array",items:{type:"string"}},layerProperties:{type:"object",additionalProperties:{type:"array",items:{type:"string"}}},presetSlots:{type:"object",additionalProperties:{type:"array",items:{type:"string"}}},fullyValidated:{type:"boolean"}},required:["contractVersion","formats","layerProperties","presetSlots","fullyValidated"]}}, +{name:"get_capabilities",title:"HyperCodeMotion Capabilities",description:"List the HyperCodeMotion JSON formats, layer types and properties, and effect presets that HyperCodeMotion Studio supports, how to reference files the user keeps in Studio by id, and the Studio contract revision they come from.",annotations:{title:"HyperCodeMotion Capabilities",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{},additionalProperties:false},outputSchema:{type:"object",properties:{contractVersion:{type:"string"},source:{type:"string"},sourceSchemaSha:{type:"string"},sourcePresetsSha:{type:"string"},formats:{type:"array",items:{type:"string"}},layerProperties:{type:"object",additionalProperties:{type:"array",items:{type:"string"}}},presetSlots:{type:"object",additionalProperties:{type:"array",items:{type:"string"}}},userMaterials:{type:"string"},fullyValidated:{type:"boolean"}},required:["contractVersion","formats","layerProperties","presetSlots","fullyValidated"]}}, {name:"create_hcmotion",title:"Create HyperCodeMotion project file",description:"Validate a finished HyperCodeMotion composition and package it as a .hcmotion project file, the format HyperCodeMotion Studio opens. If the composition has problems, returns created: false with the errors to fix and stores nothing. On success, stores the file temporarily and returns an `open` link (opens the project in HyperCodeMotion Studio) and a `download` link; both expire 24 hours after creation. It does not render video or generate media.",annotations:{title:"Create HyperCodeMotion project file",readOnlyHint:false,destructiveHint:false,idempotentHint:false,openWorldHint:false},inputSchema:{type:"object",properties:{json:{description:"The full composition (version, name, width, height, fps, duration, background, layers…), as an object or JSON-encoded string.",anyOf:[{type:"object"},{type:"string"}]},name:{type:"string",description:"Optional project name."}},required:["json"],additionalProperties:false},outputSchema:{type:"object",properties:{created:{type:"boolean"},download:{type:"string"},open:{type:"string"},expiresAt:{type:"string"},bytes:{type:"number"},errors:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}},warnings:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}}},required:["created","errors","warnings"]}}, {name:"get_template",title:"Get HyperCodeMotion template",description:"Return the full design of a template from the HyperCodeMotion Studio gallery, by its id and format: brief (palette, fonts, frame, rhythm), scenes with timings, and the composition with its HTML scene documents and layers. Use it as the design reference when asked to follow a template; it does not create or store anything.",annotations:{title:"Get HyperCodeMotion template",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{template:{type:"string",description:"Template id, such as \"mix-promo-editorial\"."},format:{type:"string",enum:TEMPLATE_FORMATS,description:"Aspect ratio of the video being made. Defaults to 16:9."}},required:["template"],additionalProperties:false},outputSchema:{type:"object",properties:{found:{type:"boolean"},template:{type:"string"},name:{type:"string"},format:{type:"string"},brief:{type:"string"},scenes:{type:"array",items:{type:"object"}},composition:{type:"object"},error:{type:"string"}},required:["found"]}}, {name:"validate_json",title:"Validate HyperCodeMotion JSON",description:"Check a composition (layers), design (elements), or improvement patch (add/change/remove/sfx) and return actionable errors and warnings. This is preliminary validation, not proof of a successful render.",annotations:{title:"Validate HyperCodeMotion JSON",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{json:{description:"HyperCodeMotion JSON object or JSON-encoded string to validate.",anyOf:[{type:"object"},{type:"string"}]},composition:{type:"object",description:"Optional original composition used to resolve patch references and timings."}},required:["json"],additionalProperties:false},outputSchema:{type:"object",properties:{valid:{type:"boolean"},format:{type:"string"},fullyValidated:{type:"boolean"},mode:{type:"string"},contractVersion:{type:"string"},errors:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}},warnings:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}}},required:["valid","format","fullyValidated","mode","errors","warnings","contractVersion"]}}, @@ -44,7 +44,7 @@ export default async function handler(req,res){ const id=data?.id??null; if(data?.jsonrpc!=="2.0"||typeof data.method!=="string")return reply(res,400,err(id,-32600,"Expected a JSON-RPC 2.0 request with jsonrpc: \"2.0\" and a method.")); if(data.method.startsWith("notifications/")){res.statusCode=202;return res.end();} - if(data.method==="initialize")return reply(res,200,ok(id,{protocolVersion:"2025-03-26",capabilities:{tools:{}},serverInfo:{name:"hypercodemotion-mcp",title:"HyperCodeMotion MCP",version:"0.6.2",description:"Creates HyperCodeMotion .hcmotion project files from AI-generated composition JSON, with validation and layer, effect and format discovery.",websiteUrl:"https://github.com/jasp402/HyperCodeMotion-MCP"},instructions:"HyperCodeMotion Studio opens .hcmotion project files. get_capabilities lists the supported formats, layers and effects. get_template returns the full design of a gallery template by its id. create_hcmotion validates a finished composition and returns open and download links to its .hcmotion file (valid 24 hours); when created is false, its errors say what to fix. validate_json checks drafts; its check is preliminary, not a render guarantee. To turn a public video into a gallery template, get_recreation_brief takes its page link and returns the source, the brief and reference frames of the original; propose_template submits the recreation, which is rendered and compared with the original; get_proposal_status returns that comparison (send fixes with propose_template and the same code); publish_template publishes it once it is faithful."})); + if(data.method==="initialize")return reply(res,200,ok(id,{protocolVersion:"2025-03-26",capabilities:{tools:{}},serverInfo:{name:"hypercodemotion-mcp",title:"HyperCodeMotion MCP",version:"0.7.0",description:"Creates HyperCodeMotion .hcmotion project files from AI-generated composition JSON, with validation and layer, effect and format discovery.",websiteUrl:"https://github.com/jasp402/HyperCodeMotion-MCP"},instructions:"HyperCodeMotion Studio opens .hcmotion project files. get_capabilities lists the supported formats, layers and effects. get_template returns the full design of a gallery template by its id. create_hcmotion validates a finished composition and returns open and download links to its .hcmotion file (valid 24 hours); when created is false, its errors say what to fix. validate_json checks drafts; its check is preliminary, not a render guarantee. To turn a public video into a gallery template, get_recreation_brief takes its page link and returns the source, the brief and reference frames of the original; propose_template submits the recreation, which is rendered and compared with the original; get_proposal_status returns that comparison (send fixes with propose_template and the same code); publish_template publishes it once it is faithful."})); if(data.method==="ping")return reply(res,200,ok(id,{})); if(data.method==="tools/list")return reply(res,200,ok(id,{tools})); if(data.method==="tools/call"){ diff --git a/package.json b/package.json index 093dcc5..322c623 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hypercodemotion/mcp", - "version": "0.6.2", + "version": "0.7.0", "private": true, "type": "module", "scripts": { diff --git a/server.json b/server.json index ca0fbbb..53cba85 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.jasp402/hypercodemotion-mcp", "title": "HyperCodeMotion MCP", "description": "Validate AI-made motion graphics and package them as editable HyperCodeMotion projects.", - "version": "0.6.2", + "version": "0.7.0", "repository": { "url": "https://github.com/jasp402/HyperCodeMotion-MCP", "source": "github" diff --git a/skills/hypercodemotion/SKILL.md b/skills/hypercodemotion/SKILL.md index 1c93083..4d68a23 100644 --- a/skills/hypercodemotion/SKILL.md +++ b/skills/hypercodemotion/SKILL.md @@ -44,7 +44,7 @@ Load only the applicable reference files instead of pasting all reference conten ## Non-negotiable truthfulness -`validate_json` currently performs **structural preflight** against a source-pinned capability snapshot, returns `fullyValidated: false`, and **cannot verify appearance or guarantee Studio import/rendering**. Images in a prompt are references; they are not automatically embedded in JSON. Never invent asset IDs, live external image URLs, render results, tool calls, or successful tests. +`validate_json` currently performs **structural preflight** against a source-pinned capability snapshot, returns `fullyValidated: false`, and **cannot verify appearance or guarantee Studio import/rendering**. Images in a prompt are references; they are not automatically embedded in JSON. The exception is files the user lists by id (photos, cut-outs, a logo, videos kept in their Studio): reference those ids in `image` and `video` layers as [the contract](references/composition.md) describes. Never invent other asset IDs, live external image URLs, render results, tool calls, or successful tests. ## Output contract diff --git a/skills/hypercodemotion/references/composition.md b/skills/hypercodemotion/references/composition.md index e461e26..56b8ba1 100644 --- a/skills/hypercodemotion/references/composition.md +++ b/skills/hypercodemotion/references/composition.md @@ -38,12 +38,26 @@ This is an illustrative sample, not evidence of a successful real render. ## Layers and timing -Supported layer types from the current contract snapshot: `text`, `rect`, `ellipse`, `polygon`, `particles`, `image`, `code`, `camera`, `svg`, `ui`, `cursor`, `html`, `component`. Query `get_capabilities` for properties per type. Layer order determines draw order (earliest at back). Use stable, unique IDs. `start` and `end` denote visible interval in composition seconds, `[start,end)`. `parent` refers to another layer ID; `clip` can mask children where supported; `fixed` remains screen-fixed through camera or scene transitions. +Supported layer types from the current contract snapshot: `text`, `rect`, `ellipse`, `polygon`, `particles`, `image`, `video`, `code`, `camera`, `svg`, `ui`, `cursor`, `html`, `component`. Query `get_capabilities` for properties per type. Layer order determines draw order (earliest at back). Use stable, unique IDs. `start` and `end` denote visible interval in composition seconds, `[start,end)`. `parent` refers to another layer ID; `clip` can mask children where supported; `fixed` remains screen-fixed through camera or scene transitions. A property value may be fixed (number/string), `{"kf":[{"t":0,"v":0},{"t":1,"v":100,"ease":"easeOutCubic"}]}`, or `{"expr":"..."}`. Keyframe timestamps are global composition seconds. Spring targets may be specified by `spring` where supported by Studio; check actual accepted names. Optional composition structures include `scenes` (sequential periods and transitions), `docs` (HTML templates), `assets`, `audio`, `camera`, `params`, `plan`, `markers` and `components`. Do not fabricate structures: inspect Studio code for advanced fields. +## The user's own files (photos, cut-outs, logo, videos) + +When the request lists the user's files by id (Studio's «Tus materiales», e.g. `"video-1" · «playa.mp4» · video · 1920×1080 · 12.4 s`), those files already live in the user's Studio and are filled in by id when the project opens. Nothing is uploaded to the MCP. + +- Image (photo, cut-out or logo): an `image` layer with `"asset": ""` and its own proportion, or `` inside an HTML scene. +- Video: a `video` layer. `props.from` is the second of the file where the clip starts; the clip lasts `end - start`. Keep `from + (end - start)` within the file's duration. Videos play without sound (sound lives in Studio's audio channels). For full frame use the canvas `w`/`h`: the video is cropped to cover, never stretched. + +```json +{"id":"clip1","name":"Clip · playa","type":"video","start":2,"end":5.5,"props":{"asset":"video-1","from":6.4,"x":540,"y":960,"w":1080,"h":1920}} +``` + +- Do not add these ids to `assets` and never use a URL for them. Use only ids the user listed; an unknown id opens as an empty slot. +- Videos usually arrive as a sheet of frames with their seconds (`materiales-para-claude.png`): pick each clip's `from` from it. + ## Editing an existing project Start from the user's composition and return all of it with the changes applied. Keep untouched layers, IDs, docs, assets and audio byte-for-byte; change only what was asked. Keep `start`/`end` and keyframe times inside `duration`. Deliver it through `create_hcmotion` like a new project. diff --git a/skills/hypercodemotion/references/html-scenes.md b/skills/hypercodemotion/references/html-scenes.md index 84f5fe4..5007466 100644 --- a/skills/hypercodemotion/references/html-scenes.md +++ b/skills/hypercodemotion/references/html-scenes.md @@ -33,7 +33,7 @@ Use HTML scenes for precise editorial layouts, dashboards and typographic compos ## Image assets -A photo in a user prompt is a **reference**, not automatically an embedded asset. Composition `assets` maps asset IDs to actual data URLs; image layers must use an asset/source format Studio supports. Verify IDs and actual media availability instead of inventing a Pexels image URL, claiming a fetch succeeded, or fabricating a brand logo. +Files the user lists by id stay in their Studio: use `` or an `image`/`video` layer with that id (see [composition](composition.md)). Any other photo in a user prompt is a **reference**, not automatically an embedded asset. Composition `assets` maps asset IDs to actual data URLs; image layers must use an asset/source format Studio supports. Verify IDs and actual media availability instead of inventing a Pexels image URL, claiming a fetch succeeded, or fabricating a brand logo. An external asset may require Studio to fetch and embed it. If not available, supply an explicitly labelled placeholder and state what is required to complete import. Avoid remote dependencies for reproducible compositions when assets can be bundled. diff --git a/src/contract.js b/src/contract.js index 2fe814a..0f3caf2 100644 --- a/src/contract.js +++ b/src/contract.js @@ -1,11 +1,15 @@ import contract from "./studio-contract.json" with { type: "json" }; import { validateComposition } from "./validator.js"; +/** The user's own files («Tus materiales» in Studio): referenced by id, never uploaded here. */ +const USER_MATERIALS = "Files the user listed by id in their request (photos, cut-outs, a logo, videos) stay in Studio and are filled in by id when the project opens. Reference them with props.asset = : an image as an \"image\" layer (or \"> in an HTML scene), a video as a \"video\" layer whose props.from is the second of the file where the shown clip starts; the clip lasts end - start and plays without sound. Do not put them in \"assets\" or as URLs, and use only ids the user gave."; + export function getCapabilities() { return { contractVersion: "studio-snapshot-1", source: contract.source, sourceSchemaSha: contract.sourceSchemaSha, sourcePresetsSha: contract.sourcePresetsSha, formats: ["composition", "design", "improvement"], layerProperties: contract.types, presetSlots: contract.presets, + userMaterials: USER_MATERIALS, fullyValidated: false }; } diff --git a/src/hcmotion.js b/src/hcmotion.js index 638209d..7980005 100644 --- a/src/hcmotion.js +++ b/src/hcmotion.js @@ -13,7 +13,7 @@ */ const MAGIC = [0x48, 0x43, 0x4d]; const HEAD = 48; -const LAYER_TYPES = new Set(["text","rect","ellipse","polygon","particles","image","code","camera","svg","ui","cursor","html","component"]); +const LAYER_TYPES = new Set(["text","rect","ellipse","polygon","particles","image","video","code","camera","svg","ui","cursor","html","component"]); const isObj = x => !!x && typeof x === "object" && !Array.isArray(x); const num = x => typeof x === "number" && Number.isFinite(x); diff --git a/src/studio-contract.json b/src/studio-contract.json index 97e83f3..885bf11 100644 --- a/src/studio-contract.json +++ b/src/studio-contract.json @@ -1,8 +1,8 @@ { "source": "jasp402/HyperCodeMotion", - "sourceSchemaSha": "c4088722b2919d164401a944e5a88e633dbbec4b", - "sourcePresetsSha": "57593271af877058bd0b9a1b77a063dd013564b7", - "generatedAt": "2026-10-13", + "sourceSchemaSha": "fb6f8502cb87baf9980e793f619b4c8de32e65c2", + "sourcePresetsSha": "3b1ce95290d180668bf102b2da64ae9a75d08c8b", + "generatedAt": "2026-10-20", "types": { "text": [ "text", @@ -209,6 +209,29 @@ "rOpacity", "rDelay" ], + "video": [ + "asset", + "from", + "w", + "h", + "radius", + "reveal", + "x", + "y", + "scale", + "scaleX", + "scaleY", + "rotation", + "opacity", + "depth", + "rotateX", + "rotateY", + "perspective", + "glow", + "glowColor", + "blur", + "blend" + ], "code": [ "w", "h", diff --git a/src/validator.js b/src/validator.js index c6f6a38..64f3776 100644 --- a/src/validator.js +++ b/src/validator.js @@ -67,6 +67,7 @@ export function validateComposition(input, options = {}) { fail(`${p}.end`, "OUTSIDE_DURATION", "Layer end exceeds composition duration."); if (layer.parent !== undefined && layer.parent !== null && typeof layer.parent !== "string") fail(`${p}.parent`, "INVALID_PARENT", "Parent must be a layer id or null."); + if (layer.type === "video" && obj(layer.props)) checkVideo(layer, p, fail, warn, input.assets); } input.layers.forEach((layer, i) => { if (!obj(layer) || typeof layer.parent !== "string") return; @@ -89,6 +90,22 @@ export function validateComposition(input, options = {}) { } } +/** + * A video layer shows the user's own file (an id from their request; Studio fills it in on open): + * props.from is the second of the file where its clip starts and the clip lasts end - start. + */ +function checkVideo(layer, p, fail, warn, assets) { + const { asset, from } = layer.props; + if (typeof asset !== "string" || !asset.trim()) + fail(`${p}.props.asset`, "VIDEO_NEEDS_ASSET", "A video layer needs props.asset: the id of a video the user listed."); + else if (/^(https?:|data:|blob:)/i.test(asset)) + fail(`${p}.props.asset`, "VIDEO_ASSET_NOT_ID", "Use the id of the user's video, not a URL or data: Studio fills it in on open."); + else if (obj(assets) && Object.hasOwn(assets, asset)) + warn(`${p}.props.asset`, "VIDEO_IN_ASSETS", "The user's videos stay in Studio: reference the id without adding it to assets."); + if (from !== undefined && (!finite(from) || from < 0)) + fail(`${p}.props.from`, "INVALID_CLIP_START", "props.from must be a number of seconds, 0 or more."); +} + /** * Preflight for the Studio "Mejorar con IA" operations format. * Compatible with the envelope accepted by packages/core/src/improve.ts, @@ -144,7 +161,7 @@ export function validateImprovement(input, { composition } = {}) { if (l.z !== undefined && l.z !== "fondo" && l.z !== "frente") warn(`${p}.z`, "UNKNOWN_Z", 'Only "fondo" and "frente" are meaningful.'); if (typeof l.type !== "string" || !l.type) fail(`${p}.type`, "MISSING_TYPE", "New layers require a type."); - if (l.type === "image") warn(`${p}.type`, "IGNORED_IMAGE", "Studio discards new image layers in improvements."); + if (l.type === "image" || l.type === "video") warn(`${p}.type`, "IGNORED_IMAGE", "Studio discards new image and video layers in improvements."); if (!obj(l.props)) fail(`${p}.props`, "INVALID_PROPS", "Expected layer props object."); if (!finite(l.start) || !finite(l.end) || l.start < 0 || l.start >= l.end) fail(p, "INVALID_TIMING", "Expected 0 <= start < end."); diff --git a/tests/validator.test.js b/tests/validator.test.js index bac73d9..b0f47b2 100644 --- a/tests/validator.test.js +++ b/tests/validator.test.js @@ -76,3 +76,22 @@ test("rejects conflicting top-level formats", () => { const result = validateComposition({ ...example(), add: [] }); assert.ok(result.errors.some(x => x.code === "AMBIGUOUS_FORMAT")); }); + +test("video layers reference the user's file by id and a clip start", async () => { + const { validateJson, getCapabilities } = await import("../src/contract.js"); + const { checkComposition } = await import("../src/hcmotion.js"); + const base = { version: 1, name: "V", width: 1080, height: 1920, fps: 30, duration: 8, background: "#000", params: {} }; + const clip = (props) => ({ ...base, layers: [{ id: "c", name: "Clip", type: "video", start: 1, end: 4, props }] }); + const ok = validateJson(clip({ asset: "video-1", from: 3.2, w: 1080, h: 1920 })); + assert.equal(ok.valid, true, JSON.stringify(ok.errors)); + assert.ok(!ok.warnings.some((w) => w.code === "UNKNOWN_LAYER_TYPE" || w.code === "UNKNOWN_PROPERTY")); + assert.deepEqual(checkComposition(clip({ asset: "video-1" })), []); + const codes = (r) => r.errors.map((e) => e.code); + assert.deepEqual(codes(validateJson(clip({ from: 1 }))), ["VIDEO_NEEDS_ASSET"]); + assert.deepEqual(codes(validateJson(clip({ asset: "https://x.test/a.mp4" }))), ["VIDEO_ASSET_NOT_ID"]); + assert.deepEqual(codes(validateJson(clip({ asset: "video-1", from: -2 }))), ["INVALID_CLIP_START"]); + const inAssets = validateJson({ ...clip({ asset: "video-1" }), assets: { "video-1": "data:video/mp4;base64,AAAA" } }); + assert.ok(inAssets.warnings.some((w) => w.code === "VIDEO_IN_ASSETS")); + assert.match(getCapabilities().userMaterials, /props\.from/); + assert.ok(getCapabilities().layerProperties.video.includes("from")); +});