* feat(audio): composition master bus (data-fx-chain on the root)
A clip can belong to only one audio group, so there was no way to put one
effect, such as a limiter, across the whole mix. The data-fx-chain on the
top-level composition root now runs once over the sum of every clip and every
group, after each group's own chain and fader. No new attribute: a sub-composition
root's data-fx-chain was never applied to anything and still is not.
Render: when a master chain exists, mixAudioTracks writes a float WAV instead
of AAC, applyAudioFxChain runs the chain over it, and ffmpeg encodes AAC last,
so the chain reads unclipped samples. An unreadable chain is a mix failure, as
for a group. Preview: the transport splices the chain between the clips' sum and
the monitor, re-anchored per play generation and watched for edits.
Only the top-level root is read. No master automation: the root's data-automation
is not read as the bus's. No lint rule: data-fx-chain is valid on clips, groups
and roots, and the one place it does nothing (a sub-composition root) has always
been silent.
— Jerrai
* fix(core): preview routes every audible video through the master bus
A root data-fx-chain is attached only inside the Web Audio graph, but a plainly
audible <video> joined that graph only for its own chain, group, automation or a
volume over 1. So preview limited the music and left the talking head dry, and a
composition whose only audio is video got no master chain at all, while export
sums every audible element into the master. A master chain on the top-level
composition root is now a reason to join, the same as a video's own chain.
Elements under a root with no chain keep native output. Sub-composition roots
are still ignored.
— Jerrai
* fix(audio): master bus review round 2 (temp dir, tests, docs, Studio meter)
- audioMixer: a fatal FX error in the master pass or a group bus left the
work directory (the full float sum, about 1.4 GB an hour) on disk. The
final mix now runs in try/finally and the group rethrow removes it first.
- Tests: the "once over the sum" case now uses a limiter then a gain, so a
per-clip chain (-10.4) and a doubled one (-18) both fail it; a new case
proves the sum stays float until the chain reads it; the fader case uses
a group volume of 0.5 so the fader's position is exercised; the work-dir
removal is covered for both paths.
- Docs: "Three rules exist" (the master-bus lint rule is gone), a bad master
chain stops the render with audio_processing_failed, a sub-composition
file opened on its own has its root chain as the master bus, and a seek
clears a group or master limiter.
- Studio: TimelineElement.masterChain carries the root's data-fx-chain, so
the program meter shows for a video-only composition whose only audio
routing is the master chain, as preview now plays it.
- Comment blocks over four lines cut; webAudioTransport.setRate split so
its CRAP score is under the Fallow threshold.
— Jerrai
* fix(audio): master chain reaches tracks the render adds; honest plain-limiter copy
- Master chain: it was carried on each parsed AudioElement, so a track the
producer adds itself (appendAutoDetectedVideoAudio, the browser-discovered
audio in probeStage) never had it, and a composition whose only audio was
such a track exported with no master chain, no ceiling and no warning. It is
now a composition property: parseMasterFxChain(html) at compile, carried on
CompiledComposition and CompositionMetadata, passed to processCompositionAudio
by runAudioStage (in-process and distributed plan). AudioElement.masterFxChain
is removed so there is one source. Each forward (compile stage, local
orchestrator, distributed plan) has a test that fails when it is dropped.
- Plain limiter: it follows the level with no lookahead and Level after is
applied past the ceiling, so it cannot promise "nothing gets past" or "never
clips". Studio copy (description, panel text, knob label, summary) and the
hyperframes-audio skill now say a fast or inter-sample peak can pass it and
point to the true-peak limiter for a delivery limit. A copy test guards it.
— Jerrai
* fix(producer): keep master bus comments within the ratchet; run the plan test with the ffmpeg tests
Drop three one-line field comments that raised their files' comment share,
and list plan.masterBus.test.ts with the integration tests because plan()
reads the ffmpeg version.
— Jerrai
* chore(ci): repin the producer test-classification source in the reachability map
The reachability map pins the hash of test-classification.mjs, which now
lists one more integration test. The map's producer lane command already
covers every test under src, in both the unit and integration matrix lanes.
— Jerrai
* fix(studio): text in Studio reads without em or en dashes
* fix(studio): the dash check also catches HTML entities, and peak reads Unavailable
* test(studio): the dash check test covers every entity form
* fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too
* fix(producer): the gate check stops early on the engine's new fallback wording
* fix(engine): copy border-width/style/color onto the replacement video frame
injectVideoFramesBatch substitutes each <video> with a sibling <img> holding
an extracted still frame, copying an allow-listed set of CSS properties from
the video's computed style onto the img. border-width/border-style/
border-color were absent from that allow-list, so any border authored
directly on a <video> never reached the replacement image and disappeared
from render/snapshot output. border-radius and clip-path were already on the
list and already clip a replaced element's content correctly without
needing overflow:hidden, confirmed by a real-Chromium test that exercises
the actual capture path and reads real screenshot pixels.
Also corrected a stale comment in frameCapture.ts claiming
detectCssEffectRisk can return "clip-path" as a risk value -- it can't; that
string only ever comes from a separate, animation-only at-risk-props gate
further down the same file.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* test(producer): regenerate style-9-prod golden for the border fix
style-9-prod's video has an authored 8px border + 16px border-radius
(design_review.md calls out the "floating" look) that never rendered before
this fix, so its committed golden baked in the pre-fix (borderless) output.
Regenerated via `tsx src/regression-harness.ts style-9-prod --update`;
verified the diff is exactly the border/radius becoming visible (extracted
frames before/after) and the fixture passes its own visual/audio checks
again (100/100 checkpoints, correlation 1.000).
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(engine): measure the video's box before styling its replacement frame
injectVideoFramesBatch measured the video's used box (offsetLeft/Top/
Width/Height) after creating, inserting, and styling the replacement
<img> sibling -- once that copy loop started carrying border-width
(added for the border-visibility fix), the freshly-inserted, still
in-flow, now-bordered <img> competed for space in a flex row before
the video's own box was ever read. In a flex-centered layout (video
width:100%, sharing a row with the img sibling), that shrank the
measured video box by the exact width of the border, so the img ended
up sized from the video's post-shrink box instead of its authored one.
Moved the measurement to before the <img> is created/inserted/styled
at all, and set img.style.boxSizing = "border-box" explicitly after
the style-copy loop, since offsetWidth/offsetHeight (and the
getBoundingClientRect fallback) are always a border-box measurement
regardless of what box-sizing value that loop copies from the video's
own computed style.
Added a flex-centered regression test asserting the replacement img's
box stays identical to the video's box; confirmed it fails with the
old measurement order (a 16px border shrinks a 500px box to 484px) and
passes with the fix. Regenerated the one fixture (style-9-prod) whose
committed golden had baked in the shrunk-frame geometry.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(engine): consolidate video-box measurement into one object
Follow-up to c88890357: group the four measurement locals into a
single videoBox object instead of four loose consts, and trim the
surrounding comments down to their load-bearing content. No behavior
change -- same measurement order, same fallback conditions, same
applied style values; re-verified red (reverting the measurement order
still reproduces the 500px -> 484px shrink) and green.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(engine): trim the border-fix comments to the invariant
* test(producer): keep main's style-9-prod compiled.html; timings are unchanged
* test(producer): repin the classification source hash for test reachability
* fix(core): a color-graded bordered source keeps its canvas inside its border box
* fix(core): a graded source with an empty content box still shows its border
* fix(core): a border-only graded canvas stays hidden until the source has drawn
* fix(core): border-only graded canvas shows only while grading hides the source
* test(core): pin the border-only canvas hide after a context loss
* fix(core): grading canvas carries no border, so a graded source never paints it twice
* fix(core): graded injected frame drops its border once grading draws over it
* fix(core): a released grading entry gives the injected frame its border back
* fix(core): a released injected frame takes the video's current border style
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* perf(packages): ship npm packages without source maps and guard their size in CI
No package or consumer reads the published source maps, so they are no
longer built. The CLI copies only the studio app it serves instead of the
whole studio dist. verify:packed-manifests now holds every published package
to a size budget that ratchets down, and CI rejects pull requests that add a
large binary outside LFS.
* fix(ci): large-binary check reads every filename and fails on git errors
* ci: run only Build when a skill bundled into the CLI changes
* fix(ci): size guards also catch type changes, odd file names, unreadable blobs and bad budgets
* fix(ci): large-binary check reads blobs by id and skips submodules
* fix(ci): exempt only exact registry copies under docs/public/catalog
* test(ci): cover the staged catalog-copy exemption; drop the local-only override hint in CI
* fix(ci): producer budget follows its slimmed package; size checks name their real cause
* fix(skills): skill scripts run when the skills dir is a symlink
* test(skills): plugin launcher fixture ships the shared main-module helper like an install
* fix(skills): the CLI's cube validator runs through a symlink too, and the guard check is stricter
* docs(skills): long agent-started renders run detached in their own process group
* docs(cli): detached renders start in their own process group, on macOS too
* docs(cli): the detached render recipe never waits on terminal input
* fix(ci): keep the catalog publish PR when main changes nothing it generates
The bot compared the whole branch tree with main, so every main push rewrote
the standing PR and dismissed its approval. It now compares only generated
catalog files, with search vectors judged by the drift check's cosine rule.
* fix(ci): republish when main edits generated files or the branch carries others
* docs(registry): motion-blur copies explain their inline styles in three lines
* refactor(ci): split the kept-snapshot check into named conditions
* fix(catalog): compare search vectors by direction so the publish PR can pass
* test(catalog): pin the vector tolerance between machine noise and different items
* refactor(catalog): split the per-row cosine out of the vector comparison
* fix(media-use): use a host app's music tools; fail loudly without the heygen CLI
* fix(media-use): name the heygen CLI only for music and sound effects; keep the sfx test offline
* fix(skills): workflow skills use a host app's own music tools first
* fix(skills): say how a host app's music track reaches the narrated workflows
* fix(skills): set a host app's music track after the last audio pass
* fix(skills): a host-music film with no narration still fetches its sound effects
* fix(media-use): keep the stub-failure test off the machine's heygen CLI; sync the skills manifest
* fix(skills): audio passes keep a host app's music and sounds; silent needs no sound cues
* fix(skills): host audio keeps string frames, stops on a broken audio_meta.json, survives a re-run
* fix(skills): report a host audio entry with no path and a broken sidecar instead of crashing
* fix(skills): keep host audio written while an audio pass runs
* test(cli): serve the telemetry capture while the resolve child waits on it
* test(cli): fail the telemetry capture when the resolve child exits non-zero
* style(skills): wrap the host audio comment to the 120-column limit
* fix(engine): motion blur works over video by holding each video frame across samples
Each output frame's injected video frame is now held for every sub-frame
sample and probe of that frame, so footage stays as shot and only the
layers over it blur. The refusal for sessions with a before-capture hook
is removed.
Fixes#5144
* fix(engine): trim held-video comments, pin the new test-classification hash
* fix(engine): a blurred frame at a sub-composition cut keeps its video
Each sample now decides which videos show at its own time, like every other
layer, and only a video on screen at the output frame's time keeps that
frame's picture. Before, the visible set was taken at the frame's time while
sub-composition hosts were hidden at the sample's time, so the samples just
before a cut showed no video and the cut frame dipped toward black. The
before-capture hook takes the held time as an optional third argument, and
the early injection is gone: the GPU re-render now always runs at the page's
own time.
* style(engine): one doc comment for the before-capture hook
* fix(engine): a video animating its own style stays in step when blurred
The injector copies a video's own style only when its frame changes, so the
first injection of each blurred frame now runs at the frame's own time
before the samples. A video sliding by its own transform no longer sits a
quarter frame behind with blur on.
* test(producer): a two-line header for the motion blur over video test
* test(producer): reach the cut fixture by path, not a URL fallow reads as an import
* fix(ci): skills sync publishes past a version the registry holds unpublished
* fix(ci): skills sync fails unless a republished skill is the registry's latest
* fix(ci): skills sync republishes one patch past a hidden version and checks it as owner
* test(ci): skills sync fails the job on an error and stops after a clean sync
Two mutants survived review: dropping the line that sets the exit code (the job would print its ::error:: lines and still pass) and dropping the clean-sync return. One test runs the script with a failing fake clawhub on PATH and expects exit 1; another pins that a clean sync makes no further call.
* test(ci): the exit-code test can only reach its fake clawhub
The child's PATH was the fake's folder plus the inherited PATH, so on a machine where the fake cannot execute, a real logged-in clawhub further down would run sync for real. PATH is now the fake's folder alone.
* fix(ci): skills sync skips only hyperframes-creative's hidden 1.0.13
Republishing one patch past any taken version would publish past every future hidden version with no one looking. Only the acknowledged case (hyperframes-creative, 1.0.13 taken) is published at 1.0.14; any other taken version fails the sync as before.
* test(ci): the skip allowlist matches the skill, not just the version
A taken 1.0.13 on any other skill must fail the sync too; without this case, accepting 1.0.13 for every skill passed all tests.
* refactor(ci): skills sync picks the republish version in its own helper
The allowlist check took republishTaken to cyclomatic 5, which the Fallow audit scores at its 30.0 CRAP threshold; deciding the version in versionPastSkippable keeps each function under it. Behaviour unchanged.
* test(producer): unit tests never reach the network
* test(producer): the unit lane refuses sockets to other hosts, not only fetch
* test(producer): refuse outside hosts at the lookup, not by parsing socket calls
* test(producer): keep dns.lookup promisify-compatible under the no-network guard
* fix(core): namespace SVG ids in runtime-mounted compositions
* fix(core): keep SVG selectors and captured ids working after loaded scenes rename them
* chore(core): generated artifacts carry a plain do-not-edit banner
* test(catalog): record the font subsets the new scene-script installer requests
* test(catalog): keep earlier mirror recordings, as re-records only add
* fix(catalog): 3D previews load their scripts under the docs host's script policy
* fix(catalog): preserve script text through HTML parsing
* docs(catalog): describe the shared script bundle
* fix(engine): a render fails when a composition script throws before its timeline binds
* fix(engine): an unrelated page error no longer fails a render whose timeline registers late
* fix(engine): only the composition's own scripts fail a render, local or distributed
* style(engine): format the script failure change
* fix(engine): a third-party browser rejection no longer fails a render
* refactor(engine): one page URL for navigation and error attribution
* fix(engine): a rejection after a hash change or in an iframe no longer fails a render
* fix(engine): a widget's error no longer fails a render once its script is inlined
* fix(core): name injected and inlined scripts without building code from library input
* fix(engine): a third-party embed's eval or inserted script no longer fails a render
* fix(producer): a widget's inline handler or a sub-composition no longer misreports a render
* refactor(engine): read each script response once and keep comments within 120 characters
* fix(engine): a scene that throws after registering its timeline no longer cuts a render short
* fix(engine): a composition script failure names the error and its real cause
* fix(engine): a failed VFX chain fails the render at once instead of shipping a missing layer
* fix(core): a VFX layer that is empty on one frame renders empty instead of failing or going stale
* fix(core): a VFX layer that grows from zero size renders instead of staying missing
* fix(core): a VFX layer hidden or late while empty renders once it has a size
* test(producer): cover a VFX layer that is 0×0 under a host that starts later
* test(producer): pin both VFX 0×0 marks, with a GSAP-shaped wipe and a real late host
* test(producer): a late VFX host with a GSAP wipe catches a walk that skips hidden hosts
* feat(core): the renderer's font pipeline is importable from @hyperframes/core/fonts/embed
* style(core): format the font pipeline and state the bundled-subset invariant
* test(producer): repin the test lane map after the font tests moved to core
* fix(core): images build fonts once, CLI bundles one pipeline, font tests pass on Windows
* test(core): font retry tests get a budget no slow machine can exhaust
* test(core): the Retry-After budget test fails when the budget is ignored
* fix(core): bundled local scripts keep their own place, defer and async
The bundler joined every local <script src> into one inline script at the first one's position. That dropped defer and async (a deferred main.js ran before a deferred CDN gsap), moved later files ahead of CDN scripts between them, and let one file's error stop the rest. Each local script is now inlined where it stands with its attributes; a deferred or async one keeps its tag with its file as a data: URL. Inline-script merging also no longer folds in nomodule scripts, which dropped the attribute.
* fix: preview scripts run after the runtime when the bundler fails, and inlined files are escaped
- When the bundler returns nothing or throws, the preview places the runtime
where the bundled page does (end of head) instead of after the composition's
scripts or not at all; core's placement moves to insertRuntimeTag.
- Every HTML-spec JavaScript MIME type counts as a classic script for merging
and the web-font gate.
- Inlined local files and merged runs are escaped with escapeInlineScriptSource.
* fix: deferred local scripts run late without data: URLs, and authored runtime reads survive
- Local defer/async scripts are inlined in their own tag with the attribute
kept; the font-gate runner (and its old-runtime fallback) runs an inline
classic defer script after the classic scripts, in document order, so a page
policy that allows inline scripts but not data: still runs them.
- stripEmbeddedRuntimeScripts matches the bootstrap attribute and runtime file
names on the tag, not runtime words in an authored script's text.
- The preview applies its runtime on every path, so an adapter page without
one still gets it; the inline escaper writes <!-- as \x3C!--, valid in a
unicode regex.
* fix: a deferred inline script waits for the deferred CDN script before it to load
* fix: an authored script that mentions the runtime marker no longer stops the runtime being added
* fix: only inlined deferred files run late, head ones included, and renders skip a CDN runtime link
* fix: deferred head files run as before, and one selector owns the deferred-file rule
* fix: a local file's legacy <!-- comment no longer breaks its merged run, and escapes keep case
* fix: a local file inlined on its own keeps working with a legacy <!-- comment
* test(core): a local non-JavaScript file keeps its text as written
* docs(skills): resolve cross-skill contradictions from the doctrine audit
Section 1 of #5025, each direction taken from the code or the shared
contract:
- async timeline build is supported (runtime rebinds an async-registered
timeline; lint only flags registering before the build finishes), so
animation and keyframes now say register after the build completes
- data-track-index is a Studio row, display only; layering is z-index
- the final gate is `check`, which reruns lint
- music-to-video keeps render approval as the one autonomous question,
per the brief contract
- faceless-explainer and product-launch-video pre-install registry blocks
before dispatch, like pr-to-video
- talking-head-recut layout files cover landscape and portrait; 4:5 is
derived
- embedded-captions Cinematic authors cinematic.json; make-cinematic.cjs
derives plan.json
- audio carve has no static mode; remove the stale static and dynamic text
Left open: the figma usage beacon and the skills-update consent rule.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore(skills): refresh manifest; treat cinematic.json and safe-zones.json as project files
Both are written into the user's project by the embedded-captions workflow,
so the comment-citation check should not look for them in the repo.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at 58934a34.
- One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says
`handoff`, and Studio's button reads it, so the gate cannot be half-flipped.
- While gated nothing opens and nothing is written: `openInDesktop` returns
`handoff-unavailable` (so `hyperframes open` prints the download line and the POST route
answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release.
- The POST answers only Studio's own same-origin request (loopback Host, matching Origin,
Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to
server/desktopRoutes.ts with their own tests.
- `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad
directory, and tells a missing app from one macOS could not open (`open-failed`). A project
that cannot be written still opens, with nothing handed over.
- Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft
and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`).
- The load smoke mocks the route.
* feat(cli,studio): hand off on macos now, and open the app on windows and linux
The released Mac app (b254) takes a handed-over folder and its conversation, so
HANDOFF_READY turns on for macOS. Windows and Linux stay on the download until the
desktop change that reads a folder from the command line ships.
- `hyperframes open` finds the app off macOS: the per-user install on Windows, and
on Linux the AppImage the app records in launcher.json; it starts it with the folder
- download links follow the machine: the DMG on macOS, the AppImage on Linux, and
nothing on Windows, which has no public download yet
- the Studio button shows wherever there is a download or an installed app; the card
no longer says "for macOS"
- every render prints the app line except batch rows: music-to-video delivers a draft
- the success line names the app that took the folder (Canary included)
* fix(studio): the edit-with-framey button asks its route through studioApiFetch
* fix(cli,studio): name the app the hyperframes desktop app, apart from the studio preview
* fix(cli,studio): fewer comment lines, so no file's comment share rises
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at 58934a34.
- One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says
`handoff`, and Studio's button reads it, so the gate cannot be half-flipped.
- While gated nothing opens and nothing is written: `openInDesktop` returns
`handoff-unavailable` (so `hyperframes open` prints the download line and the POST route
answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release.
- The POST answers only Studio's own same-origin request (loopback Host, matching Origin,
Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to
server/desktopRoutes.ts with their own tests.
- `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad
directory, and tells a missing app from one macOS could not open (`open-failed`). A project
that cannot be written still opens, with nothing handed over.
- Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft
and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`).
- The load smoke mocks the route.
* fix(studio): the edit-with-framey button asks its route through studioApiFetch
* feat(registry): eight ad templates show their video preview in the catalog
* refactor(scripts): the preview backfill reuses the shared entrypoint guard
* ci: a test-only change under studio or player needs no captures
Tests are not behaviour a user sees, like the markdown the check already skips.
A bench-harness PR (#4953) was asked for Before and After screenshots.
* ci: pin a mixed source and test change, and say tests are not counted
* fix(ci): let markdown-only changes under studio and player skip the captures check
* docs: map the Studio edit path, the edit accuracy bench and test setup for agents
* docs: name the Studio captures rule and the local comment checks
* docs: point the Studio gate notes at the scripts that own the rules
* docs: correct the Studio save paths and add the producer stage map
* docs: name where producer sizes workers and which captures encode inline
* feat(registry): add cinemagraph — a still photograph with one living region
* feat(registry): add ai-answer-rank — a mock AI answer that re-ranks to put your brand in it
A 1080x1080, 9s block for answer-engine and AI-visibility marketing. A prompt
types into a chat card, the assistant answers with a ranked list of three picks,
then re-ranks: the promoted brand slides into its slot (brandRank 1-3), the picks
below step down, the third fades out of the list, and the brand row lands with
the one accent highlight and a drawn check. Monochrome placeholder UI; prompt,
lead-in, pick names, brand, rank, accent, ink and ground are variables.
* feat(registry): add caret swap text effect
* feat(registry): add pendulum-swing component
Add a motion-primitive component that hangs content from a fixed pivot and
lets it settle with a damped oscillation, like a hanging sign coming to rest
on its hook.
The swing rides a single unitless custom property (--hf-swing-angle) that the
timeline eases 1 -> 0, so only rotation animates and it stays sub-pixel under
the seek-by-frame capture engine. The demo drives it with a damped-oscillation
ease (a cosine inside an exponential decay, normalised to land exactly upright)
so the sign passes through center several times, each pass smaller, before it
rests.
Four enum variables: pivot (top-center/left/right), amplitude
(gentle/standard/wide), direction (right/left), tone (ink/paper/accent, each a
bg/fg pair to keep AA contrast on any frame). The snippet is recipe-only and
does not own its timeline; its demo carries the same variable declaration.
Ran npx hyperframes lint/validate on both files, regenerated registry.json via
scripts/generate-registry-items.ts and the docs via
scripts/generate-catalog-pages.ts.
* fix(registry): drop hand-added cinemagraph index entry; catalog-publish adds it
* style(registry): format pendulum-swing with oxfmt
* fix(registry): keep pendulum-swing demo caption clear of the swinging sign
* fix(registry): ship the ad-template editing contract for ai-answer-rank
---------
Co-authored-by: Carlos Miranda Levy <carlosmirandalevy@gmail.com>
Co-authored-by: tugrawork-creator <319556794+tugrawork-creator@users.noreply.github.com>
Co-authored-by: Akshar Patel <aksharcommit@gmail.com>
Co-authored-by: rottenpen <12029924+rottenpen@users.noreply.github.com>
* fix(lint): fail skill lint on unresolved doc cross-references
scripts/lint-skills.ts now checks every *.md under skills/, .claude/skills/
and .agents/skills/ for cross-references that do not resolve: inline links
[text](target), reference definitions [id]: target, and backticked paths
that are explicitly relative (./ or ../) and end in .md or .html. Targets
resolve against the referencing file's directory and must exist; a #anchor
on a Markdown target must match a GitHub-style heading slug of that file,
and same-file #anchor links are checked against the file's own headings.
Fenced code blocks, URI-scheme targets, absolute paths and placeholder
targets ({ } * $ < >) are skipped. Bare backticked paths without ./ or ../
are a documented blind spot: on the current tree they are mostly skill-root
shorthand, another skill's file, or a runtime artifact, and checking them
produced ~200 hits that are not wrong.
The former top-level main now runs only when the script is the entry point,
so importing the exported checkers from the test file no longer exits the
test runner when the tree is red.
On the current tree the rule reports 34 violations across 16 files, all of
the shape described in #3919 (../ one level too shallow for the referencing
file, or a file that does not exist). The docs are corrected separately.
Refs #3919
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(lint): trim the cross-reference rule's comments and inline its reporter
* docs(lint): cite placeholders, not invented paths, in cross-reference comments
* refactor(lint): keep the skill lint entry point under the complexity limit
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(release): use portable Git paths in release guards
* fix(release): drop the comment on a path git never compares
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(studio): resizing an element GSAP only positions saves its CSS width and height
* fix(studio): a resize never saves GSAP's scale mask as the element's CSS
* fix(studio): keep the resize size writer inside the file and comment budgets
* fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders
* fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it
* test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview
* fix(studio): a move keeps an authored xPercent centring set in the saved file
* fix(studio): a drag on a centred element no longer flashes off-centre on release
* fix(studio): a font pick saves the edit and its @font-face in one write
* fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit
* test(scripts): pin that a slash after i-- before a quoted slash is division
* fix(studio): an inspector edit saved while a preview reload loads stays on screen
* fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it
* fix(studio-server): a font name or path with braces in quotes still saves its @font-face
* fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule
* test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass
* feat(cli): one owner for init media placeholders, audio stays on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): init keeps a video's sound on the clip; silent files scaffold muted
Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com>
Co-Authored-By: kapelame <kapelame@users.noreply.github.com>
* feat(registry): example templates keep the a-roll's sound on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): video audio rules recommend keeping sound on the video
Update video_missing_muted and video_audio_double_source message and
fixHint text to recommend the new default: keep audio on the video with
data-has-audio="true" (not muted). Tests verify the new wording is in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): a video is either audible (data-has-audio) or muted, never both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): follow the compiler's audio rule in the audio-track toggle
Confirm before background removal drops a video's sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): hyperframes-core teaches sound on the video by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): workflows and scaffold guidance keep a video's sound on the clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: footage with sound keeps it on the video clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): init from-file a-roll is timed, muted strip is attribute-exact
The from-file a-roll video gets data-start="0" so a scaffold lints clean, and
the orphaned audio-only slot comment is dropped. setVideoAudio now walks the
tag attribute by attribute, so a class value containing the word muted
survives. The ffprobe-missing warning says the video is assumed to have sound
and how to switch to muted, and DEFAULT_META notes the failure surfaces at
render until lint support lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep has-audio in step with the muted toggle, test cutout confirm
Muting a video now clears has-audio and unmuting sets it to true, in both the
Design and flat media panels, matching the Has audio track toggle. Audio
elements are unchanged. Adds coverage for both panels plus the cutout
window.confirm guard: declined leaves src alone, accepted swaps src and mutes,
a silent video never prompts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false
An audio element or an undefined hasAudio no longer picks up a muted attribute;
only an explicit false on a video writes muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: video_missing_muted rule text, silent-footage notes, consistent audible examples
lint.mdx and cli.mdx describe the current video_missing_muted rule instead of
the retired unmuted-video wording. The editing recipes say how to adapt for
silent footage and require the outgoing clip's sound to end at a J-cut. The
Remotion media example shows the audible mapping consistently, data-attributes
states that has-audio is required unless muted, and the hairline frame note no
longer claims the video is muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): encode init media filenames and probe --audio duration
Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded.
--audio now passes the probed track length as the composition duration in both init paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): audio-state-aware media rules and time-aware double-source check
video_missing_muted honors any explicit data-has-audio.
video_audio_double_source only considers audible videos and only fires
when the same-src clips overlap in time.
duplicate_audio_track now covers audible videos.
audio_file_without_element no longer claims silence when an audible video
carries the sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): audio-stream check for remotion ports, followable J-cut recipe
Remotion Video maps to data-has-audio only when the file has an audio stream.
The J/L-cut recipe now shows both shots with muted pictures and separate audio.
embedded-captions removes the audio.mp3 scratch file it extracts.
skills-manifest.json regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(lint): trim comments to satisfy the comment ratchet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): atomic muted edits, cutout keeps sound on a linked audio
Muted and Has audio track commit one batched write (one persist, one
undo entry) and remove data-has-audio instead of writing an empty
marker. Unmuting a video probes its src: audible -> true, silent ->
false, probe failed -> absent. Remove background on an audible video
inserts a linked sibling <audio> with the original sound in the same
write, replacing the drop-the-sound confirm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(producer): skip hidden videos when auto-adding video audio
A video hidden in Studio is dropped by the mixer, so its src never reached
existingAudioSrcs and the render re-added it at full volume. The video parser
now records the shared hidden predicate and the auto-detect step skips hidden
videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep useDomEditSession under the 600-line cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com>
Co-authored-by: kapelame <kapelame@users.noreply.github.com>
* ci(studio): measure drag smoothness on a quiet runner, with a blank-page control
* ci(studio): gates read the base the run measured, not the base branch tip
* style(ci): format the base-file test
* fix(scripts): count comments after regex literals in the comment ratchet
* fix(scripts): keep comments after JSX tags, ++/-- and wrapped division in the ratchet
* feat(studio): the code editor follows studio's light and dark theme
Replaces One Dark with a Studio editor theme whose colours are theme.css variables: eight syntax role tokens (One Light and One Dark hues held at 4.5:1), editor surfaces from Studio's roles, all measured by the contrast ratchet.
* test(studio): the code editor theme takes every colour from a theme.css token
* fix(studio): code stays at 4.5:1 when a selection or search match sits on the active line
Syntax tokens are tuned for the selection over the active line, which the ratchet now measures; matches are outlined rather than stacking another wash; the search panel's pressed button and special characters follow the theme; unused tooltip rules go.
* fix(studio): code matches carry no background, and types read apart from constants
Search and selection matches are outlined only, so CodeMirror's lime match wash and stacked washes are gone; type and constant tokens split by hue in both themes.
* fix(studio): the current search match reads apart by width, not colour alone
A 2px accent outline on the current match and a muted 1px outline on other copies of the selected word.
* feat(studio): graphite colour system with a light theme that passes contrast
Studio's tokens take the Graphite values: a lifted dark, a light set behind
data-theme="paper", the player dark in both. Every text role clears 4.5:1 and
every icon and ring 3:1 on the fills it sits on, held by the contrast ratchet.
* fix(studio): every studio surface follows the light theme and passes contrast
The dock no longer inherits dockview's forced dark scheme, popovers sit on the
raised surface, stock white, red and amber text map to roles, the keyframe
diamonds and track headers use roles, and the contrast ratchet measures every
pair in both themes (150 pairs).
* fix(studio): stock white, red and amber stay colours; studio text and fills use roles
Round-two review: white, red and amber are colour content as well as text, so
they keep Tailwind's values and hosts keep their palettes. Text, toasts, the
feedback card, the FX popover, destructive buttons, category labels and the dock
take Graphite roles instead; the contrast gate rejects an unparseable colour.
* fix(studio): every light-theme ink, icon, dot and focus ring holds its contrast
Stock white, red, amber and file-type hex left in Studio chrome now use roles or a light-dark pair; the file tree icons carry a contrast test.
* chore(studio): drop two comments the names already carry
* fix(studio): red marks, teal outlines and colour curves hold contrast in both themes
Danger graphics use the danger ink, file tree outlines use the accent ink, the colour-curve well stays dark like the scopes, and six timeline token pins come back.
* fix(studio): invalid readouts, the curve focus ring and editor dots stay distinct in both themes
The invalid readout border is dashed as well as red, the curve graph's ring sits inside its dark well, the radius dots and ease handle centre follow the theme.
* refactor(studio): ease and radius editors use colour classes; the dashed invalid border has a test
Simplify pass: the ease editor's inline accent styles become stroke and fill classes, the radius dot class is computed once, the ratio check is one expression, and the alias table is named for what it holds.
* fix: readers never see an empty or half-written project file
* test: pin the create routes and block install to whole-file writes
* fix(sdk): a write through a link to a missing file creates that file
* fix: saves through a linked folder reach the real file, atomic writes handle long names
* test(sdk): check for the stray file, not the folder listing
* fix(core): creates work on Windows FAT drives, and links through folders resolve as the system does
* fix: agree the helper move in the deletion check, and match temp paths without a path regex
* test: check POSIX file modes only off Windows
* feat(catalog): shelve 3D objects and cursors in their own catalog sections
Carries the generator grouping rules from #4310 and #4308.
* feat(registry): add a starfield ambient overlay
From #4313.
* feat(registry): add a falling snow ambient overlay
From #4331.
* feat(registry): add a torn-paper-rip transition
From #4321.
* feat(registry): add a paper-collage-reveal transition
From #4322.
* feat(registry): add a curtain-reveal transition
From #4323.
* feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo
From #4310.
* feat(registry): add eight cursor animations and the dart cursor
From #4308.
* fix(catalog): find items by everyday editor words and accept a bare search word
From #4311.
* chore(registry): remove the simulated-cursor component
The Cursors section's new pointer animations replace it (from #4308).
* fix(registry): take cursor-zoom-follow's screenshot from runtime variables only
The declared default is empty, so page markup never becomes an image URL.
Clears the code-scanning alert raised on #4308.
* docs(cli): show a catalog search example that finds items
"smoke" matches nothing in the catalog; "crossfade" finds fade-through.
* chore(ci): allow the simulated-cursor removal in the deletion guard
* fix(ci): skip tracked files missing from disk in the player CDN pin check
Catalog regeneration removes a deleted item's pages before the publish PR untracks them,
so the check crashed on every catalog deletion PR.
* chore(cli): drop comments that narrate catalog search history
* fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering
* fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s
* fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value
* fix(registry): show real file thumbnails and end the context menu demo sooner
* fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop
* fix(registry): centre the text-select toolbar on the selection and tighten its timing
* fix(registry): selection cursors default to the macOS system blue
* style(registry): format the agent and annotate cursor sources
* fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing
* fix(registry): frame the product turntable fully and light it with two studio softboxes
* fix(registry): touch-indicator taps like a real phone and the post it taps gets liked
* fix(registry): press-ripple confirms with a check and small click rings at the cursor
* fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled
* fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim
* fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press
* style(registry): format the exploded stack source
* fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen
* fix(registry): make glass-hero a clear lens that shows the headline through it
* fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card
* fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on
* fix(registry): starfield reads as a drifting night sky over a real title card
* style(registry): format the glass hero source
* fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away
* fix(registry): curtain reveal draws velvet folds that gather onto a title card
* fix(registry): make every zoom-follow click change the screen and read at zoom
* fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc
* refactor(registry): drop dead code and correct stale comments in reworked items
* fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names
* docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple
* docs(registry): plain lowercase comments in oversized-cursor and touch-indicator
* docs(registry): oversized-cursor names CLICK_AT as the click cue
Ctrl+C during a render left its whole work dir of captured frames (and a
staging dir) in the renders folder. Puppeteer's default SIGINT handler kills
Chrome and exits the process before the render's own cancel path can remove
them. The engine keeps that default unless the host owns Ctrl+C; the CLI's
render cancellation scope claims it while a render runs, so the render aborts
and cleans up after itself.
A render killed outright (SIGKILL, crash) cannot clean up, so each work dir and
staging dir now records its owner process (pid, hostname, boot, and on Linux
the pid namespace), and the next render into the same folder removes
the ones whose owner has exited.
Cancelling a render while its browsers start no longer leaves anything
running that kept a batch render alive after Ctrl+C: the BeginFrame warm-up
loop, the probe browser and file server, or a capture browser whose launch
finished after the cancel (now closed before the worker settles, since
Puppeteer's exit hook skips every other live browser).
* fix(docs): restore visible example video cards
* fix(docs): use catalog player for example films
* fix(docs): fit player posters before playback
* fix(docs): preserve the original examples grid
* fix(producer): gif renders survive rgb frames among transparent ones
* test(producer): run the gif mixed-frame encode test in the ffmpeg lane
* fix(producer): gif encode test passes the argv, reachability and comment checks
* fix(producer): transparent gifs clear every frame so an opaque frame never lingers
* fix(producer): only gifs that turn translucent after an opaque frame get whole cleared frames
* fix(core): plain text keeps its own gif control block; gif encode failures share one shape
* fix(release): name Release once in a changelog entry's tags
* fix(release): the release tag appears once in any case; four past entries lose the duplicate
* fix(producer): renders use the font named in a var() fallback
When a font-family starts with var(--x, A, B) and --x is not defined, the
render now reads A, B the way the browser does. A system font at the head of
that fallback gets Inter placed in front of it, the same rule a direct
system-font stack already follows. The rewrite stays inside the var()
fallback, so a defined --x still wins. A defined property whose value is
itself a var() is followed too. The distributed plan check reads the same
answer.
Fonts named only in such a fallback are best-effort: they are embedded when
they resolve, and one that does not resolve is logged instead of failing a
distributed render. Fonts named directly, or through a defined property,
still fail closed as before.
CSS-wide keywords such as inherit are no longer treated as font names to
fetch.
Compiled the way a distributed render compiles them, 28 registry items now
embed a font they name this way, and none newly fails.
* refactor(producer): fold var() fallback resolution into one branch
* fix(producer): fonts behind a var() chain or empty fallback add no new render failures
A family reached through a chain of defined var()s is optional like a
fallback-only one: fetched fail-closed, skipped only when no source serves
it. An empty var() fallback makes the declaration invalid, so the browser
inherits and the plan check no longer rejects the families after it.
* test(producer): a family named after a leading var() stays required
* fix(producer): a transient error on an optional font warns and renders the fallback
* chore(catalog): record the font requests var() fallbacks now make
* fix(producer): required fonts fetch before optional ones can spend the time budget
* fix(cli): commands exit without waiting on update checks or telemetry
The update and skills checks run in a detached child that refreshes the
caches for the next run; the parent only reads the caches. finalizeCli no
longer awaits the telemetry flush: the exit handler flushSync() delivers
what is queued. The events beacon, feedback and figma error paths stop
awaiting flush() too.
* fix(cli): retry a failed background check at most once an hour
Before starting the background worker, the CLI stamps a per-check attempt
time and skips a check it attempted within the hour, so a dead network no
longer starts a worker on every command. The stale-pin notice writes onto a
fresh read, detached spawns set windowsHide, and a test ties the worker path
to its build entry.
* fix(cli): a failed-check stamp dated in the future no longer silences update checks
* feat(plugins): ship portable agent bundles and installation guides
* fix(plugins): align new manifests with current release
* test(plugins): split caption fixture setup and cleanup
* test(plugins): pass render fixture path as an argument
* test(cli): CLI tests run with a home folder of their own, never the user's
* style(cli): format the test home check
* test(cli): the skills test lane also runs with a home folder of its own
* style(cli): format the test home check
* ci: the test reachability map follows the skills lane's new command