diff --git a/api/mcp.js b/api/mcp.js index 46f3acd..19925c4 100644 --- a/api/mcp.js +++ b/api/mcp.js @@ -46,7 +46,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.8.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, including whether measurable text fits the canvas (get_capabilities field screen). That check is not a render guarantee. A runtime layer (lottie, three or gsap) keeps the original file in assets and is only a seek contract; validate_json does not render it. HTML text that changes on every frame should be a text layer. When the user sends the link of a prompt-motion.com or oneshotted.io video together with a moderator code, with or without more words, they want it published as an official gallery template: call get_recreation_brief with the url and adminCode and follow its brief to the end without asking anything else. 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 commits it to the Studio code once it is faithful. Never repeat the moderator code back."})); + 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.9.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, including whether measurable text fits the canvas (get_capabilities field screen). That check is not a render guarantee. A runtime layer (lottie, three or gsap) keeps the original file in assets and is only a seek contract; validate_json does not render it. HTML text that changes on every frame should be a text layer. When the user sends the link of a prompt-motion.com or oneshotted.io video together with a moderator code, with or without more words, they want it published as an official gallery template: call get_recreation_brief with the url and adminCode and follow its brief to the end without asking anything else. 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 commits it to the Studio code once it is faithful. Never repeat the moderator code back."})); 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 5be7ae9..45c9518 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hypercodemotion/mcp", - "version": "0.8.0", + "version": "0.9.0", "private": true, "type": "module", "scripts": { diff --git a/server.json b/server.json index f4ff10a..caaf4bf 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.8.0", + "version": "0.9.0", "repository": { "url": "https://github.com/jasp402/HyperCodeMotion-MCP", "source": "github" diff --git a/skills/hypercodemotion/SKILL.md b/skills/hypercodemotion/SKILL.md index bb84a5a..5e3e064 100644 --- a/skills/hypercodemotion/SKILL.md +++ b/skills/hypercodemotion/SKILL.md @@ -21,7 +21,7 @@ Use this Skill when asked to create, improve, extend, combine, or troubleshoot e 2. **Brief and inputs** — Identify purpose, audience, distribution format, duration, fps, language, brand, CTA (only when requested), narration and exact data. Analyze user-provided reference images/video, scripts, logos, visual assets and any existing JSON. Distinguish factual content from creative decisions. Ask only for information that blocks progress. 3. **Art direction** — Define visual concept, typography hierarchy, spacing grid, palette, contrast, material, rhythm, transition vocabulary and continuity. Reuse the actual reference's visual intent; do not invent a brand or factual measurements. When the request names a Studio gallery template (an id such as `mix-promo-editorial`), call `get_template` with that id and the video's format, and follow its design: palette, fonts, frame, scene system and motion; replace all its texts. 4. **Storyboard and scenes** — Create sequential beats with one communicative objective each. Decide which objects persist and transform between beats versus which appear only within one scene. Allocate time to reading and transitions; storyboard timing must fit total duration. Join the beats with Studio scene transitions (`scenes[i].transition`: `fade`, `slide`, `push`, `wipe`, `clockWipe`, `iris`, `zoom`, `flip`, `blur`; exact shape in [motion-design](references/motion-design.md#scene-transitions)). -5. **Composition plan** — Default to **HTML scenes**: one HTML document per scene in `docs`, each visible piece marked `data-hcm` and animated by its own `html` layer (see [html-scenes](references/html-scenes.md) and [the example](examples/html-scene-launch.json)). Use native layers (`text`, `rect`, `particles`…) when a piece must be edited as geometry or needs per-property keyframes. Plan layer IDs, order, scene boundaries, assets, effects and audio. For a distinctive look (riso print, springs, camera, audio, UI mockups, per-scene backgrounds, the 13 style looks) call `get_motion_recipe` first: no name gives the index, a name gives validated JSON to adapt. +5. **Composition plan** — Default to **HTML scenes**: one HTML document per scene in `docs`, each visible piece marked `data-hcm` and animated by its own `html` layer (see [html-scenes](references/html-scenes.md) and [the example](examples/html-scene-launch.json)). Use native layers (`text`, `rect`, `particles`…) when a piece must be edited as geometry or needs per-property keyframes. Plan layer IDs, order, scene boundaries, assets, effects and audio. For a distinctive look (riso print, springs, camera, audio, UI mockups, per-scene backgrounds, the 13 style looks) or the finish of a talking-head video (`autoedit`: captions with emphasis, zooms, motion screens, sound effects) call `get_motion_recipe` first: no name gives the index, a name gives validated JSON to adapt. 6. **JSON generation** — Produce **one complete, standalone composition** with `layers` at the root. To change an existing project, return the whole updated composition (keep unchanged layers, IDs and assets as they were), not a patch: a `.hcmotion` file can only carry a full composition. In `code` layers and `components[x].draw`, never declare a name the engine already provides (`ctx`, `S`, `H`, `t`, `lt`, `dur`, `w`, `h`, `p`, `color`, `amount`, `amp`, `beat`, `hit`, `audio`, `c` and the helpers such as `range`, `lerp`, `rand`; full list in [composition.md](references/composition.md#code-layers-and-components)): use `SEG`, `ranges`, `pts`. 7. **MCP validation** — The HyperCodeMotion MCP (`https://mcp.hypercodemotion.com/mcp`) is required. Call `get_capabilities` before selecting layer/effect fields; always run `validate_json` on the finished draft before `create_hcmotion`: fix every error and warning, then go through the checklist it returns and correct the composition (overlaps, hierarchy, motion, audio) until it holds. Quality matters more than saving tokens: a second pass is expected. **Do not claim validation if no call occurred.** 8. **Studio handoff** — Call `create_hcmotion` with the finished JSON. If `created` is false, fix every error and call it again. Give the user the two links it returns: `open` (opens the project in Studio) and `download` (the `.hcmotion` file, to keep or load later with «Cargar proyecto»), and say both expire in 24 hours. **Never paste the composition JSON for the user to copy:** Studio only opens `.hcmotion` files. If the MCP is not connected, stop and ask the user to connect it instead of returning JSON. Never claim a perfect render from MCP results alone. diff --git a/src/recipes.js b/src/recipes.js index 63b92f0..420cee7 100644 --- a/src/recipes.js +++ b/src/recipes.js @@ -183,6 +183,67 @@ const sceneBg = { }, }; +// ---------- autoedit ---------- +// AutoEdit (Studio «Anima lo que tienes → Video» with a person talking): the user's video travels by id ("V1"), +// with its words timed; Claude does the professional finish. Same plan Studio builds without AI (welcome/ae.ts). +const autoedit = { + summary: "Talking-head finish: captions with one emphasised keyword, punch-in zooms, motion screens, sound effects, all by reference to the user's video.", + notes: [ + "Order of a pro edit: colour grade, zoom, captions, motion screens, sound. Screen ideas: get_motion_recipe('autoedit-screens').", + "The user's video is NOT in the JSON: a \"video\" layer FIRST, props.asset = the id given (V1), start 0, end = duration, covering the canvas. Studio fills the file and its voice; never add the voice or put the video in assets.", + "Zoom = \"scale\" keys on V1, cut-style: jump to 1.12 or 1.2 when the keyword is said (two keys 0.01 s apart), back to 1 when the next caption starts. Alternate levels, max one every ~2.5 s, none under a screen.", + "Captions: EVERY spoken word at its second, lower third (y 70% in 9:16, 76% in 16:9), never over the face (see the frames sheet), max 3 lines. Reading order: words before the keyword small (50-60 px, white, glow), the keyword ALONE on its line big (110-180 px, max 86% width), words after it small below. Small lines anim \"words\", stagger = word gap; keyword enters at its second (scale 0.6 or 1.8 -> 1, 0.3 s easeOutBack). A caption lasts until the next (min 0.8 s). Names «Sub C1»...", + "Emphasis, ONE style per video: accent colour | accent box | accent underline | white outline | karaoke (fromOpacity 0.35). One keyword per caption; numbers always.", + "Screens every 4-6 s on the strongest phrase, 1.3-2.6 s, covering the video (paper #F3F1EC + ink #111214 with the accent darkened, ink + accent, or the accent). Slide in 0.22 s, hard cut out. Phrase small, keyword or number huge (max 78% width), an HTML/SVG illustration of what is said, editorial details (index 01 / 04, hairlines, barcode, side labels). Names «Pantalla S1»...", + "Story first: the hook (first 3 s) and its numbers go huge; a changed number = old one struck out, new one big; irony keeps quotes («felices»); the topic becomes a visual (blackout = video darkens and flickers). Proposed keywords/screens are only a start.", + "User photos/videos (ids) = B-roll in screens under a 0.42 black veil, white text. An image you cannot draw: rect «Falta imagen: ».", + "audio.sfx (library ids): whoosh 0.7 before each screen, pop 0.35 on zooms, impact on a big number, riser (t = end) before the peak. Max one per 1.5 s.", + ], + fragment: { + background: "#000000", + duration: 4, + audio: { bpm: 120, volume: 0.8, auto: false, autoGain: 0.7, profile: "minimal", density: "normal", tracks: [], sfx: [{ id: "s1", t: 0.4, sound: "pop", gain: 0.35 }, { id: "s2", t: 2.12, sound: "whoosh", gain: 0.7 }] }, + layers: [ + { id: "v1", type: "video", name: "Tu video", start: 0, end: 4, props: { asset: "V1", x: 540, y: 960, w: 1080, h: 1920, scale: { kf: [{ t: 0, v: 1 }, { t: 0.39, v: 1 }, { t: 0.4, v: 1.12 }, { t: 1.99, v: 1.12 }, { t: 2, v: 1 }] } } }, + { id: "c1", type: "text", name: "Sub C1", start: 0, end: 2.2, props: { text: "If you want to", x: 540, y: 1290, size: 58, weight: 700, font: "Inter", color: "#FFFFFF", align: "center", glow: 34, glowColor: "rgba(0,0,0,.6)", anim: "words", stagger: 0.12, animDur: 0.18, animEase: "easeOutCubic", fromY: 20, fromOpacity: 0 } }, + { id: "c1box", type: "rect", name: "Sub C1 · caja", start: 0.4, end: 2.2, props: { x: 540, y: 1400, w: 720, h: 150, radius: 0, fill: "#FF6A3D", scaleX: { kf: [{ t: 0.4, v: 0, ease: "easeOutExpo" }, { t: 0.65, v: 1 }] } } }, + { id: "c1key", type: "text", name: "Sub C1 · clave", start: 0.4, end: 2.2, props: { text: "EARN MORE", x: 540, y: 1400, size: 130, weight: 400, font: "Archivo Black", color: "#111214", align: "center", anim: "words", stagger: 0, animDur: 0.3, animEase: "easeOutBack", fromScale: 0.6, fromOpacity: 0 } }, + { id: "s1bg", type: "rect", name: "Pantalla S1", start: 2.2, end: 4, props: { x: { kf: [{ t: 2.2, v: 1620, ease: "easeOutCubic" }, { t: 2.42, v: 540 }] }, y: 960, w: 1080, h: 1920, fill: "#F3F1EC" } }, + { id: "s1t", type: "text", name: "Pantalla S1 · frase", start: 2.35, end: 4, props: { text: "how much money", x: 540, y: 880, size: 46, weight: 500, font: "Inter", color: "#111214", opacity: 0.75, align: "center" } }, + { id: "s1k", type: "text", name: "Pantalla S1 · clave", start: 2.5, end: 4, props: { text: "COMPANY", x: 540, y: 1000, size: 180, weight: 400, font: "Archivo Black", color: "#8A4A1F", align: "center", anim: "chars", stagger: 0.03, animDur: 0.35, animEase: "easeOutBack", fromY: 70, fromBlur: 8, fromOpacity: 0 } }, + ], + }, +}; + +// ---------- autoedit screens ---------- +// Patterns seen in professional before/after talking-head edits (owner's references, 2026-11-02). +const autoeditScreens = { + summary: "What a talking-head edit puts on screen beyond captions: steps, lists, stat cards, logos, face in a circle, text behind the person.", + notes: [ + "Steps (\"3 ways\", \"first\", \"number two\"): an accent circle with the number beside the caption, then a frosted card (rect fill rgba(255,255,255,.14), radius 28, backdrop blur) whose text types in word by word.", + "Spoken list (\"shooting, editing, talking to clients\"): a dark card over a gradient of the video's main colour; one row per item (icon + label) appearing when it is said; the face shrinks into a circle at the top (V1 scale ~0.3, a circle mask or an ellipse frame).", + "Money or metrics: a UI stat card (label, big value, green when up, red when down) that counts to the value; for \"leads\" or \"clients\" a row of avatar circles.", + "Tools or brands: their logos as rounded app-icon stickers popping around the keyword (draw them in SVG, never pretend a real logo you cannot draw: use the name in a pill).", + "Keyword icons: 1-3 glossy 3D-style icons (emoji or SVG) pop around the big keyword with a glow in the accent.", + "Full-screen idea cards: a solid brand colour with a mixed-font line (sans + script for one word), split screens with B-roll photos and a huge title, or huge text BEHIND the person (needs a cut-out of the user: only with a recorte material).", + "Grid of B-roll: photos appear one by one in a 2x2 grid on a soft gradient, each with a label (SCRIPTING, FILMING, EDITING).", + "Voice-over cutaway (user asks to animate a phrase): a full-screen drawn scene (opaque rect over V1; the voice keeps playing) built from shapes that act out the words at their seconds (e.g. mug drops, sachet tilts, powder falls, spoon stirs, check on \"listo\"), captions in dark ink on it, hard cut or flash back to the person.", + "Polish: vary entrances (slide, scale-pop, wipe, chars) instead of one for every card; a transition between topic blocks; burned-in text in the source = ask for a clean video before covering it with a band; a real logo or photo is missing = «Falta imagen», never a fake.", + "Close with a call to action: a button-shaped card (\"Click the button below\") with 2-3 small steps under it.", + "Captions in these edits: heavy uppercase with a yellow or orange keyword and a dark outline, or lowercase tight-leading white with one word much bigger. Change the caption position only for a reason.", + ], + fragment: { + background: "#000000", + duration: 3, + layers: [ + { id: "n1", type: "ellipse", name: "Paso 1", start: 0, end: 3, props: { x: 170, y: 1180, w: 120, h: 120, fill: "#FF8A1F", scale: { kf: [{ t: 0, v: 0, ease: "easeOutBack" }, { t: 0.35, v: 1 }] } } }, + { id: "n1t", type: "text", name: "Paso 1 · número", start: 0, end: 3, props: { text: "1", x: 170, y: 1180, size: 70, weight: 400, font: "Archivo Black", color: "#111214" } }, + { id: "card", type: "rect", name: "Paso 1 · tarjeta", start: 0.2, end: 3, props: { x: 600, y: 1180, w: 680, h: 200, radius: 28, fill: "rgba(255,255,255,.14)" } }, + { id: "cardt", type: "text", name: "Paso 1 · texto", start: 0.3, end: 3, props: { text: "Learn the shortcuts", x: 600, y: 1180, size: 48, weight: 700, font: "Inter", color: "#FFFFFF", anim: "words", stagger: 0.15, animDur: 0.2, fromOpacity: 0 } }, + ], + }, +}; + // ---------- looks ---------- /** Title size caps so "Big idea" fits the 928 px safe width of a 1080 canvas (checked by tests/recipes.test.js). */ const TITLE_MAX = { medianoche: 130, maximalista: 130 }; @@ -266,7 +327,7 @@ function fragmentText(f) { return out.join("\n"); } -const STATIC = { riso, choreography, audio, camera, "code-texture": codeTexture, "ui-mockup": uiMockup, "scene-bg": sceneBg }; +const STATIC = { autoedit, "autoedit-screens": autoeditScreens, riso, choreography, audio, camera, "code-texture": codeTexture, "ui-mockup": uiMockup, "scene-bg": sceneBg }; function lookRecipe(look) { const C = look.colors, F = look.fonts, m = look.motion, s = look.shape; diff --git a/tests/recipes.test.js b/tests/recipes.test.js index 3ca8374..f22ad14 100644 --- a/tests/recipes.test.js +++ b/tests/recipes.test.js @@ -17,11 +17,11 @@ const call = async (name, args = {}) => { return JSON.parse(body).result; }; -test("there are 7 topic recipes, the looks table and one recipe per look", () => { +test("there are 9 topic recipes, the looks table and one recipe per look", () => { assert.equal(LOOKS.length, 13); - for (const n of ["riso", "choreography", "audio", "camera", "code-texture", "ui-mockup", "scene-bg", "looks"]) assert.ok(RECIPE_NAMES.includes(n), n); + for (const n of ["autoedit", "autoedit-screens", "riso", "choreography", "audio", "camera", "code-texture", "ui-mockup", "scene-bg", "looks"]) assert.ok(RECIPE_NAMES.includes(n), n); for (const l of LOOKS) assert.ok(RECIPE_NAMES.includes("look:" + l.id), l.id); - assert.equal(RECIPE_NAMES.length, 8 + 13); + assert.equal(RECIPE_NAMES.length, 10 + 13); }); test("every recipe passes validateJson with no errors and no warning but the contract notice", () => { @@ -48,7 +48,7 @@ test("recipe sizes stay on demand: topic recipes under ~800 tokens, looks table // 1 token ≈ 3.2 characters for JSON/HTML (cl100k measured on these recipes); generous bounds for (const name of RECIPE_NAMES) { const chars = recipeText(name).length; - assert.ok(chars < (name === "looks" ? 5200 : name === "riso" ? 4400 : 3200), name + " " + chars); + assert.ok(chars < (name === "looks" ? 5200 : name === "riso" || name.startsWith("autoedit") ? 4400 : 3200), name + " " + chars); } });