The motion-blur snippet integrated translation only. Its output stage was one
SVG filter per target whose copies were feOffset, and feOffset only translates,
so a beat that only scaled or only turned sampled a displacement of zero and
rendered sharp. No SVG filter primitive can give a copy its own affine
transform, so the sampling loop was never the problem: the output stage was.
Copies are now DOM clones of the element in an isolation:isolate group, each at
1/N opacity with mix-blend-mode plus-lighter, each carrying the element's
resolved transform at its sample time. plus-lighter adds premultiplied colour,
so N + 1 copies at 1/N sum to the average the shutter integral asks for; the
isolation is load-bearing rather than tidy, because without a transparent
backdrop of its own the first copy adds onto the page and blows a light
background out to white. Reading the resolved matrix instead of a list of named
GSAP properties is what makes every driver work at once: translation, scale,
rotation, 3D rotation and skew arrive in the same one value.
Measured against the After Effects export the shutter model was fitted to, per
beat, previous stage to this one: scale +0.62 dB, rotate X +0.43, rotate Y
+0.84, rotate Z +1.15, translate X -0.18, translate Y -0.04. The two small
translate regressions are inherent and not tunable: the filter offset one
rasterisation so every copy was pixel-identical, where each copy is now
rasterised independently at its own fractional transform with its own subpixel
antialiasing.
Three consequences of the new stage, each carrying its own fix:
- mix-blend-mode flattens preserve-3d, so a copy cannot inherit the parent's 3D
context. Each copy carries the parent's perspective as its own first transform
function instead.
- A copy is styled by nothing that selected the original, since dropping the id
also drops every #id rule that gave it size, colour and font. Each copy
carries its own resolved style inline, read once and replayed. Those values
are px, so a container-relative element needs them again when its box changes:
a ResizeObserver re-snapshots rather than paying a subtree walk every frame.
- A beat that moved and faded at once kept a full-strength smear behind a
vanishing element. The element's opacity now rides the group; the 1/N weight
stays on the copies.
The axis option is gone. It selected which of x and y to sample, a question the
resolved matrix does not have.
shutter-slam is the new component: one word through six single-property beats,
two translate slams, a scale punch, then a full turn about each axis, which
exercises every driver the model covers in one mount. Easing is power2.inOut
because it measured best, not because it was the default. A bezier fitted to the
reference's own bbox centres looks like the better curve and renders worse: a
blurred bbox centre is the shutter window's average rather than the instant, so
on an accelerating segment the fit measures its own blur.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* chore: prepare v0.8.46
Draft only, not tagged: bun run release:prepare 0.8.46 --no-tag.
Changelog rewritten in plain words for the one change since v0.8.45
(#4031, adaptive per-frame motion-blur sample count). Cutting the
release is a separate step on Miguel's word.
* docs(release): fix false render-time savings claim in v0.8.46 notes
Adaptive sampling never drops below the old fixed-16 default; it only
adds cost on frames that measure real spatial motion. Per Miga's review
on #4033.
* fix(registry): make motion-blur match After Effects shutter-based blur
The motion-blur catalog snippet estimated speed from the x/y delta
between consecutive timeline updates and drew three one-sided ghost
copies with decaying alpha plus a Gaussian on top. After Effects does
temporal supersampling: each frame integrates the layer over a shutter
window (shutter angle, shutter phase, samples per frame), producing a
symmetric box smear of length speed x shutter time.
The snippet now does the same inside the page. After every timeline
update a deferred pass seeks the GSAP timeline to N sub-frame times
across the shutter window (events suppressed), reads each target's x/y,
restores the frame time, and drives an SVG filter that averages N
feOffset copies with equal weight (feComposite arithmetic, 1/N, sRGB).
Defaults follow AE: shutterAngle 180, shutterPhase -90, samplesPerFrame
16; fps comes from the render config, then the root data-fps, then 30.
Displacements are mapped through the inverse of the element's own
scale/rotation; percent x/y values are resolved to px; SVG targets use
their bbox; an existing inline filter is kept and the blur appended to
it. The pass runs in a microtask rather than inside the tracker tween's
onUpdate, because seeking GSAP from within its own render reads stale
tween state and suppresses the composition's callbacks on forward
seeks. The heuristic options blurScale/blurMax/stretchScale/stretchMax
are removed.
Reference: an AE-algorithm reconstruction of the same composition
rendered without the component at an effective 960 fps and averaged
over the 16 sub-frames of the 180 degree window per output frame.
Against it (1920x1080, 30 fps, solid + text translating linear then
power2.inOut): old mean PSNR 53.2 dB (min 26.7), SSIM 0.9930; new mean
PSNR 72.4 dB (min 52.6), SSIM 0.9998. The 10-90 edge ramp at 66.7
px/frame is 26.8 px in the reference, 27.5 px new, 14.5/6.3 px
(one-sided) old; at the ease peak 79.3 vs 79.5 vs 7.9/17.7 px, with
smear centres matching to 0.1 px.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(registry): drop render-config fps probe from motion-blur and refresh its docs
The snippet resolved fps by reconstructing the renderer's private seek
config key so the compiler's inline-script guard would not strip it. A
public catalog snippet must not depend on that key, so fps is now the
explicit option, then the root data-fps, then 30; renders with an fps
override pass fps explicitly. The catalog page's Usage, How it works and
Options sections still described the old velocity heuristic and are
rewritten for the shutter model. The 1/N weight keeps full precision.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(registry): match the After Effects reference export's shutter window
The shutter model landed on this branch was built against a reconstruction
of After Effects' algorithm, not against After Effects. A real 1920x1080 /
30 fps export of translating text disagrees with it on three points, all
measurable off the export itself.
The window is two frames wide, not half a frame. On the measured frame the
outermost duplicate on the trailing side sits 128 px back while the previous
frame is 125.8 px back, and the outermost leading duplicate sits 189 px ahead
while the next frame is 184.9 px ahead: the window ends land on the
neighbouring frames. Defaults become shutterAngle 720, shutterPhase -360.
Duplicates sit on the sub-interval boundaries, both ends included, not at
sub-interval centres. The staircase across a stroke steps every 15.6 px on a
125.8 px displacement and every 23.0 px on a 184.9 px one, i.e. an eighth of
a frame, with the run closing exactly on the neighbouring frame's position.
samplesPerFrame now means sub-intervals of the window, so there are N+1
copies at k/N rather than N copies at (k+0.5)/N.
The frame-time instance is drawn sharp over the smear. At full ink the core
bounding box is the whole word at every frame and total ink rises to 1.44x
the unblurred frame's, neither of which an average can produce. A final
feComposite over of SourceGraphic reproduces it. Each duplicate stays at
1/N: the staircase plateaus step by 0.063 +- 0.002 of the sharp text
intensity, flat, with no taper toward the window edges, and triangle
weighting scores 1.6 dB worse against the export.
Verified by replaying the export's translating section as a composition
whose sprite is the export's own rest-frame ink and whose eight legs follow
its own measured per-frame positions, rendered at 1920x1080 / 30 fps and
compared frame by frame over the text band. Mean PSNR against the export
rises from 24.19 dB (min 17.07) to 32.51 dB (min 27.57) across 72 frames,
and from 19.83 dB (min 17.07) to 32.03 dB (min 27.57) across the 26 frames
that actually move. Ghost extents match to a few px and the rendered
staircase plateaus match the export's to two decimal places. The export's
later scale, skew, flip and 3D rotation sections are out of reach here:
integrating anything but translation inside the shutter is engine work
tracked in issue #4010.
The filter region now clamps its bounds through zero. It was derived from
the duplicate offsets alone, which was correct while the output was nothing
but duplicates; the sharp instance draws at offset zero, so a window sitting
entirely on one side of the frame time - any shutterPhase above 0 or below
-shutterAngle - produced a region starting inside the element's own box and
cut the instance off. The defaults straddle the frame time and were never
affected, so the measured numbers above are unchanged.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
- Lint no longer reports root_dimensions_mismatch for sub-compositions, the
false positive v0.8.43 shipped with; a top-level mismatch is still reported
and a viewport-meta-only mismatch gets its own wording (#4012)
- The streaming encoder accepts lockGopForChunkConcat and gopSize, emitting the
same arguments as the disk encoder. Off by default, so an existing render
produces an unchanged FFmpeg arg list (#3973)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
- Portrait compositions render the full frame when the project was
scaffolded at a different size: html/body are sized to the composition
root at runtime; new lint warning root_dimensions_mismatch (#4005,
fixes#4001)
- render_complete telemetry adds composition_element_tags,
aroll_video_count, heygen_video_count; render_error adds error_name and
failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
search no longer shows [object Object] (#4006)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
The generated catalog pages rendered the "Source" accordion title as a
JSX expression, `<Accordion title={`name.html`}>`. Mintlify's search
indexer stringifies expression attribute nodes rather than evaluating
them, so every one of the 399 catalog pages surfaced its Source section
in search results as "[object Object]" (visible on the Scroll Feed page,
among others).
The template literal carried no escaping benefit: every title is a
single-segment `name.html` path, and a double-quoted JSX string handles
it directly. Introduce `mdxStringAttribute`, which emits a plain
`name="value"` attribute and escapes `& " < { }` as HTML character
references (MDX decodes references inside quoted attribute values, and
unlike JSON.stringify it does not rely on backslash escapes, which JSX
strings do not honour). Use it for both the Source accordion title and
the preview iframe title, and regenerate the catalog pages.
Regenerated with `npx tsx scripts/generate-catalog-pages.ts`; the only
change across the 399 pages is the one Accordion title line each.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42
- Studio preview works for projects whose path has non-Latin-1 characters (#3982)
- Studio render downloads use RFC 6266 headers for non-Latin-1 filenames (#3984)
- check --json reports HDR auto-promotion; render prints one warning (#3985)
- Forced --sdr renders tone-map HDR sources before extraction (#3981)
- Render/preview workers no longer survive as orphans after Ctrl-C or parent exit;
cancellation reaches final artifact promotion; HYPERFRAMES_RENDER_DETACHED=1 for
intentionally detached renders (#3974)
- HDR preflight test coverage for held tails (#3963)
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42 notes — name the #3982 symptom and scope
- #3982 entry: a Studio preview server crash for compositions whose file path
has non-Latin-1 characters (the project directory is not involved)
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42 notes — name the #3982 symptom
- #3982 entry: the uncaught ETag TypeError took the preview server down, so
call it a Studio preview server crash rather than a preview failure
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* docs(changelog): weekly digest 2026-09-07–2026-09-14
Rewrites the generated draft into themed sections. 113 new commits across v0.8.32 through v0.8.40.
Signed-off by Rames.
* docs(weekly-updates): embed Sep 07-14 changelog video
Adds the weekly changelog video to the Week of September 7 digest entry,
matching the DocsVideo embed convention used by the prior weeks.
Video: 57.0s, square 1080, h264 + aac. Verified through CloudFront -
content-length and SHA-256 both match the local render exactly.
- Rames Jusso
* feat(cli): render summary names the capture path, gpu mode and stage timings
* fix(lint): drop gsap_exit_missing_hard_kill, its hazard does not reproduce on cold workers
* fix(cli): render slow-path hint fires only when the gpu was auto-probed
* test(cli): isolate agent hint suite from ambient agent env keys
The suite runs inside agent sessions. Ambient env keys fill the 16-key
hint cap and fail assertions that a plain shell would pass.
* docs(music-to-video): drop leftover hard-kill mandate after lint rule removal
Frame workers still required a tl.set hard-kill that the dropped lint
rule no longer enforces. Align the self-check with montage.md.
* refactor(cli): share hint-key pattern and collapse render summary args
Export HINT_KEY_PATTERN for the test isolation strip. Read duration and
frame count from the perf summary instead of passing them beside it.
* test(cli): lock capture-mode preference and 16-key hint isolation
Session mode, including drawelement, must print over observability.
The hint suite now fails if ambient keys already fill the 16-key cap.
* feat(cli): default BeginFrame, agent blank, and looks quality
Local auto render opts out of the software-GPU screenshot clamp so
BeginFrame can run. init --agent scaffolds a centered Inter blank with
no prompts. --quality looks (now the default) is CRF 16; delivery is high.
* fix(cli): honor screenshot env and teach agent pitfalls on the happy path
Local auto still requests BeginFrame unless PRODUCER_FORCE_SCREENSHOT=true.
The agent blank fills its canvas so --resolution can resize it. Skills now
name the four pitfalls and tell agents to read the render summary line.
* feat(cli): default init to the centered blank
Bare init scaffolds the centered Inter stage. --agent is a hidden alias.
* fix(cli): pack from-file and use it for video init
TTY --video with no --example selects from-file. Pack copies that template.
* fix(cli): wire --video onto from-file even with --example blank
One resolver picks the scaffold. Spawned init --video asserts a-roll src.
* fix(cli): narrow looks CRF on the quality alias
Only looks carries crf. Typecheck failed on the union without a guard.
* style(cli): shorten render-pipeline summary comment
The install command is the one line a reader comes to a catalog page to copy,
and it sat below the whole variables explorer, under its Customize panel, a
screen or more down on long items. Move the Install section above the preview
and regenerate all 399 pages.
## What
`keyframes --shot --layout strip`'s help text, its type's inline doc comment, and the CLI reference docs all described `strip` as an unqualified "filmstrip by time." The tool doesn't actually do that for the overwhelmingly common case.
## Why
A real per-time pixel filmstrip is only produced when the sampled selector is an SVG element (gated by an internal shape check — `typeof element.getBBox === "function" && typeof element.getScreenCTM === "function"`). Any other selector — including every nested sub-composition host, which is always a `<div data-composition-src>` — silently falls back to one live screenshot plus vector position markers instead.
This isn't a capture bug: for a non-SVG selector, real per-time pixel compositing was never implemented, only 3D bbox/marker sampling. But the documented behavior over-promised what the tool does, so a user following the docs on the common case (a DOM/sub-composition selector) sees root captions and empty image boxes where they expected the nested composition's actual content to move across frames — the diagnostic strip is misleading, even though the real render is correct.
## How
Reworded all three descriptions (CLI help text, `ShotOptions.layout` TSDoc, and the reference docs table) to state the SVG-only condition and the DOM/sub-composition fallback explicitly. No behavior changed — this is a documentation-accuracy fix, per the ticket's own framing that a doc-only fix fully resolves the reported symptom (a silent, misleading omission) for a P3.
## Testing
Added a test asserting the CLI help text no longer makes the unqualified "filmstrip by time" claim and does disclose the SVG-only condition — guards against a future regression back to the misleading wording. Verified RED (fails against the pre-fix string) and GREEN (passes after the fix) via a local before/after comparison.
- `bunx vitest run src/commands/keyframes.test.ts src/commands/motionShotLayout.test.ts` — 46/46 passing
- `bunx tsc --noEmit` in `packages/cli` — clean
- `bunx oxlint` / `bunx oxfmt --write` on changed files — clean
- Full CLI suite (excluding known-broken-in-sandbox browser-launch tests, unrelated to this change): 217 test files / 3025 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* fix(motion-graphics): make the catalog search fire before hand-authoring
The workflow's only reuse instruction pointed at catalog-map.md, a
hand-maintained snapshot of ~60 registry items, and no file in the skill
ever named `hyperframes catalog --query`. An agent asked mid-build for
CRT scanlines and a glitch effect had no instruction to search, so it
hand-authored both while caption-glitch-rgb ("RGB chromatic aberration
with CRT scanline overlay") ranks first for that query on either tier.
The search reads the hosted registry and needs nothing installed, from
any directory with no project, so "the components were not installed"
was never the cause. Say that where the reader is, since the wrong
diagnosis is the intuitive one.
Director Part 2 and the Builder now run the search before naming a
block, and catalog-map.md is labelled a partial snapshot whose misses
prove nothing. Pinned by a content test in coreSkillContent.test.ts.
* fix(skills): search the component catalog before hand-building a look
Authoring workflows never told the agent to search the component library,
so agents rebuilt effects the registry already shipped. A user reported
building an effect from scratch that the registry already contained; the
search that would have found it needs nothing installed, which is why the
usual self-diagnosis ("I forgot to install the components") is wrong.
All ten workflow skills carried zero mentions of `hyperframes catalog`.
The instruction lived only in hyperframes-cli and hyperframes-registry,
both loaded on demand, and the registry skill's own trigger named the
command rather than the symptom - circular, because an agent that never
thought to search could not reach the doc telling it to search.
- Eight workflows now run the search at the point they decide what to
build, before authoring. The two that compile through a closed
authoring vocabulary (embedded-captions, talking-head-recut) document
why they deliberately do not.
- hyperframes-registry triggers on the symptom (a named look, effect,
treatment or transition) instead of the command name; the router table
and the catalog surfaces carry the same framing.
- Fixes hand-maintained lists that had drifted: bar-chart-race was listed
as a hand-author gap in two files while shipping in the registry;
stat-motion was named as an installable block and is not one; the
caption-* family count was one high; the registry discovery tables
claimed to be the block list while covering 97 of 180.
- bun run lint:skills now fails when a doc marked as a registry snapshot
names an item the registry does not have.
* refactor(scripts): reuse native recursive readdir and the shared registry type
Simplify pass on the new registry-snapshot check, behaviour identical:
- collectMarkdownFiles uses readdirSync({ recursive: true }) instead of
hand-rolled recursion, matching scripts/generate-template-previews.ts.
- registryItemNames types registry.json with the exported RegistryManifest
instead of an ad hoc inline shape, matching scripts/catalog/build-local-vectors.ts.
The runtime guard stays: a cast describes the file, it does not validate it.
- One report() helper replaces the duplicated print-and-count block in both
lint passes.
* refactor(scripts): name the registry check's blind spots and stop self-arming
Applies the review findings on the new check, behaviour identical except
where noted:
- lintRegistryItemRefs returns null for an unmarked file instead of an
empty array, so "not a snapshot" and "a clean snapshot" have one owner
and the marker is matched once rather than twice.
- Marker detection ignores fenced blocks, so a doc that documents the
marker syntax in an example no longer arms the check on itself. The id
scan still reads full content, so fenced examples stay covered.
- The header comment and two tests now pin both known false negatives:
identifiers outside backticks, and single-word item names. Measured on
the six marked files, dropping the hyphen requirement would monitor 3
more items and force 46 allow= entries for ordinary prose words, so the
requirement stays and the gap is stated instead of silent.
* chore(skills): regenerate skills manifest after catalog-search edits
* feat(registry): add 25 image carousel blocks (5 families × 5 variants)
Five carousel families, each with 5 style variants:
- Orbit (1–5): image cards on a spinning 3D Fibonacci sphere
- Path (1–5): cards following animated CSS motion paths
- Circle (1–5): circular carousel layouts
- Vision (1–5): Apple Vision-style spatial presentations
- Text Circle (1–5): circular carousels with text overlays
All blocks are 1920×1080 at 6s, with 12–24 configurable image slots.
Includes catalog preview thumbnails for each block.
Co-Authored-By: Jake Moran <jake.moran@heygen.com>
* refactor(registry): host carousel block images on the CDN
The registry is served straight out of this repository
(DEFAULT_REGISTRY_URL points at raw.githubusercontent.com), so every byte a
block ships is permanent history. The 25 carousel blocks added 421 JPEGs,
27.7 MiB in a checkout, and made the diff 472 files. Only 23 of those images
were distinct: the same 12-24 placeholders were copied into every block.
files[] entries gain an optional `url`. When set, the installer fetches the
bytes from there instead of joining the registry base. `path` does not change
and still says where the file lands relative to the item, so composition HTML,
target mirroring and `hyperframes add` behave exactly as before.
Keys are content-addressed, so the 396 manifest entries resolve to 23 objects,
and a changed image gets a new URL rather than a stale one cached behind
`immutable, max-age=31536000`.
The catalog preview renderer copies an item's directory and renders it, so it
needs the same materialisation step. Without it the preview draws every card
blank and reports success, which is worse than failing.
Also drops registry/catalog/, 25 hand-made thumbnails referenced by nothing;
catalog previews are rendered by CI and served from docs/images/catalog.
Verified: all 23 objects return 200 from the CDN with hashes matching their
keys; `hyperframes add carousel-orbit-1` against a local registry installs 24
real JPEGs; the preview render produces the album art, and produces blank
cards when the fetch step is removed.
* style(registry): format the carousel composition HTML
`oxfmt --check .` covers the whole tree, and these 25 files were never run
through it. The pre-commit hook only formats staged files, so nothing local
caught it.
* feat(catalog): publish the carousel blocks without republishing their images
These 25 blocks had no Catalog page. Every other item in the registry has one,
so they shipped invisible: installable by name, unfindable by browsing.
Generating them naively undid the change they were added by. The Catalog
payload copies an item's assets into docs/public/, which is tracked, so the 396
images this PR just removed came back as 43 MB one directory over — worse than
the 3 MB they started as, because each block got its own copy.
The copy exists because these compositions assemble `img.src` at run time out
of a variable value, so there is no `src="..."` in the markup for the payload's
asset scan to resolve. An unpredictable path can only be satisfied by serving
every file beside it, which is what `needsOwnDirectory` asks for.
An absolute URL needs no directory: the scan already skips any `https:`
reference. So for the payload path only, hosted files are left undownloaded and
the composition's variable defaults are rewritten to their URLs. The preview
renderer still downloads them, because it paints real frames and a missing file
is a blank card.
The explorer posts every value to the preview frame on mount, including
untouched ones, so the page's variable list carries the URLs too. Left as local
paths they would have overridden the payload's own defaults and asked the frame
for a file that was deliberately never published.
Result: 25 pages, 25 payloads, zero bytes of image added.
Verified: a spike item declaring no assets at all rendered its 24 covers from
the CDN, proving the variable-default path; payload generation for a carousel
block now writes no item directory and no shared asset; the preview render
still produces the album art. mint validate and mint broken-links pass on the
new pages. test:scripts is green.
* refactor(catalog): split the hosted-asset step out of prepareProjectDir
Two functions rather than one: finding the composition and rewriting its
variable defaults are separate jobs, and inlining the mode branch pushed
prepareProjectDir past the complexity gate it was already sitting on.
Behaviour is unchanged. Re-verified both paths after the split: the payload for
a carousel block still writes no item directory and no shared asset, and the
preview render still produces the album art.
* feat(catalog): give the carousels their own shelf
25 image carousels landed in Showcases and were 53% of it, so the scenes that
shelf exists for disappeared underneath them. That is the same shape the 24
editor themes made, and it gets the same fix they got.
Keyed on the first tag, which is this file's stated grouping rule, rather than
on the name. `screen-flow-carousel` leads with `product-demo` and stays on the
shelf that says what it is for; a future carousel that is not named
`carousel-*` still lands here.
Showcases 47 -> 22, Carousels 25, and no existing item changed shelf.
* fix(registry): centre the circle-5 carousel path in its composition
Its ring was centred at x=3832.6 in a 3840-wide composition, so it sat on the
right edge and most of it fell outside the frame. Only a few cards were ever
visible, cropped, with two thirds of the composition empty.
The exported path carried absolute coordinates from a layout that was never
recentred. Shifting the four vertices by (-1912.597, -4.340) puts the ring on
the composition centre. Handles are relative, so only the anchor points move
and the shape is unchanged.
carousel-text-circle-5 shares the identical path and had the identical fault.
The other three circle variants sit within 12% of centre, which reads as
authored placement rather than the same bug, so they are left alone.
* fix(catalog): rebuild the circle-5 payloads after recentring the path
The Catalog preview plays the payload, not the composition on disk, so
recentring the source changed nothing a reader sees. The payload still carried
the old vertices and the ring still hung off the right edge of the frame.
Verified the consumer this time, not just the producer: both payloads now
resolve to a path centre of x=1920. The other 23 rebuild byte-identical, so the
formatting pass did not reach them.
* feat(catalog): promote Carousels to its own section
It was a shelf inside Scenes & demos, which is where a scene type belongs by
kind but not by weight. At 25 items it is larger than Data & charts (17) and
Blocks (13), each of which is already a section holding a single shelf, so the
catalog's own precedent puts it one level up.
Pulling it out also takes the largest section in the catalog from 120 items to
95, which is the reason the shelf was added in the first place.
The two circle-5 pages change because their embedded source block carries the
recentred path; nothing else in them moved.
---------
Co-authored-by: Jake Moran <jake.moran@heygen.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
* docs(changelog): weekly digest 2026-08-31–2026-09-07
Rewritten from the 111 non-merge commits in the window, grouped by theme.
The week was mostly hardening: file reads pinned to checked descriptors,
bounded parser scans, and isolated concurrent writes. Studio agent control
and site capture continued.
Every commit sha and PR number was resolved and verified against the repo,
not hand-typed.
— Rames
* docs(changelog): add weekly changelog video for 2026-08-31–2026-09-07
Embeds the rendered 49.5s digest video. Uploaded to
static.heygen.ai/hyperframes/changelog-videos/weekly-changelog-aug31-sep07.mp4
and verified live before committing the src (200, content-length matches the
object, cache miss).
— Rames Jusso
* Add Mini Course Generator to adopters list
Added a new card for the Mini Course Generator showcasing its AI-powered course generation capabilities.
* docs(adopters): add Mini Course Generator
---------
Co-authored-by: eren-commits <eren@minicoursegenerator.com>
Bare `hyperframes skills` passed `--skill '*'` to the upstream installer, which discovers every SKILL.md in the clone, so a full install put 26 skills on disk against the 20 the manifest publishes (the six repo-internal ones under .claude/skills / .agents/skills). It now resolves the canonical manifest and installs its names, the same path `skills update` uses; offline it warns and degrades to the pinned core set instead of the wildcard. Docs point the full set at `npx hyperframes skills` and say what `skills add --all` really pulls.
Rebuilding footage means checking the render against the source at the same timestamps. `--against <video>` saves the reference frame at every captured time (frame-exact seek: -ss after -i) plus a labelled render|reference pair sheet built with the existing contact-sheet helper. Reference frames and pair sheets are derived artifacts, kept out of the snapshot list like contact-sheet.jpg. No new dependencies.
Capture drops assets for four reasons and reported none of them, so a folder
with thirty images and a folder truncated to thirty images were the same
object. Every drop site was a bare `continue`, `break`, `return null` or an
empty `catch`, and the only signals downstream were two hand-written warning
strings that fired when the budget was already gone before a download pass
started, which is the one case where the pass could not say how much it lost.
`downloadAssets` and `downloadAndRewriteFonts` now return an `AssetDropCounts`
tally beside their result, incremented at the single line that performs each
drop. `capture --json` carries it as `dropped`; the human summary prints a
`Dropped:` line when it is non-zero.
Four reasons, three decisions and one failure:
size-floor fetched, then judged too small to be a real asset
budget-exhausted the post-navigation clock ran out before this one
cap-reached 30 inline SVGs, 30 fonts, or 6 faces per family
unavailable the request or the write failed
A break now counts everything it did not reach rather than the one it stopped
on, because "how many did we lose" is the question and one is never the answer.
The two budget warnings are gone. Both existed only to cover the case where
the budget ran out before a pass was called, so both passes are now called
unconditionally: a zero budget makes each loop break on its first item and
record `budget-exhausted` for the rest, which costs no network and produces a
real number instead of the word "some". The single remaining warning is derived
from the tally, so the prose and the count cannot disagree.
Measured on a live capture of a large marketing site:
default budget 232 kept, 91 dropped (39 size-floor, 20 cap-reached,
32 unavailable)
15s budget 30 kept, 299 dropped (279 budget-exhausted, 20 cap-reached)
Same page, same command, and until now both runs described themselves the same
way.
* feat(registry): add kinetic center build component
* chore: regenerate search vectors for kinetic-center-build
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(registry): load all kinetic center build font weights
* chore(catalog): regenerate local search vectors
---------
Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>