Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/mcp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hypercodemotion/mcp",
"version": "0.8.0",
"version": "0.9.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion skills/hypercodemotion/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
63 changes: 62 additions & 1 deletion src/recipes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: <what>».",
"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 };
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions tests/recipes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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);
}
});

Expand Down
Loading