From ec770b168bb5152716cc3028d7069c00b5ff4ac5 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2026 14:57:41 -0700 Subject: [PATCH 1/6] chore(skills): package Codex plugin upload --- README.md | 10 +++++++++ package.json | 1 + scripts/package-codex-plugin.mjs | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 scripts/package-codex-plugin.mjs diff --git a/README.md b/README.md index a94b9cd123..b3e8d69560 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,16 @@ Default to the **core set** — the router installs each creation workflow on de Installs stay lean after that: `npx hyperframes init` keeps the **core set** fresh (the router, the `hyperframes-*` domain skills, and `media-use` — plus whatever is already installed; `/figma` stays on demand) and never expands a partial install; the creation workflows install **on demand** — the router runs `npx hyperframes skills update ` before entering one. Nothing re-pulls the full set behind your back. +### Upload to Codex + +Build the upload-ready Codex plugin archive from the committed `HEAD` version of the manifest, brand assets, and skills: + +```bash +bun run package:codex-plugin +``` + +This writes `dist/hyperframes-plugin.zip` with a `hyperframes/` root folder and fails if the archive exceeds Codex's 100 MB upload limit. + ### Router | Skill | Use when | diff --git a/package.json b/package.json index 81da41d9f7..13a2a73e3a 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "test:skills": "node --test 'skills/**/*.test.mjs'", "generate:previews": "tsx scripts/generate-template-previews.ts", "generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts", + "package:codex-plugin": "node scripts/package-codex-plugin.mjs", "upload:docs-images": "bash scripts/upload-docs-images.sh", "prepare": "test -d .git && lefthook install || true" }, diff --git a/scripts/package-codex-plugin.mjs b/scripts/package-codex-plugin.mjs new file mode 100644 index 0000000000..8ba4fe1c32 --- /dev/null +++ b/scripts/package-codex-plugin.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { mkdirSync, rmSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const REPO_ROOT = join(import.meta.dirname, ".."); +const OUTPUT = join(REPO_ROOT, "dist", "hyperframes-plugin.zip"); +const MAX_UPLOAD_BYTES = 100 * 1_000_000; +const PLUGIN_PATHS = [".codex-plugin", "assets/icon.png", "assets/logo.png", "skills"]; + +mkdirSync(join(REPO_ROOT, "dist"), { recursive: true }); + +execFileSync( + "git", + [ + "archive", + "--format=zip", + "--prefix=hyperframes/", + "--output", + OUTPUT, + "HEAD", + "--", + ...PLUGIN_PATHS, + ], + { cwd: REPO_ROOT, stdio: "inherit" }, +); + +const bytes = statSync(OUTPUT).size; +if (bytes > MAX_UPLOAD_BYTES) { + rmSync(OUTPUT); + throw new Error( + `Codex plugin archive is ${(bytes / 1_000_000).toFixed(1)} MB; the upload limit is 100 MB.`, + ); +} + +console.log(`Wrote ${OUTPUT} (${(bytes / 1_000_000).toFixed(1)} MB).`); From eee6eb8ed787d2700986b39c201c7463db879127 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 20 Jul 2026 15:59:48 -0700 Subject: [PATCH 2/6] chore(skills): harden Codex plugin content --- skills-manifest.json | 26 +-- skills/embedded-captions/CATALOG.md | 4 +- skills/embedded-captions/SKILL.md | 6 +- .../assets/brand/CDPR-fankit-terms.txt | 35 ---- .../assets/brand/CyberpunkReplica.ttf | Bin 16684 -> 0 bytes .../assets/brand/cyberpunk-widths.json | 115 ---------- skills/embedded-captions/dna/README.md | 4 +- skills/embedded-captions/dna/neon.json | 4 +- .../embedded-captions/scripts/make-theme.cjs | 196 ------------------ skills/embedded-captions/themes/PORTING.md | 1 - skills/embedded-captions/themes/README.md | 1 - .../embedded-captions/themes/nightcity.json | 56 ----- .../references/motion-language.md | 2 +- skills/figma/SKILL.md | 32 +-- skills/general-video/SKILL.md | 2 +- .../scripts/package-loader.mjs | 2 +- .../transitions/css-destruction.md | 2 +- .../references/upgrade-info-misc.md | 2 +- .../references/subagent-dispatch.md | 32 ++- .../{claude => code-editorial}/FRAME.md | 10 +- .../caption-skin.html | 8 +- .../fonts/EBGaramond-400.woff2 | Bin .../fonts/EBGaramond-700.woff2 | Bin .../fonts/Inter-400.woff2 | Bin .../fonts/Inter-700.woff2 | Bin .../fonts/JetBrainsMono-400.woff2 | Bin .../fonts/JetBrainsMono-700.woff2 | Bin .../fonts/OFL-eb-garamond.txt | 0 .../fonts/OFL-inter.txt | 0 .../fonts/OFL-jetbrains-mono.txt | 0 .../frame-showcase.html | 6 +- .../references/design-spec.md | 2 +- .../scripts/package-loader.mjs | 2 +- .../media-use/references/setup-providers.md | 5 +- .../references/telemetry-dashboard.md | 23 +- skills/pr-to-video/SKILL.md | 22 +- .../pr-to-video/references/code-vocabulary.md | 8 +- .../pr-to-video/references/motion-language.md | 2 +- skills/pr-to-video/references/story-design.md | 12 +- .../pr-to-video/references/visual-design.md | 16 +- .../scripts/frame-contract.test.mjs | 10 +- skills/pr-to-video/scripts/ingest.mjs | 4 +- skills/pr-to-video/sub-agents/frame-worker.md | 6 +- .../references/cut-catalog.md | 2 +- .../references/motion-language.md | 2 +- skills/talking-head-recut/SKILL.md | 3 +- 46 files changed, 125 insertions(+), 540 deletions(-) delete mode 100644 skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt delete mode 100644 skills/embedded-captions/assets/brand/CyberpunkReplica.ttf delete mode 100644 skills/embedded-captions/assets/brand/cyberpunk-widths.json delete mode 100644 skills/embedded-captions/themes/nightcity.json rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/FRAME.md (97%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/caption-skin.html (96%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/EBGaramond-400.woff2 (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/EBGaramond-700.woff2 (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/Inter-400.woff2 (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/Inter-700.woff2 (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/JetBrainsMono-400.woff2 (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/JetBrainsMono-700.woff2 (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/OFL-eb-garamond.txt (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/OFL-inter.txt (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/fonts/OFL-jetbrains-mono.txt (100%) rename skills/hyperframes-creative/frame-presets/{claude => code-editorial}/frame-showcase.html (99%) diff --git a/skills-manifest.json b/skills-manifest.json index 737c286b10..d52be995d3 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -2,19 +2,19 @@ "source": "heygen-com/hyperframes", "skills": { "embedded-captions": { - "hash": "cabda69328a29062", - "files": 144 + "hash": "e720500bda376535", + "files": 140 }, "faceless-explainer": { - "hash": "ff47c02598dc7947", + "hash": "d53bcc3cbcfaa7bb", "files": 22 }, "figma": { - "hash": "0e6e96f5a76ff824", + "hash": "f8832ad44acf80d6", "files": 2 }, "general-video": { - "hash": "94fd399a0dd0b6ce", + "hash": "3303742225924078", "files": 4 }, "hyperframes": { @@ -22,19 +22,19 @@ "files": 17 }, "hyperframes-animation": { - "hash": "7b3cd6bf281b1141", + "hash": "78c12ab0f31e2095", "files": 104 }, "hyperframes-cli": { - "hash": "b1a0725560016894", + "hash": "7bba303bc9002695", "files": 11 }, "hyperframes-core": { - "hash": "23f64febbc5cae37", + "hash": "3addccfebe2862df", "files": 19 }, "hyperframes-creative": { - "hash": "b9e2cbfa49e6ed6c", + "hash": "e763a7030152e88f", "files": 78 }, "hyperframes-keyframes": { @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "b10751149c9ea1da", + "hash": "9806c03a0c4d6e19", "files": 145 }, "motion-graphics": { @@ -58,11 +58,11 @@ "files": 132 }, "pr-to-video": { - "hash": "d37f9f5411c34089", + "hash": "524af95d6273beb5", "files": 29 }, "product-launch-video": { - "hash": "10e0b80f7040ad1e", + "hash": "530a22c8acf75051", "files": 26 }, "remotion-to-hyperframes": { @@ -74,7 +74,7 @@ "files": 2 }, "talking-head-recut": { - "hash": "0d0ede12041c51b7", + "hash": "2f5d99f823c48e75", "files": 28 } } diff --git a/skills/embedded-captions/CATALOG.md b/skills/embedded-captions/CATALOG.md index 71dd062b05..8e6d1cd1cc 100644 --- a/skills/embedded-captions/CATALOG.md +++ b/skills/embedded-captions/CATALOG.md @@ -24,7 +24,7 @@ Routing procedure (replaces any mode decision): | `keynote` | scene-embedded column flow | confident centered slides-in-the-scene | launches, founder updates, tech explainers (mood over verbatim reading) | any mid/dark | cinematic.json → make-cinematic | | `documentary` | scene-embedded column flow | burn-in stillness, bone on charcoal | interviews, journalism, testimony — gravitas IS the style | any | cinematic.json → make-cinematic | | `loud` | scene-embedded, body in front | percussion type that competes with the face | hype, social reels, sport, music, motivational | any (counter-accent handles warm) | cinematic.json → make-cinematic | -| `neon` | scene-embedded column flow | captions as neon signage in the dark | cyberpunk / nightlife / tech-noir / music | dark scenes | cinematic.json → make-cinematic | +| `neon` | scene-embedded column flow | captions as neon signage in the dark | neon-noir / nightlife / tech-noir / music | dark scenes | cinematic.json → make-cinematic | | `glitch` | scene-embedded column flow | corrupted-feed RGB-split type | hacker / AI / dystopia content | mid/dark | cinematic.json → make-cinematic | | `chrome` | scene-embedded column flow | liquid-metal hero with sheen sweeps | fashion-tech, Y2K, product-flex | mid/dark | cinematic.json → make-cinematic | | `velocity` | scene-embedded column flow | every word arrives along its motion vector | sport, automotive, fitness, kinetic content | any mid/dark | cinematic.json → make-cinematic | @@ -35,7 +35,6 @@ Routing procedure (replaces any mode decision): | `stardust` | accumulating poem, condensing letters | words condense from dust and return to it | poetic / memory / science-wonder monologues | one clear column side (probe fails → fall back to `cream`) | theme.json → make-theme | | `stomp` | full-frame takeover cards | hard-cut typographic takeover with a void before the drop | hooks, manifestos, ad openers — the words ARE the show | any (plate runs dimmed) | theme.json → make-theme | | `lastpage` | manuscript rail + a blurred future-field | the future hangs unreadable in the room; one rack-focus reveals every faint line was the apex word | visionary thesis, prophecy-toned talks, trailers, literary content | mid/dark (the field needs air) | theme.json → make-theme | -| `nightcity` | corrupted-UI rail + cover-letterform apex | body words boot through a corrupted-cyan frame into acid-yellow UI; the apex slams in BEHIND the subject set in the CP2077 cover replica typeface (cyan offset, baseline streak, circuit trace), tears in as displaced slices, lives as a glitching print | gaming / tech-edge / glitch culture / hype drops, "赛博朋克/游戏封面" asks | any (the lockup carries its own contrast); CP2077-adjacent content only (fan-kit terms) | theme.json → make-theme | | `scoreboard` | split-flap chyron rail + departures-board apex | stadium split-flap mechanics — body words flip-cycle wrong words on tiles before clacking onto the real one; ROW counter + odometer chip furniture; the apex is a giant departures board behind the subject that locks the word letter-by-letter under floodlights and a crowd-flash, status ticker types on | sports, transit/travel, scores, countdowns, reveals, "翻牌/记分牌/机场大屏" asks | any (board + chyron bring their own dark metal contrast) | theme.json → make-theme | | `transit` | LED departure-strip rail + illumination-wipe apex | amber dot-matrix station PA — body words light column-by-column (L→R LED wipe + arrival flicker) on a docked departure strip whose lines page by scroll-snap; platform tag, ticking clock, ⚠ DELAYED → ● ON TIME gag; the apex panel expands behind the subject inside a chasing LED-marquee border, the word illuminates column-by-column under a PA flash + ding pulses, then pages up and collapses | travel/transit, announcements, schedules, countdowns, retro-terminal, "LED屏/车站到站/点阵屏" asks | any (LED strip + panel bring their own dark contrast) | theme.json → make-theme | | `vhs` | camcorder OSD rail + REC-FREEZE tracking-slam apex | tape playback — permanent lens vignette + OSD furniture (▶ PLAY, ● REC, blinking green timestamp); body words carry a permanent red/cyan misregistration fringe, arrive by tracking glitch and REWIND off with flat-white echoes + scrub lines (the deck flips PLAY→REW in dead air); the apex is REC FREEZE: 1-frame white tear + head-switching noise bar, the word slams in behind the subject as three sheared tracking bands that SNAP into register, misregistration breathes through the hold under tape weave + auto-iris, then rewinds off | retro / nostalgia / found-footage / home-video, '90s tech, music & lifestyle edits, "录像带/VHS/复古/DV" asks | any (backlight-comp scrim makes its own contrast on bright skies) | theme.json → make-theme | @@ -66,7 +65,6 @@ Routing procedure (replaces any mode decision): every beat (`loud`). - **Named worlds** → direct hits: 控制台/终端 → `terminal`; 霓虹/招牌 → `neonsign`; 诗/星尘/记忆 → `stardust`; 军事/重击 → `ordnance`; 预言/未来/悬念揭示 → `lastpage`; - 赛博朋克/游戏封面/夜之城 → `nightcity`. - **Scene constraints veto last**: luma > 180 kills cream/screen column looks (→ `ink` or a rail/panel surface); dusk/dark unlocks `neon`/`neonsign`. - **Unsure** → `anchor` (the conservative default: words read, scene safe, zero spectacle). diff --git a/skills/embedded-captions/SKILL.md b/skills/embedded-captions/SKILL.md index c33b2d481d..d64a9a0b32 100644 --- a/skills/embedded-captions/SKILL.md +++ b/skills/embedded-captions/SKILL.md @@ -165,8 +165,8 @@ Both modes draw from **[dna/](dna/README.md)** — ten art-directed visual langu | **keynote** | tech-premium | product / launch | opaque white Inter 800, dead-center stillness | | **documentary** | formal | interview / serious | burn-in reveals, no hero — gravitas IS the style | | **loud** | loud | hype / sport / social | Anton + scene-sampled accent, single-unit slam + ripple; body ANNOUNCES in front (`bodyLayer: fg`) | -| **neon** | loud-cyber | cyberpunk / nightlife / tech-noir (dark scenes) | electric-cyan signage, ignition flicker, the hero powers ON like a sign | -| **glitch** | loud-cyber | digital / hacker / AI | RGB-split echoes snap together on landing; machine-percussive timing | +| **neon** | loud-neon | neon-noir / nightlife / tech-noir (dark scenes) | electric-cyan signage, ignition flicker, the hero powers ON like a sign | +| **glitch** | loud-neon | digital / hacker / AI | RGB-split echoes snap together on landing; machine-percussive timing | | **chrome** | loud-luxe | Y2K / fashion-tech / music | liquid-metal gradient hero + one sheen sweep during the hold | | **velocity** | loud-sport | sport / auto / fitness | every word arrives along its motion vector (streak+skew), hero passes with speed trails | @@ -230,7 +230,7 @@ The full **embed-track** playbook lives in **[references/composition-craft.md](r - **Face must never be 100%-covered continuously** — every 0.3s window, face bbox ≥30% uncovered. - **WCAG contrast** — final render lints; fix palette if it fails. - **Deterministic** — no `Math.random()`, no `Date.now()`, no `repeat:-1`. -- **Never grade/recolor the video.** The footage ships untouched — captions are the only addition. No full-frame scanlines / duotone / darken / vignette over the a-roll. Cyberpunk/CRT texture belongs _inside_ a caption element, not over the whole frame. +- **Never grade/recolor the video.** The footage ships untouched — captions are the only addition. No full-frame scanlines / duotone / darken / vignette over the a-roll. neon-noir/CRT texture belongs _inside_ a caption element, not over the whole frame. - **Rail-first for talking-head / explainer.** Don't embed the whole transcript — most text is the rail; embed only peaks. Embedding everything is the default mistake. - **Embed is scarce + spaced.** ≤1 embed per sentence/beat, never two adjacent or co-visible, ≥ a beat apart, at most one `apex`. climax = per-beat peak, **not** "the single payoff of the entire clip." - **Matte = the PERSON (hyperframes `remove-background`, u2net_human_seg, Apache-2.0).** Human segmentation by intent, but not surgically: thin offset furniture (mic boom arms) is usually excluded — captions render over it, behind the person — while large salient objects NEAR the subject (a telescope, a desk rig) can still leak into the matte and occlude captions. Objects HELD by the subject (products, phones) may drop out intermittently, letting captions pass in front. NEVER assume: sample `frames_fg/` at 2-3 timestamps before placing the hero, and prefer hero positions clear of any leaked furniture (`heroAnchor` can be skewed by leaks — cross-check against frames_bg). diff --git a/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt b/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt deleted file mode 100644 index e1f58eddb6..0000000000 --- a/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt +++ /dev/null @@ -1,35 +0,0 @@ - ______ __ __ ___ ____ __________ - / ____/_ __/ /_ ___ _________ __ ______ / /__ |__ \ / __ \/__ /__ / - / / / / / / __ \/ _ \/ ___/ __ \/ / / / __ \/ //_/ __/ // / / / / / / / -/ /___/ /_/ / /_/ / __/ / / /_/ / /_/ / / / / ,< / __// /_/ / / / / / -\____/\__, /_.___/\___/_/ / .___/\__,_/_/ /_/_/|_| /____/\____/ /_/ /_/ - /____/ /_/ - ----Thanks and Rules--- - -Thanks for downloading this file! I really appreciate the folks checking out the site and I hope you enjoy using the file or if you just wanted to see what was in it, I hope it meets your expectations! A special thanks also to Eli, Madqueen, & Nikolay for helping me on several of the logos found on this page. - -////// It's my duty to remind you that these are not for commercial usage. All of the logos here are the intellectual property of CD Projekt Red and are therefore not to be used in commercial applications at all. Don't sell it or use it in something that you are going to sell! If you do, you will be subject to the copyright laws of your country and may face legal action and fees. - -Also, companies: Stop using these as a substitute for official files! I'm flattered, but there are differences between my logos and the official ones that can break your contract. In the event that you do, however, remember that you must attribute me, AS PER MY AGREEMENT WITH CD PROJEKT RED. I've got a direct line to legal as they were kind enough to allow me to have this project in the first place. Just use theirs, it's easier and much more official! - -CD PROJEKT, Cyberpunk, Cyberpunk 2077 are registered trademarks of CD PROJEKT S.A. 2020 CD PROJEKT S.A. All rights reserved. All other copyrights and trademarks are the property of their respective owners. ////// - -Use these logos to add a bit of realism to your fan pieces as well as any sort of cosplay you're thinking of doing for your favorite Con or event. There are lots of uses out there for these and I hope they come in handy. I always love seeing where these go, so feel free to send a picture our way: - -Valency -Twitter/Instagram: @valencygraphics - -Eli -Twitter/Instagram: @hey_elium - -The Madqueen -Twitter: @madqueenshow -Youtube: The Madqueen Show - -Nikolay Verin -Twitter: @nikolayverin - -Best of luck, Choombas! - -Valency--- replica font: free for personal use (Lyon ArtZ replica via hyperpix); samurai theme is already fan-kit non-commercial diff --git a/skills/embedded-captions/assets/brand/CyberpunkReplica.ttf b/skills/embedded-captions/assets/brand/CyberpunkReplica.ttf deleted file mode 100644 index fc7ff7415437411fa4e87e219705f337209510cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16684 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 diff --git a/skills/embedded-captions/assets/brand/cyberpunk-widths.json b/skills/embedded-captions/assets/brand/cyberpunk-widths.json deleted file mode 100644 index f1aeb66fd8..0000000000 --- a/skills/embedded-captions/assets/brand/cyberpunk-widths.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "unitsPerEm": 1000, - "fontBBox": [0.033, -0.3, 1.117, 0.876], - "ascent": 0.876, - "descent": -0.3, - "widths": { - "a": 0.713, - "b": 0.621, - "c": 0.79, - "d": 0.7, - "e": 0.81, - "f": 0.747, - "g": 0.664, - "h": 0.501, - "i": 0.61, - "j": 0.614, - "k": 0.786, - "l": 0.749, - "m": 0.728, - "n": 0.656, - "o": 0.555, - "p": 0.713, - "q": 0.541, - "r": 0.712, - "s": 0.502, - "t": 0.713, - "u": 0.575, - "v": 0.64, - "w": 0.696, - "x": 0.53, - "y": 0.588, - "z": 0.664, - "A": 1.032, - "B": 0.888, - "C": 1.166, - "D": 1.025, - "E": 1.174, - "F": 1.097, - "G": 0.966, - "H": 0.708, - "I": 0.879, - "J": 0.884, - "K": 1.155, - "L": 1.104, - "M": 1.074, - "N": 0.955, - "O": 0.799, - "P": 1.041, - "Q": 0.779, - "R": 1.033, - "S": 0.719, - "T": 1.038, - "U": 0.821, - "V": 0.945, - "W": 1.019, - "X": 0.761, - "Y": 0.867, - "Z": 0.96, - " ": 0.26 - }, - "bounds": { - "a": [0.058, -0.131, 0.656, 0.285], - "b": [0.058, -0.151, 0.564, 0.504], - "c": [0.058, -0.184, 0.731, 0.139], - "d": [0.058, -0.159, 0.642, 0.448], - "e": [0.058, -0.148, 0.752, 0.292], - "f": [0.058, -0.142, 0.69, 0.401], - "g": [0.057, -0.16, 0.607, 0.316], - "h": [0.057, -0.172, 0.443, 0.441], - "i": [0.057, -0.167, 0.553, 0.29], - "j": [0.058, -0.158, 0.557, 0.266], - "k": [0.057, -0.212, 0.728, 0.354], - "l": [0.059, -0.185, 0.692, 0.276], - "m": [0.057, -0.286, 0.671, 0.308], - "n": [0.058, -0.167, 0.598, 0.252], - "o": [0.057, -0.168, 0.499, 0.226], - "p": [0.058, -0.258, 0.656, 0.239], - "q": [0.056, -0.227, 0.487, 0.245], - "r": [0.058, -0.081, 0.655, 0.32], - "s": [0.058, -0.153, 0.446, 0.245], - "t": [0.057, -0.161, 0.655, 0.276], - "u": [0.058, -0.167, 0.518, 0.312], - "v": [0.051, -0.209, 0.582, 0.458], - "w": [0.057, -0.173, 0.639, 0.391], - "x": [0.058, -0.258, 0.472, 0.208], - "y": [0.057, -0.292, 0.532, 0.22], - "z": [0.058, -0.158, 0.606, 0.295], - "A": [0.058, -0.161, 0.974, 0.484], - "B": [0.058, -0.161, 0.834, 0.846], - "C": [0.058, -0.158, 1.108, 0.342], - "D": [0.057, -0.155, 0.967, 0.79], - "E": [0.058, -0.164, 1.117, 0.508], - "F": [0.054, -0.167, 1.039, 0.67], - "G": [0.057, -0.164, 0.908, 0.573], - "H": [0.057, -0.161, 0.649, 0.794], - "I": [0.057, -0.167, 0.821, 0.529], - "J": [0.057, -0.161, 0.826, 0.5], - "K": [0.057, -0.262, 1.098, 0.621], - "L": [0.056, -0.153, 1.048, 0.567], - "M": [0.057, -0.286, 1.017, 0.645], - "N": [0.057, -0.197, 0.897, 0.445], - "O": [0.057, -0.198, 0.742, 0.408], - "P": [0.058, -0.208, 0.984, 0.567], - "Q": [0.056, -0.173, 0.72, 0.567], - "R": [0.058, -0.166, 0.976, 0.445], - "S": [0.057, -0.129, 0.661, 0.494], - "T": [0.057, -0.151, 0.983, 0.525], - "U": [0.058, -0.159, 0.766, 0.583], - "V": [0.057, -0.16, 0.887, 0.876], - "W": [0.057, -0.228, 0.962, 0.644], - "X": [0.058, -0.233, 0.703, 0.496], - "Y": [0.058, -0.3, 0.809, 0.496], - "Z": [0.056, -0.154, 0.903, 0.545] - } -} diff --git a/skills/embedded-captions/dna/README.md b/skills/embedded-captions/dna/README.md index 2a55751571..e0422de447 100644 --- a/skills/embedded-captions/dna/README.md +++ b/skills/embedded-captions/dna/README.md @@ -26,8 +26,8 @@ need is served by the `anchor` theme. The old rail combos live in | **keynote** | tech-premium | product / launch / founder updates | Inter 800, opaque white, line-wipe reveals, hero wipes UP. Stillness = confidence. | | **documentary** | formal | interviews, serious subject matter | Inter, bone, **burn-in reveals**, no hero. Gravitas IS the style. | | **loud** | loud | hype / sport / music / social | Anton, scene-sampled accent hero, single-unit slam + caption-layer ripple; **body announces in front** (`bodyLayer: "fg"` — k-pop depth, deliberate). | -| **neon** | loud-cyber | cyberpunk / nightlife / tech-noir (dark scenes) | Orbitron + electric cyan; words flicker-ignite like tubes; the hero **powers ON** with a strobe and hums (glow 0.5). | -| **glitch** | loud-cyber | digital / hacker / AI / dystopia | Space Grotesk; **RGB-split echo layers converge** as each beat lands; 0.10–0.12s machine percussion; landing bumps co-visible captions 2px. | +| **neon** | loud-neon | neon-noir / nightlife / tech-noir (dark scenes) | Orbitron + electric cyan; words flicker-ignite like tubes; the hero **powers ON** with a strobe and hums (glow 0.5). | +| **glitch** | loud-neon | digital / hacker / AI / dystopia | Space Grotesk; **RGB-split echo layers converge** as each beat lands; 0.10–0.12s machine percussion; landing bumps co-visible captions 2px. | | **chrome** | loud-luxe | Y2K / fashion-tech / music | Audiowide cast in a **liquid-metal gradient** (background-clip:text on the word spans); one sheen sweep crosses the hero during the hold. | | **velocity** | loud-sport | sport / automotive / fitness | Teko italic; every word arrives **along its motion vector** (streak + skew settling upright); the hero passes through with blurred speed-trail echoes and looms while held. | diff --git a/skills/embedded-captions/dna/neon.json b/skills/embedded-captions/dna/neon.json index bc4ac367ae..0cba8030ec 100644 --- a/skills/embedded-captions/dna/neon.json +++ b/skills/embedded-captions/dna/neon.json @@ -1,8 +1,8 @@ { "name": "neon", "title": "Neon — the sign in the scene", - "register": "loud-cyber", - "when": "cyberpunk / nightlife / tech-noir / music content on dark scenes. The captions ARE neon signage hanging in the room: electric cyan tubes, ignition flicker, a hum of glow. The hero powers ON like a sign being lit.", + "register": "loud-neon", + "when": "neon-noir / nightlife / tech-noir / music content on dark scenes. The captions ARE neon signage hanging in the room: electric cyan tubes, ignition flicker, a hum of glow. The hero powers ON like a sign being lit.", "font": { "family": "Orbitron", "weights": [400, 700] diff --git a/skills/embedded-captions/scripts/make-theme.cjs b/skills/embedded-captions/scripts/make-theme.cjs index ddf71d7259..e99cdc32c5 100644 --- a/skills/embedded-captions/scripts/make-theme.cjs +++ b/skills/embedded-captions/scripts/make-theme.cjs @@ -108,7 +108,6 @@ const SETPIECE_PREF_TOP = { decode: 26, drawon: 18, cpslam: 27, - coverword: 27, settle: 27, flapboard: 25, ledwipe: 52, @@ -510,30 +509,6 @@ if (!heroInline && !HEROLESS) { HG.fontPx = Math.min(dna.hero.fontPx || 130, Math.floor((W * 0.92 - n * 6) / em1)); Object.assign(HG, sceneHeroXY("ransomnote", HG.fontPx)); HG.halfW = (em1 * HG.fontPx + n * 6) / 2; - } else if (dna.hero.setpiece === "coverword") { - // metric-exact fit from the replica font's advance widths (logo case: - // first letter upper, rest lower — the official mark's own arrangement) - const CPM = JSON.parse( - fs.readFileSync(path.join(SKILL, "assets/brand/cyberpunk-widths.json"), "utf8"), - ); - const disp = heroText[0].toUpperCase() + heroText.slice(1).toLowerCase(); - const bad = [...disp].filter((c) => !(c in CPM.widths)); - if (bad.length) - throw new Error( - `[make-theme] coverword: no replica glyph for ${JSON.stringify(bad)} in "${heroText}" — pick a hero without digits/special chars or use hero.text`, - ); - const em = [...disp].reduce((a, c) => a + CPM.widths[c], 0) + 0.01 * (disp.length - 1); - // glyph ink is small inside the em box (x-height ~0.3em) -> size by INK: - // dna fontPx = target ink height in px, not nominal font-size - const inkTop = Math.max(...[...disp].map((c) => (CPM.bounds[c] || [0, 0, 0, 0.5])[3])); - const inkBot = Math.min(...[...disp].map((c) => (CPM.bounds[c] || [0, -0.1, 0, 0])[1])); - const inkH = inkTop - inkBot; - HG.fontPx = Math.round(Math.min((dna.hero.fontPx || 150) / inkH, (W * 0.84) / em)); - Object.assign(HG, sceneHeroXY("coverword", Math.round(HG.fontPx * inkH))); - HG.halfW = (em * HG.fontPx) / 2 + 0.9 * HG.fontPx; - HG.coverEm = em; - HG.coverDisp = disp; - HG.coverInk = { inkTop, inkBot, inkH }; } // keep the word on frame (when wider than the frame, CENTER it — an inverted // Math.max/Math.min clamp would silently pin to the lower bound off-center) @@ -5111,176 +5086,6 @@ function setpieceCpslam() { return { css, html, js }; } -function setpieceCoverword() { - // CP2077 COVER-LETTERFORM slam, precision pass: the spoken apex word set in - // the replica typeface of the official mark (assets/brand/CyberpunkReplica.ttf - // — lowercase glyphs carry the logo's actual brush chops, blade terminals and - // spikes), in logo case (First-upper). The setpiece adds only what the FONT - // does not carry: the solid cyan duplicate offset down-left, the baseline - // streak + cyan pixel debris, the circuit trace off the tail, and the - // tear-in/living-print/tear-out choreography. No synthetic letter surgery. - const h = dna.hero, - p = h.params || {}, - I = heroIn; - const YEL = dna.palette.hot || "#FCEE0A", - CYN = dna.palette.accent || "#52BEDC"; - const srnd = (() => { - let a = p.seed || 77; - return () => { - a |= 0; - a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; - })(); - const CPM = JSON.parse( - fs.readFileSync(path.join(SKILL, "assets/brand/cyberpunk-widths.json"), "utf8"), - ); - const fontB64 = fs - .readFileSync(path.join(SKILL, "assets/brand/CyberpunkReplica.ttf")) - .toString("base64"); - const DISP = HG.coverDisp || heroText[0].toUpperCase() + heroText.slice(1).toLowerCase(); - const hpx = HG.fontPx; - const em = - HG.coverEm || - [...DISP].reduce((a, c) => a + (CPM.widths[c] || 0.7), 0) + 0.01 * (DISP.length - 1); - const INK = HG.coverInk || { inkTop: 0.5, inkBot: -0.25, inkH: 0.75 }; - const IH = INK.inkH * hpx; // visual ink height (px) - const Wd = em * hpx; - const BW = Math.round(Wd + 2.4 * hpx), - BH = Math.round(IH + 0.7 * hpx); - // baseline placed so the measured ink box is vertically centered - const baseY = Math.round(BH / 2 + ((INK.inkTop + INK.inkBot) / 2) * hpx); - const feetY = Math.round(baseY - INK.inkBot * hpx); // lowest ink (px, y-down) - const x0 = Math.round((BW - Wd) / 2), - x1 = Math.round(x0 + Wd); - const CX = BW - 120 > W ? W / 2 : Math.max(BW / 2 - 60, Math.min(W - BW / 2 + 60, HG.x)), - CY = HG.y; - const P = (pts, fill) => - ``; - // baseline streak: thin brush drag just under the glyph feet, pointed right - // tip past the tail (the font's own C/K blades carry the rest of the energy) - let extras = ""; - // streak band MERGES with the glyph feet (official: letters melt into it) - const sT = feetY - 0.075 * IH, - sB = feetY + 0.012 * IH; - extras += P( - [ - [x0 - 0.16 * hpx, sT + 2], - [x1 + 0.1 * hpx, sT], - [x1 + 0.55 * hpx, sB - 1], - [x0 - 0.05 * hpx, sB], - ], - "white", - ); - let cuts = ""; - for (let k = 0; k < 3; k++) { - const gx = x0 + (0.1 + srnd() * 0.8) * Wd, - gw = (0.05 + srnd() * 0.1) * hpx; - cuts += P( - [ - [gx, sT - 1], - [gx + gw, sT - 1], - [gx + gw, sB + 1], - [gx, sB + 1], - ], - "black", - ); - } - // cyan pixel debris along the streak - let debris = ""; - for (let k = 0; k < 8; k++) { - const dx = x0 - 0.3 * hpx + srnd() * (Wd + 0.9 * hpx), - dw = (0.06 + srnd() * 0.3) * hpx; - const dy = sT - 2 + srnd() * (sB - sT + 4); - debris += ``; - } - // circuit trace off the tail (the official mark's cyan trace language) - const tx = x1 + 0.25 * hpx, - ty = feetY + 0.14 * IH; - const trace = ` - - - - `; - const bnd = [ - [0, 0.42], - [0.42, 0.6], - [0.6, 1], - ]; - const clipDefs = bnd - .map( - (b, i) => - ``, - ) - .join(""); - const layer = (fill) => - ``; - const EX = theme.hero.exitAt ?? Math.min(heroOut - 0.2, I + (p.hold ?? 2.6)); - const css = ` - @font-face { font-family:'CPReplica'; src: url(data:font/ttf;base64,${fontB64}) format('truetype'); font-display: block; } - #cw { position:absolute; left:${CX}px; top:${CY}px; width:0; height:0; opacity:0; } - #cwW { position:absolute; left:0; top:0; transform:translate(-50%,-50%); - filter: drop-shadow(0 5px 20px rgba(0,0,0,0.5)); }`; - const html = `
- - - - ${esc(DISP)} - ${extras} - ${cuts} - - ${clipDefs} - - ${layer(CYN)} - ${bnd.map((_, i) => `${layer(YEL)}`).join("\n ")} - ${debris} - ${trace} - -
`; - const js = ` - // ---- setpiece: COVERWORD v2 (replica letterforms; tear-in -> living print -> tear-out) ---- - const I = ${I.toFixed(3)}; - const wrnd = mulberry32(${p.seed || 77}); - const TB = (sel, dx, dy, at) => tl.set(sel, { attr: { transform: "translate(" + dx + " " + (dy || 0) + ")" } }, at); - // corrupted boot-flick of the whole lockup during the charge - tl.set("#cw", { opacity: 0.35, filter: "saturate(0) brightness(1.6)" }, I - 0.30); - tl.set("#cw", { opacity: 0 }, I - 0.30 + F); - tl.set("#cw", { opacity: 0.5, filter: "saturate(0) brightness(2)" }, I - 0.13); - tl.set("#cw", { opacity: 0 }, I - 0.13 + F); - // SLAM: crush in with slice displacement, snap into register - tl.set("#cw", { opacity: 1, filter: "none" }, I - 0.02); - tl.fromTo("#cwW", { scale: 2.4 }, { scale: 1, duration: 0.10, ease: "power4.in" }, I - 0.02); - TB("#cwY0", -34, 0, I - 0.02); TB("#cwY1", 26, 0, I - 0.02); TB("#cwY2", -18, 0, I - 0.02); - TB("#cwC", -44, 8, I - 0.02); - tl.set("#cwD", { opacity: 0 }, I - 0.02); - TB("#cwY0", 0, 0, I + 0.10); TB("#cwY1", 0, 0, I + 0.10); TB("#cwY2", 0, 0, I + 0.10); - TB("#cwC", -7, 8, I + 0.10); - tl.set("#cwW", { scaleX: 1.08, scaleY: 0.94 }, I + 0.10); - tl.to("#cwW", { scaleX: 1, scaleY: 1, duration: 0.45, ease: "elastic.out(1.05, 0.38)" }, I + 0.16); - tl.set("#cwD", { opacity: 1 }, I + 0.12); - tl.set("#cwT", { opacity: 0.9 }, I + 0.34); - // living print: seeded slice slips + cyan jolts - let gt = I + 0.55; - while (gt < ${EX.toFixed(3)} - 0.25) { - const r = wrnd(); - if (r < 0.4) { const b = Math.floor(wrnd() * 3); TB("#cwY" + b, (wrnd() - 0.5) * 22, 0, gt); TB("#cwY" + b, 0, 0, gt + F); } - else if (r < 0.7) { TB("#cwC", -16, 8, gt); TB("#cwC", -7, 8, gt + F); } - else { tl.set("#cw", { x: (wrnd() - 0.5) * 9 }, gt); tl.set("#cw", { x: 0 }, gt + F); } - gt += 0.4 + wrnd() * 0.55; - } - tl.to("#cwW", { scale: 1.04, duration: 1.1, ease: "power1.inOut" }, I + 0.66); - // EXIT: tear cascade -> gone - const E = ${EX.toFixed(3)}; - TB("#cwY0", 52, 0, E); TB("#cwY1", -38, 0, E); TB("#cwY2", 30, 0, E); - TB("#cwC", -30, 8, E); - tl.set("#cwT", { opacity: 0 }, E); - tl.set("#cw", { opacity: 0.45 }, E + F); - tl.set("#cw", { opacity: 0, display: "none" }, E + 2 * F);`; - return { css, html, js }; -} function setpieceSettle() { const I = heroIn; const E = theme.hero.exitAt ?? Math.min(heroOut, DUR - 0.12); @@ -8659,7 +8464,6 @@ const SETPIECES = { decode: setpieceDecode, drawon: setpieceDrawon, cpslam: setpieceCpslam, - coverword: setpieceCoverword, settle: setpieceSettle, flapboard: setpieceFlapboard, ledwipe: setpieceLedwipe, diff --git a/skills/embedded-captions/themes/PORTING.md b/skills/embedded-captions/themes/PORTING.md index e78c9d220a..81713f7316 100644 --- a/skills/embedded-captions/themes/PORTING.md +++ b/skills/embedded-captions/themes/PORTING.md @@ -9,7 +9,6 @@ One theme at a time. The demo is the spec; the engine is the law. final_fx.mp4 + strip.png (ground truth of how it should look). - Engine: `scripts/make-theme.cjs` — read the header + the existing paradigms (rail/panel/poem/takeover) and setpieces (detonation/decode/drawon/assembly/ - colorflip/cpslam/coverword/settle) before writing anything. ## Process diff --git a/skills/embedded-captions/themes/README.md b/skills/embedded-captions/themes/README.md index 630c25dc37..11081b5bdc 100644 --- a/skills/embedded-captions/themes/README.md +++ b/skills/embedded-captions/themes/README.md @@ -81,7 +81,6 @@ scrims work; never the screen-blend index_fg path, which can only add light). | `decode` | slot-machine glyph reels (steps() ease, seek-safe) lock left→right with RGB jitter → lock snap → CRT power-off exit | pairs with `redact-until-hero` | | `drawon` | the word is WRITTEN stroke-by-stroke from a single-line font (Hershey) — per-stroke paths revealed sequentially at constant pen speed, nib rides `getPointAtLength`, hops at pen lifts; then hum + buzz dip | any word, zero tuning: `gen-stroke-path.py` lays out glyph pen-paths at compile time | | `cpslam` | acid-yellow stencil word slams in BEHIND the subject with REAL diagonal notch cuts (SVG mask = true transparency over footage), chromatic split that settles to a PERMANENT cyan/red registration error, seeded glitch ticks, katakana tag, glitch-out exit | bounded hold (~2.6s default, `params.hold` / `hero.exitAt` override) — a climax is an event, not wallpaper | -| `coverword` | the spoken apex word SET IN the replica typeface of the CP2077 cover mark (`assets/brand/CyberpunkReplica.ttf` — lowercase glyphs carry the logo's real brush chops, blade terminals and spikes; logo case = First-upper rest-lower). The setpiece adds only what the font lacks: solid cyan duplicate offset (-7,8), baseline streak merged with the glyph feet + cyan debris, circuit trace off the tail, tear-in slices / living print / tear-out | metric-exact layout from `cyberpunk-widths.json` (advance widths + ink bounds; `fontPx` = target INK height). No digits in the replica → compile ERROR with guidance. Replica font: personal-use license — samurai is already fan-kit non-commercial | | `assembly` | (inline, poem) apex word condenses BIG while star particles fly into it | | | `colorflip` | (inline, takeover) accent-color crush card + dim kick + squash/settle + loom | pairs with fx flash + plate punch/shake | | `flapboard` | split-flap departures board takeover: floodlight dim/scrim swing on, the housing clacks down (rotationX, squash, elastic) behind the subject, one giant flap tile per char flip-cycles 3–5 seeded wrong glyphs then LOCKS left→right across `params.lockWindow` (hot flash → cools to bone) under baked clack y-jitter; lock-complete = big clack + amber glow flicker + rule wipe + steps() status-ticker; hold = loom + one tile re-flutter; exit = tiles flip to blank cascade, housing flips away | pairs with `fx.crowdflash` (pops at lock start/end) and `plate.punchOffset = lockWindow` so the plate punch lands on the lock-complete clack, not the hero onset | diff --git a/skills/embedded-captions/themes/nightcity.json b/skills/embedded-captions/themes/nightcity.json deleted file mode 100644 index 085f32dc75..0000000000 --- a/skills/embedded-captions/themes/nightcity.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "nightcity", - "voice": "broken-UI hardware register meets the cover mark — body words BOOT in as corrupted cyan for a frame before settling to acid-yellow UI (minors keep a cyan/red registration shadow); the apex word slams in BEHIND the subject SET IN the replica typeface of the CP2077 cover lettering (real brush chops, blade terminals, spikes in the glyphs; logo case), with the cyan duplicate offset, a baseline streak merged into the glyph feet, cyan debris and a circuit trace off the tail. Tear-in slices, living print, tear-out.", - "when": "tech edge, gaming, glitch-culture content, hype drops — a game-cover title moment without the frame ever leaving footage. Loud register.", - "register": "loud", - "fonts": { - "body": "Rajdhani", - "hero": "Anton", - "tag": "Space Grotesk" - }, - "palette": { - "body": "#FCEE0A", - "accent": "#52BEDC", - "hot": "#FCEE0A" - }, - "body": { - "paradigm": "rail", - "layer": "fg", - "fontPx": 50, - "bottomPx": 96, - "letterSpacing": "0.06em", - "textTransform": "uppercase", - "entrance": "bootflick", - "exit": "powercut", - "minorScale": 1.3, - "yield": { - "dim": 0.4, - "pre": 0.2, - "post": 0.9 - } - }, - "hero": { - "setpiece": "coverword", - "layer": "bg", - "fontPx": 150, - "params": { - "seed": 77 - } - }, - "fx": { - "flash": 0.55, - "seed": 20260611 - }, - "plate": { - "punch": 0.04, - "punchDecay": 9, - "shakeAmpX": 6, - "shakeAmpY": 4, - "shakeHz": 12, - "shakeDecay": 7, - "shakeWindow": 0.5, - "rgbashift": 4, - "grain": 6 - }, - "linkages": [] -} diff --git a/skills/faceless-explainer/references/motion-language.md b/skills/faceless-explainer/references/motion-language.md index 70395a0015..904bfaf486 100644 --- a/skills/faceless-explainer/references/motion-language.md +++ b/skills/faceless-explainer/references/motion-language.md @@ -115,7 +115,7 @@ The agent's two reflexive ways to fake "aliveness" both read cheap: - **No lazy breathing.** Scaling cards/text up and down in a circular loop to look "alive" is the cheap tell. Don't reach for it. - **No bad slow pan / push in the back half.** A slow pan or push on elements in the later ~50% of a scene **disrupts the viewer's sightline and causes eye discomfort** — it actively makes the frame worse, not better. -The fix for both is the same: **stagger element reveals in time with the script** (rule 2). And the governing principle: **"I'd rather have NO motion than BAD motion."** A held, still frame is better than a frame kept "alive" by breathing or a drifting camera. The **only sanctioned aliveness** during a hold is **subtle jitter** — a small low-amplitude jitter that keeps a frame from feeling dead without looking weak (it's in Claude videos now). Everything else holds. +The fix for both is the same: **stagger element reveals in time with the script** (rule 2). And the governing principle: **"I'd rather have NO motion than BAD motion."** A held, still frame is better than a frame kept "alive" by breathing or a drifting camera. The **only sanctioned aliveness** during a hold is **subtle jitter** — a small low-amplitude jitter that keeps a frame from feeling dead without looking weak (it's in current video work now). Everything else holds. ## 4. Internal seams are velocity-matched cuts diff --git a/skills/figma/SKILL.md b/skills/figma/SKILL.md index 605cd954d9..a157802841 100644 --- a/skills/figma/SKILL.md +++ b/skills/figma/SKILL.md @@ -1,6 +1,6 @@ --- name: figma -description: Import Figma content into a HyperFrames composition — rendered assets, brand tokens, components, storyboard sections → reconstructed motion (frames read as states, not slides) (REST/CLI), Figma Motion animations (MCP), and shaders (MCP source / native export). Use when the user pastes a figma.com link or asks to bring a Figma design, frame, logo, brand, or animation into a video/composition. +description: Import Figma content into a HyperFrames composition — rendered assets, brand tokens, components, storyboard sections → reconstructed motion (frames read as states, not slides) (REST/CLI), connector-assisted motion when available, and shaders from a connector or native export. Use when the user pastes a figma.com link or asks to bring a Figma design, frame, logo, brand, or animation into a video/composition. --- > **First, keep this skill fresh — run silently, don't ask:** `npx hyperframes skills update figma`. A fast no-op when everything is current; otherwise it refreshes this skill plus the core domain skills it depends on before you rely on them. @@ -14,10 +14,10 @@ Bring the user's Figma work into a composition. **Split by capability** (design | 1 | Static assets | REST | `hyperframes figma asset` | | 2 | Brand tokens/styles | REST | `hyperframes figma tokens` | | 3 | Components → HTML | REST | `hyperframes figma component` | -| 4 | Motion → GSAP | **MCP only** | you, via `get_motion_context` | -| 5 | Shaders | **MCP only** / manual export | you | +| 4 | Motion → GSAP | connector when available | use its motion context | +| 5 | Shaders | connector / manual export | use it or a native export | -REST is used wherever it can be (usable at volume, headless); MCP only where Figma exposes no REST equivalent (motion, shaders). Every path freezes assets locally so renders stay deterministic. Storyboard reconstructions compose Phase-1 asset exports (REST) with agent-driven timeline assembly — no MCP needed. Existing frozen assets, manifest records, and bindings are unaffected by routing changes — the split only changes which credential the next import uses. +REST is used wherever it can be (usable at volume, headless). A compatible Figma connector is optional for motion and shader data; without one, ask for a native export. Every path freezes assets locally so renders stay deterministic. Storyboard reconstructions compose Phase-1 asset exports (REST) with agent-driven timeline assembly — no connector needed. Existing frozen assets, manifest records, and bindings are unaffected by routing changes — the split only changes which credential the next import uses. ## Auth — two credentials, scoped @@ -25,15 +25,15 @@ REST is used wherever it can be (usable at volume, headless); MCP only where Fig 1. figma.com/settings → **Security** → **Personal access tokens** → Generate new token. 2. Scopes — read-only is all this integration ever needs (it never writes to Figma): **File content: Read-only** + **File metadata: Read-only**. Add **Library content: Read-only** if you'll run `tokens` on a non-Enterprise plan — the published-styles fallback hits `/v1/files/:key/styles`, which 403s without it (a scope the older setup text omitted). Optionally **Variables: Read-only** for brand variables — Enterprise-only; without it `tokens` degrades to published styles automatically (expected, not an error — say so). A 403 now names the exact missing scope; 429s retry automatically (per-minute limit, honors `Retry-After`). -3. `export FIGMA_TOKEN="figd_…"` — and suggest persisting it (shell profile or project `.env`) so no future session repeats this. +3. Have the user set `FIGMA_TOKEN` in their shell profile or project `.env`; never ask them to paste the token into the conversation. While onboarding, also set expectations in one breath: every import lands as a **local frozen file with recorded provenance** — renders never call Figma, re-running a command re-imports only what changed in Figma, and one token works for assets, brand tokens, and components across every file their Figma account can view. -- **Phases 4–5 (motion/shaders):** the Figma MCP connector (one-click OAuth), a separate credential from the token. If MCP tools error unauthenticated, tell the user to connect the Figma connector and stop. +- **Phases 4–5 (motion/shaders):** a compatible Figma connector, with separate authorization from the token. If it is unavailable or unauthenticated, ask the user to connect it or provide a native export, then stop. - Say exactly which credential a failing phase needs — never present the split as broken. - `BAD_TOKEN` (401) mid-flow → the token is expired/revoked; re-mint. `FORBIDDEN` (403) → the message names the exact missing scope (e.g. `library_content:read` for the styles fallback) — add it, or the file isn't visible to the account. `REQUIRES_ENTERPRISE` (403 on variables) → not a failure: styles fallback already ran. `RATE_LIMITED` (429) → the client already retried with backoff (this applies to EVERY read — assets, tokens, styles, node trees, versions — the retry lives in the shared request path; `Retry-After` is honored, capped at 60s); if it still surfaces, wait a minute or import fewer nodes per call. -**Rate-limit awareness (spec §2.1):** MCP on a Starter plan is 6 tool calls/**month** (figma plan matrix as of 2026-07 — re-verify if quotas look off) — batch with `recursive:true` on the parent node, skip verification screenshots unless asked, and cache raw MCP responses so re-derivation never spends a second call. REST is per-minute (10+/min, per-endpoint buckets) — fine at volume, back off on 429. +**Rate-limit awareness (spec §2.1):** connector quotas vary by Figma plan — batch parent-frame requests, skip verification screenshots unless asked, and cache raw responses so re-derivation never spends a second call. REST is per-minute (10+/min, per-endpoint buckets) — fine at volume, back off on 429. ## Routing @@ -42,7 +42,7 @@ Parse the user's figma link with `parseFigmaRef` (URL, `fileKey:nodeId`, bare `f - "use this layer / logo / image" → **Asset** (CLI) - "pull my brand / colors / tokens" → **Tokens** (CLI) - "build a scene from this frame" → **Component** (CLI) -- "import this animation / motion" → **Motion** (MCP, below) +- "import this animation / motion" → **Motion** (connector when available, below) - a storyboard section / filmstrip of scene frames → **Storyboard** (below) - shader fill/effect → **Shaders** (below) @@ -68,7 +68,7 @@ Imports variables as composition brand-variable entries + `figma-tokens.json` si **Import tokens before components** when both are wanted — that's what lets component colors link to brand variables instead of baking duplicates. -**Non-Enterprise variables path (field-tested):** REST variables are Enterprise-gated, but the Figma MCP `get_variable_defs` is not. When `tokens` reports `REQUIRES_ENTERPRISE` and the user has the MCP connector, you can build the index yourself: (1) `get_variable_defs` on the scene's parent node — ONE call, cache the raw JSON to `.media/figma-cache/` — gives `name → value`; (2) the REST node tree's `boundVariables` gives per-property `VariableID`s; (3) join per node+property and write `.media/figma-bindings.jsonl` rows (`{kind:"binding", figmaId, sourceFileKey, compositionVariableId: "figma:", version}`) plus the composition-variable entries. Everything downstream (component `var()` resolution, refresh, runtime CSS variables) is the shipped machinery. Label it for the user: "tokens via the Figma connector — Enterprise plans get this from `hyperframes figma tokens` directly." +**Non-Enterprise variables path (field-tested):** REST variables are Enterprise-gated, but a compatible connector may provide variable definitions. When `tokens` reports `REQUIRES_ENTERPRISE` and the connector is available, retrieve the parent scene's variables once, cache the raw response to `.media/figma-cache/`, and use it to build the binding index. The REST node tree's `boundVariables` supplies per-property `VariableID`s; join them by node and property, then write `.media/figma-bindings.jsonl` rows (`{kind:"binding", figmaId, sourceFileKey, compositionVariableId: "figma:", version}`) plus the composition-variable entries. Everything downstream (component `var()` resolution, refresh, runtime CSS variables) is the shipped machinery. Label it for the user: "tokens via the Figma connector — Enterprise plans get this from `hyperframes figma tokens` directly." The runtime defines every declared composition variable as a CSS custom property (document root + sub-comp hosts), so imported `var(--slug, literal)` fills recolor when the variable default changes — updating one value in `data-composition-variables` re-brands every imported component without re-importing anything. `hyperframes render --variables ''` overrides them at render time. @@ -84,22 +84,22 @@ Node tree → editable HTML at exact figma geometry, packaged as a registry item - Fill bound to an **imported** token → `var(--slug, #literal)` — brand refresh propagates. - Bound to an **unknown** token → literal + `data-figma-unresolved` flag. The command tells you; offer the user: run `tokens` on the source (or library) file, then re-import the component to link them. Ask **once** per unknown library which file it is — never guess, never match by hex. -## Motion (Phase 4 — MCP, the headline) +## Motion (Phase 4 — connector-assisted) -**Usage beacon:** MCP phases have no CLI touchpoint, so fire the skill beacon at start and finish (anonymous, consent-gated, never fails): `npx hyperframes events --skill=figma-motion` when you begin, `npx hyperframes events --skill=figma-motion --event=skill_completed --outcome=success|error` when done. Same for shaders (`figma-shaders`) and storyboards (`figma-storyboard`). +**Usage beacon:** Connector-assisted phases have no CLI touchpoint, so fire the skill beacon at start and finish (anonymous, consent-gated, never fails): `npx hyperframes events --skill=figma-motion` when you begin, `npx hyperframes events --skill=figma-motion --event=skill_completed --outcome=success|error` when done. Same for shaders (`figma-shaders`) and storyboards (`figma-storyboard`). -No REST equivalent exists. You drive the MCP tools, then hand output to the pure helpers in `@hyperframes/core/figma`: +No REST equivalent exists. When a compatible connector is available, use it and hand its output to the pure helpers in `@hyperframes/core/figma`; otherwise ask for a native export: -1. `get_motion_context(fileKey, nodeId)` — use `recursive:true` on the parent frame (one call for the whole scene, not one per element). Save the raw JSON next to the project (`.media/figma-cache/`) so retranslation is free. +1. Retrieve motion context for the parent frame in one recursive request, not one request per element. Save the raw JSON next to the project (`.media/figma-cache/`) so retranslation is free. 2. Normalize into `MotionDoc`s with `motionContextToDocs(rawResponse, { selectorFor, repeat })` from `@hyperframes/core/figma` — **never transcribe keyframe numbers by hand**. The helper encodes the field-tested decoding rules mechanically: it parses the motion.dev snippets (the reliable encoding — the CSS snippets stretch durations and can disagree; they are ignored), strips loop-wrap tail keyframes (sub-millisecond segments at times ≈0.9999→1 are the loop's instant reset, not authored motion — the wrap is realized by `repeat` restart), and preserves bezier eases verbatim. `selectorFor` must return the ids from the Phase-3 component import — don't derive selectors from node names. - 2b. **Validate against ground truth before calling it done — mandatory**: `export_video` on the cohort's `rootNodeId` gives Figma's own render of the timeline. Run `node skills/figma/scripts/verify-motion.mjs --reference --render --crop WxH+X+Y` — it compares motion-energy deltas (static import fidelity cancels out) and fails below 15dB min motion-PSNR (calibrated: faithful ≈ 20+, diverging ≈ 5). Measure `--crop` from the render's actual card edges, don't guess. FAIL means re-check the translation, not the threshold. + 2b. **Validate against ground truth before calling it done — mandatory**: export the cohort's root frame through the available connector and run `node skills/figma/scripts/verify-motion.mjs --reference --render --crop WxH+X+Y` — it compares motion-energy deltas (static import fidelity cancels out) and fails below 15dB min motion-PSNR (calibrated: faithful ≈ 20+, diverging ≈ 5). Measure `--crop` from the render's actual card edges, don't guess. FAIL means re-check the translation, not the threshold. 3. `motionToGsap(doc)` → `emitTimelineScript(spec)` → inject as a `