Commit Graph
180 Commits
Author SHA1 Message Date
Miguel Ángel 5658f70065 fix(core): color grading applies to canvas compositions (#5860)
* fix(core): color grading applies to canvas compositions

data-color-grading on a <canvas> was ignored by the runtime and refused by
lint, the Studio panel and the CLI writer. One source-tag list in the grading
contract now feeds all of them. A canvas is sized from its drawing buffer and
re-uploaded on every seek and transport tick.

* fix(core): keep the comment share of grading files at the base
2026-10-19 00:38:53 +00:00
WaterrrForeverandClaude Opus 5.5 cea8aa8a7d feat(cli): judge text behind a transparent video by its decoded alpha in check (#5657)
* feat(cli): judge text behind a transparent video by its decoded alpha in check

The layout audit treated every <video> as opaque over its whole box, so a
caption between footage and a remove-background cutout always failed
text_occluded and needed data-layout-allow-occlusion, which also hid a word
the subject's head fully covered.

check now draws the video's current (seeked) frame into one reused,
downscaled scratch canvas, once per sample and only for a video a text probe
lands under. A fully opaque frame keeps today's behaviour. A frame with
transparency is judged per rendered word against its alpha: partial cover is
an info finding carrying the most covered word's share, and a word 80% hidden
or two neighbouring words each half hidden is an error. A frame that cannot
be read falls back to opaque with a video_alpha_unread info note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cli): serve the video-alpha fixtures from memory, not request paths

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): check finds words under narrow, mirrored or backed cutouts

Review follow-ups on #5657:
- A matte the probe walk passes over, even at clear-alpha points, now triggers
  the per-word check, so a feature between two probe columns (an arm, a mic
  stand) still fails the word it hides.
- Mirrored videos and images map viewport points through the flip; a rotated,
  skewed or 3D-transformed video falls back to opaque with video_alpha_unread.
- A video with an opaque CSS background keeps its whole box occluding.
- cli.mdx lists every unread cause and why a fully transparent frame is one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 18:53:16 +00:00
Miguel Ángel 38cfcdd56c fix(engine): renders under WSL reach the GPU on Chrome 150 and later (#5591)
* fix(engine): renders under WSL reach the GPU on Chrome 150 and later

Part of #5548.

* fix(engine): shorten the WSL browser env comment

* fix(engine): cite the Chromium bug in one line and allow the fallback test's removal

* fix(engine): drop WAYLAND_DISPLAY under WSL only when that is what reaches the GPU

* fix(engine): restore the WSL note from before the full-Chrome fallback

* test(engine): give the second worker's page load room on a busy Windows runner

* fix(engine): keep the Chromium bug comment under the line limit
2026-10-14 06:22:39 +00:00
Miguel Ángel 9b996059af fix(engine): renders under WSL use the GPU through full Chrome when the headless shell cannot (#5574)
* fix(engine): renders under WSL use the GPU through full Chrome when the headless shell cannot

Part of #5548.

* fix(engine): a missing or unreadable full Chrome cache never stops a WSL render
2026-10-14 02:49:08 +00:00
Miguel Ángel 96e2154d7c fix(engine): say why WSL renders fall back to software and how to get the GPU (#5560)
* fix(engine): say why WSL renders fall back to software and how to get the GPU

Under WSL the GPU check often falls back to SwiftShader and the only output
was the probe line, with Docker passthrough advice for --browser-gpu. Print
one note that says what was checked (can this user open the render node),
the exact fix, and the native Windows alternative. The CLI's WSL detector
moves to the engine so both use one.

Part of #5548

* fix(engine): keep WSL GPU notes true to the probe result and render mode

The WSL note now prints only when auto mode really selects software, and
says whether the GPU check found no GPU or could not finish. Explicit
--browser-gpu keeps its unverified and Chrome-install advice and adds the
WSL checks. The render-node check verifies group membership before
suggesting usermod.
2026-10-13 22:18:59 +00:00
Abdullah M. Nazif 9e69821b8b docs: clarify async font measurement and timeline registration (#5203)
* docs: clarify async font measurement and timeline registration

* chore: align skill manifest with main before regeneration

* chore: regenerate animation skill manifest
2026-10-09 17:56:30 +00:00
Miguel Ángel cf1b04bb71 feat(cli): accept host-managed HeyGen access tokens (#5177)
* fix(auth): accept host-managed HeyGen access tokens

* fix(auth): share environment header validation

* docs(auth): condense credential resolver guidance

* fix(auth): report the account a browser login just signed in

Identify the account from the tokens the login issued, not a fresh credential
lookup that an environment token or API key outranks.

* docs(auth): list the host access token in the CLI credential order

* fix(cli): name a rejected host access token on publish

Publish told a host-token user to log in again, which cannot help while the token outranks the login.

* docs(auth): show the media credential order main resolves

* fix(cli): warn at logout that a host access token still signs commands

One map of environment credential variables, owned by the resolver, now feeds the logout warning and both publish messages.

* docs(auth): scope the media credential order to the audio scripts

* refactor(cli): read every env credential name from the resolver's map
2026-10-08 10:33:57 +00:00
Miguel Ángel 80c2547eac feat(cli): send missing features and host-app friction as ratingless feedback (#5140)
* docs(skills): ask agents inside a host app to report its friction as feedback

* feat(cli): send missing features and host-app friction as ratingless feedback

* fix(cli): keep the rating error, refuse blank reports and document ratingless feedback

* docs(skills): leave names and figures out of missing-feature reports

* docs(skills): keep anything private out of missing-feature reports
2026-10-07 06:40:37 +00:00
James RussoandClaude Opus 5.5 f74f260286 feat(core): bound imported scene timing to source intervals (#5132)
* feat(core): support deterministic async frame sources

* feat(core): bridge preserved film HTML to native capture

* feat(core): bound imported scene timing to source intervals

* test(producer): verify film bridge pixels in real browser

* fix(runtime): retain queued draws and disable unsafe dedup

* fix(runtime): fail promptly on runner reloads and send errors

* fix(runtime): align film draws with export windows

* docs(core): mark frame-source hosts with data-no-timeline

A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(core): require data-no-timeline on imported film hosts

Imported film wrappers have no GSAP timeline. Without data-no-timeline
on the scene hosts and root, lint fails with missing_timeline_registry
and each render waits 45 s for timeline registration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): avoid transport redraws after render seeks

* test: set explicit film fixture canvas background

* test: diagnose film capture timeout phase

* test: foreground film pages before screenshot capture

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:25:09 +00:00
James RussoandClaude Opus 5.5 891a18af76 feat(core): bridge preserved film HTML to native capture (#5131)
* feat(core): support deterministic async frame sources

* feat(core): bridge preserved film HTML to native capture

* fix(runtime): retain queued draws and disable unsafe dedup

* fix(runtime): fail promptly on runner reloads and send errors

* docs(core): mark frame-source hosts with data-no-timeline

A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(core): require data-no-timeline on imported film hosts

Imported film wrappers have no GSAP timeline. Without data-no-timeline
on the scene hosts and root, lint fails with missing_timeline_registry
and each render waits 45 s for timeline registration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): avoid transport redraws after render seeks

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:55:42 +00:00
James RussoandClaude Opus 5.5 a17d0bfc00 feat(core): support deterministic async frame sources (#5130)
* feat(core): support deterministic async frame sources

* fix(runtime): retain queued draws and disable unsafe dedup

* docs(core): mark frame-source hosts with data-no-timeline

A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): avoid transport redraws after render seeks

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 01:59:55 +00:00
Miguel Ángel 0ca69472d9 feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches (#4984)
* feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches

* refactor(cli): split the .env loader so each part stays simple

* fix(cli): a broken .env loader fails loudly; only a missing .env is ignored
2026-10-04 05:07:49 +00:00
jbernard077andMiguel Ángel 6f799aaa41 docs: align infinite-repeat guidance with bounded repeat: -1 lint (#3795)
Since v0.7.71 (#2763), gsap_infinite_repeat demotes to a warning when the
root declares a finite data-duration: deterministic seeking and export clip
to that explicit window. Without one it remains a hard error (unbounded
timeline duration breaks render planning).

Update every doc site that still teaches the old absolute ban — the core
determinism contract, the animation adapter notes, and the design-guide
tables/checklists — to state the bounded allowance and keep the finite-count
formula for loops that must end before the composition does.

skills/hyperframes-animation/rules/ambient-glow-bloom.md is intentionally
untouched: its shimmer must stop before the composition ends, which is
exactly the case where a computed finite count is still required.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 05:59:04 -07:00
37f30b1d06 feat: keep a video's sound on the clip by default (1/8) (#4813)
* 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>
2026-10-01 23:56:36 +00:00
Miguel Ángel fa044a89e5 feat(cli): tag telemetry and feedback with the app that launched the CLI (#4632)
* feat(cli): tag telemetry and feedback with the app that launched the CLI

* fix(cli): keep the client tag to a short slug and correct the telemetry docs

* test(cli): read the delivered batch through the shared helper

* refactor(cli): let the tag pattern alone bound the client tag's length

* docs(cli): describe the client tag generically as the launching app's tag

* style(cli): format the client tag test cases

* test(cli): use a neutral example value for the client tag

* chore(skills): regenerate the skills manifest for the telemetry reference
2026-09-28 05:11:11 +00:00
James Russo 0a5e3c7fc4 feat(plugins): ship portable agent bundles and installation guides (#4608)
* 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
2026-09-27 21:58:51 +00:00
WaterrrForeverandClaude Opus 5 480beb62bd docs: state the 45 s cost of a missing data-no-timeline (#4402)
The attribute is in the HTML schema reference and enforced by the missing_data_no_timeline lint rule, but no page said what omitting it costs: a composition that never registers a GSAP timeline waits out player-ready-timeout (45 s by default) on every render.

That matters for triage because it is a fixed cost. "The render is slow" currently lists only causes that scale with the project - draft quality, media size, filter complexity - so an author whose renders are uniformly 45 s long reads the section, tunes the things it names, and sees no change.

- troubleshooting: add a symptom-keyed entry above "The render is slow", and point that section at it for the constant-penalty case.
- html-schema: give the attribute row the consequence and the lint code.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 02:29:25 +08:00
James Russo 01601d1105 feat: add Gemini TTS models and service-account auth to media skills (#4377)
* feat: add opt-in Gemini 3.8 TTS to media skills

* feat: support Gemini service accounts and older TTS models

* fix(audio): isolate Gemini auth and narrow OAuth scope

* fix(audio): pin Gemini Python helpers to UTF-8
2026-09-24 02:21:23 +01:00
Vance IngallsandClaude Sonnet 5 b32b9a7dab feat(runtime): data-vfx-chain — a seek-deterministic per-pixel effect element (#4306)
* feat(core): data-vfx-chain defs and chain parsing (mirrors data-fx-chain)

Mirrors audioFx.ts's shape for the per-pixel VFX chain: a versioned
JSON chain attribute (data-vfx-chain), a def registry with typed
params (number/enum/bool/ref), and parse/serialize/normalize helpers.
HF_VFX holds one def, fractal-noise, with a placeholder frag; the
kernel and its consumers (packages/core/src/runtime/vfx.ts) land in
Task 1.2/1.3 of the vfx-chain-runtime plan, so vfx.ts is added to
.fallowrc.jsonc's entry list for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(runtime): paint data-vfx-chain canvases on every seek

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(vfx): fractal-noise kernel (Basic/fBm approximation) with CPU reference

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(vfx): self capture through layoutsubtree with the page-composite readiness protocol

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(vfx): wave-warp kernel (Sine)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(vfx): displacement-map kernel (self-referential map)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(vfx): chain order is nesting order; preceding CSS effects are captured

...and following ones apply outside. The plan's one-line message for Task 2.4
is 105 characters; commitlint caps the header at 100, so the tail moved here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(vfx): seek determinism and CPU-reference PSNR for fractal-noise

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(vfx): size the output canvas from the host's layout box, not its transformed AABB

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(telemetry): vfx node count, capture class and types on render_complete

Threads vfxNodeCount, vfxCapture and vfxTypes from a static scan of the
compiled composition's data-vfx-chain hosts (mirrors the existing
compositionElementCount/colorGradingCount scan) through
RenderPerfSummary.drawElement into the render_complete event, next to
de_capture_mode. Exports chainCapture/parseVfxChain/HfVfxCapture from
@hyperframes/core's index for the producer to consume. The PostHog
dashboard tile is not created here — left for the coordinator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(vfx): data-vfx-chain guide — element shape, capture table, Studio flag, known constraints

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(vfx): register chain hosts that arrive with a sub-composition mount

`initVfx(document.body, fps)` ran once, synchronously, three lines after
`void loadExternalCompositions(...)` — so every `data-vfx-chain` host that
enters the DOM only when a `data-composition-src` mount resolves was never
registered. An unregistered host paints nothing and reports nothing: no
`[HyperFrames] composition script error`, the `.hf-vfx-out` canvas keeps its
300x150 default, and `paintVfx` never even arms the page-composite flag.
Measured on a real multi-file export served raw: `#main-l5-text`'s output
canvas stayed 300x150 and `window.__timelines` never gained the sub-comp.

Re-scan in the loader's `.finally`, before `maybePublishRenderReady()`, so
registration precedes readiness. `initVfx` now releases the outgoing
registry's programs, capture textures and ping-pong targets first — the
output canvas hands back the same WebGL2 context on a re-scan, so the
previous pass's GL objects would otherwise leak.

Two browser cases in `vfxDeterminism.test.ts`, both against the real runtime
bundle: a host whose `.hf-vfx-in` IS the mount (already worked — the mount
replaces the wrapper's children, not the wrapper, so the registered source
node survives), and a host that arrives INSIDE a mounted sub-composition
(failed with `armed: false` and no error; passes now).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(vfx): runtime tsconfig typecheck passes

Under noUncheckedIndexedAccess, resolveCaptureSource's null return
widened VfxEntry.src past its VfxCaptureSource | undefined type; the
function is internal-only, so its return type moves to
VfxCaptureSource | undefined instead. Indexing PingPong.framebuffers
by a non-literal index produced | undefined against
WebGLFramebuffer | null, coalesced to null. No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(vfx): structural DOM predicates instead of realm-bound instanceof

vfx.ts checked `instanceof HTMLCanvasElement`/`HTMLElement` directly, which
returns false for inner-realm nodes when the Studio previews across an
iframe realm. domRealm.ts had no canvas predicate yet, so this adds
isCanvasElement mirroring the existing isImageElement pattern, then swaps
all three vfx.ts checks to the structural predicates the lint guard requires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(vfx): paint self-capture hosts under hyperframes snapshot, not only the engine path

`paintVfx(t, { engineMode: true })` armed the page-composite protocol and
returned, betting that every capturing chain runs under `frameCapture.ts`.
`hyperframes snapshot` (and check/compare/validate/layout, and Studio's
thumbnail capture) seek through the same `renderSeek`, never read
`__hf_page_composite_pending` and never call `__hf_page_composite_resolve` —
so a `self` chain painted nothing there, in silence, because its content
lives in a `layoutsubtree` canvas the browser never paints directly.

Engine mode now arms the preview-side capture as well and paints on whichever
completes first. A seek token stops a superseded async capture from repainting
a newer frame, and a registry-identity check stops it painting an entry
`initVfx` already released. `resolveVfxCapture` stays non-idempotent on
purpose: the engine resolves only after its own before-capture hooks have
mutated the DOM, so its capture is still the authoritative one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(vfx): an exporter-emitted .hf-vfx-out gets the box the spec fixes for it

findOrCreateOut adopted a canvas the exporter had already emitted without
applying the layout cssText, so it kept the default position:static;
display:inline and wrapped to the line AFTER the layer-wide .hf-vfx-src
canvas — painting a second, misplaced copy of the layer one full layer
height below the host. Measured on retro-wave #main-l6-text: .hf-vfx-src
rect top 315.1 h 93.0, .hf-vfx-out rect top 409.2, both style=null,
position static (ae-mcp findings Task 3.5b). Hosts with no emitted
out-canvas were never affected, which is why only this one host moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(telemetry): vfx_node_count counts hosts, not chain nodes — rename to vfx_host_count

The field increments once per `data-vfx-chain` attribute occurrence, never
per node inside a chain's JSON, so the name promised something the scan does
not measure. Renames the internal `vfxNodeCount` and the wire property
`vfx_node_count` to `vfxHostCount`/`vfx_host_count` across the orchestrator
scan, the perf summary, the CLI render path and the telemetry event. No
dashboard consumes the field yet, so the wire rename costs nothing now and
would cost a migration later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(vfx): join the preview capture to the seek-completion barrier, not race it

`paintVfx` fired `capturePreviewThenPaint` and forgot it, so nothing the CLI
awaits waited for a `self` chain's paint. Every snapshot-family command
(`snapshot`, `check`, `compare`, `validate`, `layout`) seeks through
`seekCompositionTimeline`, which drains `window.__hfWaitForSeekCompletion`
and then allows one settle race before screenshotting — and only the
`"hf-seek"` event's `waitUntil()` could put anything in that barrier. The
screenshot therefore landed on however many hosts happened to have finished,
which varied with host count and machine speed.

Extracts the "track, untrack on settle, retain the first rejection" block
`dispatch()` built inline into an exported `registerSeekCompletion`, and has
`paintVfx` register the preview capture through it. Also awaits the per-host
paints with `Promise.all` instead of sequentially: N hosts cost about two
animation frames now, not 2N.

Measured on the pre-fix bundle with two `self` hosts, both output canvases
read `[0,0,0,0]` at the moment the barrier returned; they painted only
because the settle race that follows happened to be long enough. Both tests
added here fail on that bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(vfx): report WebGL context loss loudly instead of silently no-op painting

One WebGL2 context per `data-vfx-chain` host, against a browser cap that is
commonly 16 and shared with `colorGrading.ts`'s per-element contexts. A lost
context makes every later call on it do nothing, per spec — so the chain kept
"painting" a blank canvas with no error, which is exactly what the module's
loud-failure contract exists to prevent.

Adds the `webglcontextlost` listener pair's first half, following
`colorGrading.ts`'s precedent: `preventDefault()`, mark the entry, report
once through `reportVfxError`. The entry is then skipped by `paintVfx`,
`capturePreviewThenPaint` and `resolveVfxCapture` — silently, since the one
loud report already happened and repeating it per frame is spam.

Restore is left unhandled on purpose, with the reasoning in the doc comment:
`initVfx`'s existing re-scan already rebuilds every GL object, and a second
narrower rebuild path for `webglcontextrestored` would duplicate it. The
shared-context redesign the review floated is likewise out of scope here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* perf(vfx): cache uniform locations at pass-build time instead of querying every paint

`setPassUniforms` asked the driver for `u_size`, `u_t`, `u_fps` and every
param's location on every paint of every pass, and `bindPass` did the same
for `u_src` on every bind. `buildPasses` already constructs each `VfxPass`
once at init, right after the program links, and a linked program's
locations never change — so they are resolved there now and read from
`pass.locations` in the hot path.

Same values, same `null`-means-optimised-out semantics (`gl.uniform*(null,
…)` is a defined no-op, which is what the per-frame query produced too), so
the existing vfx.test.ts and the browser determinism suite cover it
unchanged — both still green, the latter against a rebuilt runtime bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(vfx): bound the paint wait per host so a stalled compositor can't hang the seek barrier

`paintVfx` registers `capturePreviewThenPaint` into the shared seek-completion
barrier (48aaa2bbb), and that promise settled only via `awaitCanvasPaint` — a
race between the canvas `paint` event and a double-rAF chain. Under BeginFrame
control (Linux headless-shell, `drawelement` capture) neither can fire before
the barrier is drained: `frameCapture.ts` awaits the barrier in
`prepareFrameForCapture` (~2692), while the frame's
`HeadlessExperimental.beginFrame` is issued later, in the per-frame capture
stage (~3797). Fire-and-forget that was harmless; registered it can stall every
`__hfWaitForSeekCompletion` caller, the engine's render path included.

rAF-under-BeginFrame: CONFIRMED gated, from this repo's own engine notes —
`frameCapture.ts` ~2445 ("waitForFunction uses rAF polling internally, which
won't fire in beginFrame mode") and ~4755 ("headless only fires rAF when a
frame is produced, and nothing produces one until a screenshot asks"). Not
reproduced locally: every environment here is macOS with a free-running
compositor (`beginFrameTimeTicks === 0`). `setTimeout` is not compositor-gated
there — `drawElementService.ts` ~341 describes its 250 ms paint fallback as
burning on every BeginFrame frame, which requires the timer to fire — so a
timer ceiling is a sound escape.

`awaitCanvasPaint` now resolves `"painted" | "timeout"` with a 2000 ms ceiling
(the repo's own `BEGINFRAME_PROBE_TIMEOUT_MS`; ~20x a real two-frame paint,
far short of the ~180 s CDP `protocolTimeout` a hang would burn). The race is
per host, so one stalled compositor no longer delays hosts that would have
painted; a timed-out host is skipped (no capture, no guessed pixels) and
reported loudly. The report is not suppressed in engine mode: `frameCapture.ts`
~2708 skips `__hf_page_composite_resolve` in `drawelement`/`beginframe` modes,
so there is no authoritative second attempt to defer to there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(ci): re-pin test-classification.mjs hash after registering vfxDeterminism.test.ts

An earlier commit added vfxDeterminism.test.ts to INTEGRATION_TEST_FILES;
check-test-reachability.mjs pins a sha256 of that file to force a conscious
review whenever it changes. Reviewed: runner[1]'s glob already covers the
file structurally, the classification move (unit -> integration) is correct
for a Chrome-launching test, and the pinned hash is now current.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(core): regenerate position-edits-render-inline.ts for Linux esbuild output

check:position-edits-render (bun run build:position-edits-render && git diff
--exit-code) failed on both Linux CI runners (Test (runtime), Tests on
windows-latest: studio-core) but passed on macOS: esbuild's minifier named
two identically-scoped WeakMap/function bindings differently on Linux than
on the mac that last committed this artifact, and the divergence appeared
only on this branch (main's own current tip passes CI cleanly), so it is
tied to a change on this branch rather than a random flake, even though it
is not attributable to any specific line of that change.

Confirmed stable across two independent Linux CI runs (byte-identical diff
both times) and reconstructed directly from that diff rather than guessed:
the committed content, byte length (3817), and 3/3 existing artifact tests
all match. A future contributor rebuilding on macOS will see this same
diff again in the other direction until the build script's cross-platform
determinism is fixed at the esbuild-invocation level, which is out of this
PR's scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 03:14:08 -07:00
James RussoandClaude Opus 5.5 ec3bc93b90 docs: alphabetize supported coding agents (#4297)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 22:21:34 +00:00
Brun3y 105984cac4 feat(cli): add IBM Bob runtime support (#3930) 2026-09-22 22:01:42 +00:00
Miguel Ángel a25da94b77 refactor(studio): remove the storyboard view and its Storyboard/Preview switch (#4140)
* refactor(studio): remove the storyboard view and its Storyboard/Preview switch

* refactor(studio): fold the thumbnail output branches left after the storyboard mode went

* docs(studio): drop leftover storyboard references after the view was removed

* ci: allow the storyboard view deletions in the no-deletions guard

* docs(studio): wrap an over-long comment line in the caption overlay helper

* style(studio): reindent App.tsx after the view-mode wrapper was removed
2026-09-18 23:47:12 -04:00
Aditya Poudyal e4b1832fad feat(cli): add hls output format (#4022) 2026-09-16 21:09:14 -04:00
James Russo 8820a01090 fix(skills): exclude repo-native skills from public installs (#3856) 2026-09-10 12:35:29 -07:00
Vance Ingalls 78e84a9ca8 docs(render): explain alpha-aware WebM inspection 2026-09-09 00:00:24 -07:00
Miguel Ángel 2f7b7a9529 fix(render): reject unsupported ProRes rate controls (#3702) 2026-09-08 18:09:19 +00:00
Kakumanu Ashok ReddyandashokDevs 250afc313b docs: fix broken Claude Design guide link (#3584)
Co-authored-by: ashokDevs <bhargavcodes3@gmail.com>
2026-09-08 17:56:48 +00:00
WaterrrForever c8300c69a1 fix(cli): install the published skill set, not every SKILL.md in the repo (#3636)
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.
2026-09-04 23:25:43 +08:00
Miguel Ángel 6b5b4cb988 feat(studio): make agent edits live and explicit (#3581) 2026-09-02 01:11:13 -04:00
2d6b055f31 feat(studio): let an agent author motion (#3520)
* feat(studio): let an agent drive Studio's selection and playhead

Adds `studio_select` and `studio_seek`, so an agent and the human are looking
at the same element and the same instant. Selecting reveals the inspector,
exactly as a click does, which is what makes the agent's move visible.

Selection is shared state, not a per-call argument, and that is forced rather
than chosen. Most of Studio's edit handlers read the ambient React selection,
and `applyDomSelection` only schedules a state update, so selecting and
committing inside ONE call would write to whatever was selected before. Two
tool calls are separated by a render, so the contract is select first, then
act. That is also how a human works: click, then type.

`studio_seek` uses `requestSeek`, not `setCurrentTime`. The latter only moves
the timeline's displayed number and leaves the composition where it was.

Two things the tools refuse to fake:

Seek does not clamp. `seek()` already clamps against the adapter's duration,
which can differ from the store's, and clamping again would give that
invariant two owners that can disagree. The tool reports where the playhead
actually landed instead, read back afterwards.

`requestSeek` is fire-and-forget, so it cannot report that no adapter was
mounted to receive it. The tool compares the playhead before and after and
fails rather than claiming a seek that never happened.

Select separates three failures that a single message would have merged: the
preview is not mounted yet (wait), no element matches the handle (re-read),
and the element cannot be selected (try a neighbour). The agent's next move
differs for each, so collapsing them would cost it a round trip or a retry
loop.

* feat(studio): give an agent eyes with studio_frame

Renders the composition to a PNG at a given time and returns the URL. This is
what turns the tool set from a remote control into a loop: author a change,
capture the instant it affects, look, adjust. No agent can judge motion from
source, because "what does this look like at 2.4 seconds" is not a question a
file answers.

Reuses Studio's existing capture endpoint via `buildFrameCaptureUrl` rather
than inventing a second one.

Two things this does not fake:

It reports the time the playhead LANDED on, not the time requested. The player
clamps, so those differ at the ends, and attaching the wrong time to a frame is
how an agent draws a confident wrong conclusion about motion.

It waits before capturing, by default 150ms. The frame is rendered from the
file on disk, and the render cache is cleared by a file watcher with a 40ms
write-stability threshold, so a capture that beats the watcher renders the
PRE-edit composition. That exact staleness was a real bug here once. An agent
reading a stale frame as "my edit failed" would thrash, so the wait is on by
default, `settleMs` makes it tunable, and the tool description names the
failure rather than leaving it to be rediscovered.

It probes with HEAD before returning, so a URL that 404s comes back as a
failure with a hint instead of as a link the agent cannot render.

* feat(studio): add studio_inspect, so an agent reads before it writes

Everything about one element in one call: resolved styles, text fields, box,
data attributes, GSAP animations, and what the element will and will not
accept.

The point is to prevent a failed write rather than to satisfy curiosity.
`can.reasonIfDisabled` is passed through verbatim from Studio's own
capabilities, so an agent that reads first should never attempt an edit the
element would refuse.

Three things it refuses to get wrong:

Animations are reported ONLY for the current selection, because that is the
only element Studio parses them for. Attributing them to any other element
would be reporting the wrong element's motion, which is worse than reporting
none. When a handle names something else the field is empty and
`animationEditingBlocked` says why.

`animationEditingBlocked` also carries the two states where animation editing
is off entirely, multiple timelines and an unsupported timeline pattern. Both
live on the selection context. Learning them from a read costs one call;
learning them from a failed write costs a retry loop.

Inspecting a handle does NOT change what is selected. It is a read, and
stealing the human's selection would be a side effect they did not ask for.
There is a test asserting `applySelection` is never called.

Nothing selected and no handle given is a failure, not an empty result. An
empty result would assert "this element has nothing", which is a different and
false claim.

* feat(studio): let an agent edit text and styles, guarded

The first tools that change the composition. Both act on the current
selection and take no handle, which is forced rather than chosen: the
handlers read the ambient React selection, and `applyDomSelection` only
schedules a state update, so selecting and committing inside one call would
write to whatever was selected before. Select first, then edit.

Also plumbs the write-blocked state, which was the blocker for shipping any
write at all. `domEditSaveQueuePaused` and the external-file conflict both
lived on App and were unreachable from the tool surface, so `canWrite` was
optimistic and a comment said so. They now derive into a single
`writeBlockedReason` on the shell context: one field, one owner, conflict
taking precedence because resolving it is what unblocks the queue.

That guard matters more than it looks. Both states are BANNERS in Studio with
no lock behind them, so nothing else was stopping a programmatic write from
landing on top of a conflict the user had been asked to adjudicate.

Three things the tools refuse to fake:

They check the outcome, not the absence of a throw. Studio has several paths
where a failed commit resolves anyway, so awaiting the handler proves nothing.
The tagged outcome added earlier is what proves the write landed.

A partial style result is reported as partial. `handleDomStyleCommit` is one
property per call, so N properties are N commits; the result carries `applied`
and `rejected` maps rather than a single boolean that would have to pick a
side.

Style commits run sequentially, never concurrently. Two commits racing through
Studio's client-side read-modify-write can record undo entries that both claim
the same starting content. There is a test that measures concurrency rather
than trusting the loop.

Every decline reason maps to a hint naming what to do instead, so a refusal
routes the agent rather than just stopping it.

* feat(studio): move, resize and rotate, verified by reading back

`studio_transform` does what a drag does, and then checks. The box in the
result is READ BACK after the write, never echoed from the request, and
`applied` lists what actually took effect.

That is not belt-and-braces. The plan for this unit said to re-derive the
geometry handlers' behaviour rather than trust any description of them, and
doing that turned up three different behaviours behind one interface.

The handlers on `DomEditActionsValue` are the GSAP-AWARE wrappers, aliased in
`useDomEditSession.ts:534-538`, not the CSS ones in `useDomGeometryCommits.ts`
that an earlier note in this workstream described.

`handleGsapAwarePathOffsetCommit` and `handleGsapAwareRotationCommit` are
`if (gsapCommitMutation) { ...intercept... }` with no else branch. Their own
comments say the absence is deliberate: position and rotation are written as
GSAP code and there is no CSS fallback to write to. So they can return having
done nothing.

`handleGsapAwareBoxSizeCommit` is not like the other two. It runs through
`runGestureTransaction` with separate scale and width/height routes, so resize
works more generally.

Reading back is what turns that middle case from a silent lie into a reported
one. A move that did nothing comes back in `unchanged` with a reason.

Three smaller decisions:

Operations re-read between each other, so a move is judged against the box
AFTER a resize in the same call. Comparing against the original would credit
the resize's change to the move.

Rotation is reported as dispatched, not verified. `rotate` is an individual
transform property and does not appear in the computed transform, so there is
no honest box-derived signal, and claiming one would be worse than saying so.

x pairs with y and width pairs with height. Accepting one alone would mean
inventing the other from the current value, which moves the element somewhere
the caller did not ask for. The pairing rule and its minimum live in one
`parsePair` helper rather than as four separate branches.

* feat(studio): let an agent author motion

Four tools: add an animation, change its duration/ease/position, add a
keyframe, delete it. This is the capability that makes the tool set worth
having, because motion is the one thing an agent cannot judge or author from
source.

These are deliberately less confident than the rest of the set, and the
reason is the handlers underneath them:

`handleGsapAddAnimation(method)` takes only a method. Its insert position
comes from the live playhead, not the caller, and the call is `void ...catch()`
so it returns nothing.

`handleGsapAddKeyframeBatch` returns a promise but catches its own failure, so
awaiting proves the call finished, not that it landed.

`handleGsapDeleteAnimation` discards its promise entirely.

`handleGsapUpdateMeta` is the one honest signal. It returns a boolean.

U8 handled the same problem by reading the result back. That does not work
here: the animation list comes from React state that only refreshes on a
render, and no render happens inside one tool call. Rather than fake a
verification with a frame-timer, these report what was DISPATCHED and the
descriptions tell the agent to call studio_inspect to see the result. Saying
"I asked for this" is honest; saying "this happened" would not be.

Three consequences worth stating:

`studio_add_animation` takes no position. The handler reads the playhead, so
accepting one would report a number that had no effect. It reports where the
playhead actually was and tells the agent to seek first.

`studio_update_animation` rules out the no-selection case BEFORE dispatch. The
handler answers `false` for both "nothing selected" and "the write failed", so
eliminating one is what makes the other legible.

Keyframe percent and properties are validated in the tool, because nothing in
the platform checks input against the declared schema.

* feat(studio): add studio_inspect, so an agent reads before it writes (#3517)

Everything about one element in one call: resolved styles, text fields, box,
data attributes, GSAP animations, and what the element will and will not
accept.

The point is to prevent a failed write rather than to satisfy curiosity.
`can.reasonIfDisabled` is passed through verbatim from Studio's own
capabilities, so an agent that reads first should never attempt an edit the
element would refuse.

Three things it refuses to get wrong:

Animations are reported ONLY for the current selection, because that is the
only element Studio parses them for. Attributing them to any other element
would be reporting the wrong element's motion, which is worse than reporting
none. When a handle names something else the field is empty and
`animationEditingBlocked` says why.

`animationEditingBlocked` also carries the two states where animation editing
is off entirely, multiple timelines and an unsupported timeline pattern. Both
live on the selection context. Learning them from a read costs one call;
learning them from a failed write costs a retry loop.

Inspecting a handle does NOT change what is selected. It is a read, and
stealing the human's selection would be a side effect they did not ask for.
There is a test asserting `applySelection` is never called.

Nothing selected and no handle given is a failure, not an empty result. An
empty result would assert "this element has nothing", which is a different and
false claim.

* feat(studio): move, resize and rotate, verified by reading back (#3519)

`studio_transform` does what a drag does, and then checks. The box in the
result is READ BACK after the write, never echoed from the request, and
`applied` lists what actually took effect.

That is not belt-and-braces. The plan for this unit said to re-derive the
geometry handlers' behaviour rather than trust any description of them, and
doing that turned up three different behaviours behind one interface.

The handlers on `DomEditActionsValue` are the GSAP-AWARE wrappers, aliased in
`useDomEditSession.ts:534-538`, not the CSS ones in `useDomGeometryCommits.ts`
that an earlier note in this workstream described.

`handleGsapAwarePathOffsetCommit` and `handleGsapAwareRotationCommit` are
`if (gsapCommitMutation) { ...intercept... }` with no else branch. Their own
comments say the absence is deliberate: position and rotation are written as
GSAP code and there is no CSS fallback to write to. So they can return having
done nothing.

`handleGsapAwareBoxSizeCommit` is not like the other two. It runs through
`runGestureTransaction` with separate scale and width/height routes, so resize
works more generally.

Reading back is what turns that middle case from a silent lie into a reported
one. A move that did nothing comes back in `unchanged` with a reason.

Three smaller decisions:

Operations re-read between each other, so a move is judged against the box
AFTER a resize in the same call. Comparing against the original would credit
the resize's change to the move.

Rotation is reported as dispatched, not verified. `rotate` is an individual
transform property and does not appear in the computed transform, so there is
no honest box-derived signal, and claiming one would be worse than saying so.

x pairs with y and width pairs with height. Accepting one alone would mean
inventing the other from the current value, which moves the element somewhere
the caller did not ask for. The pairing rule and its minimum live in one
`parsePair` helper rather than as four separate branches.

* docs: document Studio's WebMCP agent tools, proven end-to-end in a browser (#3521)

* docs: document Studio's WebMCP agent tools

Adds `guides/webmcp`, under Developers > Agent setup.

Its first job is to defuse a name collision. `guides/mcp` already exists and
covers HeyGen's HOSTED MCP connector, which builds a video from a chat. This
page is about an agent working inside Studio on a composition already open in
front of you. Different feature, confusingly similar name, so the page says
what it is not before it says what it is.

Written to DOCS_GUIDELINES: one-sentence intro, outcome before implementation,
real values rather than placeholders, and three callouts.

The three things a reader most needs are the ones easiest to get wrong:

The API is `document.modelContext`, not `navigator.modelContext`. Most
published examples use the second, which is a polyfill compatibility shim
rather than a spec member, so feature-detecting it misleads.

Select first, then edit. Most editing tools act on the current selection, and
an agent that skips it gets an error rather than a wrong-element write.

Leave Studio visible. Some of Studio's write paths report failure through a
toast rather than a return value, so the human is the one who sees it. That is
a real property of the co-pilot design, not a nicety, so the page says it
plainly.

Verified with `npx mint validate` and `npx mint broken-links --check-redirects`,
both passing.

* fix(studio): target the text field that exists, not one named self

Found by running the tools end to end in a browser, which is the only way it
could have been found: the unit tests mock `setText`, so they never crossed the
boundary where this breaks.

An element's text usually lives in a CHILD field, keyed like `self:0:h1` or
`child:0:h1`. `studio_set_text` passed no field key, so
`buildNextDomTextFields` planned zero operations, the request went out with an
empty patch, and the server answered:

  POST /api/projects/<id>/file-mutations/patch-element
  -> 400 {"error":"target and operations required"}

Which surfaced as `persist-failed`. The tool was telling the truth, so the
reporting work in the earlier PRs did its job, but the failure looked like a
server problem and was not.

The tool now resolves the field: the one the caller named, or the element's
single field when it has exactly one. An element with several fields is asked
to name one; an element with none is reported blocked. Naming a field the
element does not have is rejected with the list of the ones it does have,
rather than silently writing nowhere.

Four regression tests, including the exact `child:0:h1` shape that failed. One
existing assertion changed: it expected the field to be `undefined`, which is
precisely the bug, so it now expects the resolved key.

Also documents two things the browser run surfaced, both real and neither a
defect: registration is asynchronous, so a caller reading `getTools()` too
early sees a partial list; and the tools that act on the current selection need
a render between the select and the edit, which a real agent gets for free
because its calls arrive as separate messages.

* docs: give the agent-tools kill switch instructions that work

The page told readers to set agentToolsEnabled in Studio's preferences.
Nothing writes that flag: it is read in useStudioAgentTools and parsed in
studioUiPreferences, but there is no settings UI and no toggle, so the
instruction could not be followed. Replace it with the localStorage write
that actually flips it, and spell out the merge, since overwriting the key
drops every other stored preference.

* docs: do not promise a per-call permission prompt we have not verified

The page said the browser asks before any agent calls a tool. Prompt
granularity is browser-specific and unsettled during the origin trial, and
we have not observed it on the native path. Say what holds, that access is
gated, and name the part that is still moving.

* fix(studio): re-apply WebMCP test polyfill fix (#3532 regression)

The squash merge of #3518 re-introduced the old assertion that
document.modelContext is absent. The polyfill from #3514 installs it
as a fallback — that is expected behavior.

Same fix as #3532: remove the assertion, keep the boot-cleanly contract.

---------

Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-08-31 14:45:09 -04:00
Vance Ingalls 0eca7b1e0a docs(audio): document the audio effects system (#3420)
* docs(audio): document the audio effects system

The effects feature had no documentation at all — the only mentions anywhere in
docs/ were changelog entries. This adds the three pages the color-grading feature
already has, one per audience, and closes a gap in the existing audio guide.

- prompting/audio-effects — a new Level 5 chapter on asking for a mix in
  symptoms rather than in filters, with the voiceover carve as the headline, the
  level-before-depth check, groups, and the three requests that have no honest
  answer (de-essing, noise removal, tone matching).
- studio/audio-effects — the rack, presets by symptom, the carve module, groups,
  the deliberately asymmetric mute/solo, automation lanes, and troubleshooting.
- reference/audio-effects — the contract: all four attributes, every effect and
  parameter range, why some parameters cannot be automated, the 19 presets, the
  five jobs and five one-knob profiles, carve semantics, the group model, the
  render bus, preview/render parity, and the three lint rules.

Also points the existing "duck music under important speech" advice in
guides/voice-and-audio at the carve, which does that properly and was never
named there.

Facts verified against source rather than the shipped skill prose: the panel
section is "Audio FX" (the neighbouring "Effects" section is visual effects, and
an earlier draft of this page named the wrong one), MAX_AUTOMATION_POINTS is 512,
fromPreset carries a preset id rather than a boolean, the leveller targets the
track's own 80th percentile, group mute reaches the render while solo never
leaves Studio, and the CLI carve currently writes clip ids.

No screenshots in this pass, and no placeholders for them either — the pages
carry concrete markup and tables instead. Screenshots of the rack, the carve
module, and a group row would each earn their place later.

mint validate and mint broken-links both pass.

* docs(audio): give audio its own Studio group, split by task

Review feedback: the audio page did not belong in Studio / Edit. That group is
already one task per page — canvas, timeline, animation, captions — and a single
page covering the rack, the carve, groups, and automation lanes was four tasks
bundled together and dropped in beside them.

Studio now has an Audio group holding four task pages:

- Effects and presets — the rack, presets by symptom, adding single effects in a
  working order, the one-knob controls, Even Out Levels
- Voiceover carve — its own page, because it is the feature people come for
- Groups, mute, and solo — including why mute reaches the export and solo cannot
- Automation lanes — drawing envelopes, the shape menu, and which parameters
  cannot move at all

Not a new top-level tab: the tabs here are audience-scoped (Guides, Studio,
Catalog, Developers), so a feature tab would be the only one of its kind and
would strand the prompting chapter out of its Level 5 sequence and the reference
page out of Developers.

Repointed the deep link in guides/voice-and-audio at the carve's own page.

Lane interactions verified in source before documenting: the right-click menu
offers Ramp up, Ramp down, Swell, Dip, and Simplify (which needs three points).
Deliberately not documented: "clicking a lane label reveals it in the rack",
which is not on main.

mint validate and mint broken-links both pass.

* docs(audio): fix the four review blockers

All four verified in source before fixing; the review was right on every count.

**The registry was incomplete.** `pitchshift` ships at `audioFx.ts:509-535`
(`semitones` −12–12, `mix` 0–1, worklet-backed so neither automatable) and was
missing entirely — sixteen effects, not fifteen. It also joins the worklet list,
so five effects expose no automatable parameters rather than four.

**Three presets were missing.** `chipmunk`, `giant`, and `monster` ship at
`audioFxPresets.ts:336-357`, all built on `pitchshift`. Twenty-two presets, and
Character holds ten. Fixed in both the reference table and the Studio list.

**The copyable markup contradicted the warning above it.** The page said
`carve.mjs` only finds double-quoted attributes and then gave three
single-quoted examples — copying the chain example would make a later carve miss
the existing chain and overwrite it. All three are now double-quoted with
`&quot;`, each followed by its unescaped reading so it stays legible.

**The attribute table over-claimed.** `data-audio-group` is a plain id, not JSON,
and is ignored on `<video>`; the other three also live on `<hf-audio-group>` for
a group. The table now carries shape and valid host per attribute.

**The automation contract was wrong on two axes.** A clip lane's `t` is
clip-relative but a GROUP lane's is composition time, because a group has no
`data-start` (`webAudioTransport.ts:337-342`, `audioMixer.ts:1311-1344`) — both
pages now split the two clocks. And `volume` is not 0–1: the ceiling is
`MAX_AUDIO_GAIN`, +12 dB or about 3.981 (`audioGain.ts:8-9`), so a boosting lane
is valid and documented.

**Current-main drift.** #3416 is merged, so the CLI now records the voices'
shared group when it is safe and falls back to clip ids when that group contains
the bed or a music/SFX member. Documented, including why neither refusal shows
up on the run that writes it, and rebased onto main.

mint validate and mint broken-links both pass.

* docs(audio): name the real add-menu family, and finish propagating pitchshift

Second review round. All three findings were my own incomplete propagation — I
corrected the reference for `pitchshift` last round and left the reader-facing
pages behind it.

**The add-menu family is `Time`, not `Space`.** `propertyPanelFxAddMenu.tsx:22-28`
labels the four groups Filters / Dynamics / Non-linear / Time, and the time group
holds pitchshift, delay, chorus, phaser, and reverb. The Studio page sent readers
looking for a group that does not exist. It is now a table naming the family and
its contents, and the reference's "Time — space and width" heading is retitled,
since that description stopped covering the family the moment pitch shift joined
it.

Also from the same file: the menu offers the named jobs in place of a bare
`peaking`, because picking `peaking` is picking a machine and leaving the real
decision — which range — for afterwards. Worth saying on the task page.

**Pitch shift was missing from both no-automation lists** that a reader actually
follows — `studio/audio-automation` and, unflagged but the same defect, the
prompting chapter. Five worklet effects in all four places now. Called out
explicitly on the Studio page, because a rising pitch is exactly the thing
someone reaches for a lane to do, and the lane will not report that it cannot.

**Narrowed the group-metadata sentence.** "The other three are JSON, and on a
group they live on `<hf-audio-group>`" swept in `data-fx-carve`, contradicting the
table directly above it. Only `data-fx-chain` and `data-automation` are group
metadata.

mint validate and mint broken-links both pass.
2026-08-22 05:49:35 -07:00
Miguel Ángel 9ec75a485f docs: drop --full-depth from skills install commands (#3399)
* Update skills.mdx

* docs: drop --full-depth from skills install commands
2026-08-21 14:45:01 -04:00
James Russo f7d2260f9d feat(engine): stamp rendered files with hidden renderer provenance (#3264)
* feat(engine): stamp rendered files with hidden renderer provenance

* fix(engine,producer): re-assert provenance at every container writer

Review found that a no-audio MOV render still shipped untagged. The concat
step is the last container write on that path (mux is skipped without audio,
and applyFaststart only copies mov/webm), and the concat demuxer does not
carry the chunks' container metadata through.

The same hole applies to no-audio WebM, and to the in-process chunked encode
in chunkEncoder, not just the distributed assemble path. mp4 was masked
throughout because applyFaststart re-runs ffmpeg for that format and re-tagged
the output.

Tags the four remaining writers: the chunked-encode concat, and assemble's
single-chunk remux, concat and cfr re-encode.

Also corrects the trust claim. These are unsigned, freely writable keys, so a
present tag means the file claims to be HyperFrames output, not that
HyperFrames wrote it. Documented as an unauthenticated diagnostic hint rather
than an authenticity or attribution boundary.

Tests assert on the assembled file through the real assemble() path for both
mov and webm; both fail without the concat fix.

* test(engine): pin provenance through the in-process chunked concat

Review noted the distributed writers are mutation-pinned but the
encodeFramesChunkedConcat fix had no real-file regression of its own.

Encodes 70 frames at a 30-frame chunk size so the concat step actually runs,
then asserts the tags on the resulting no-audio mov. Fails without the concat
fix, passes with it.
2026-08-13 16:32:09 -07:00
James Russo 27d113e56c docs: add user interview booking links (#3059) 2026-08-05 12:35:58 -07:00
ukimsanov ad4c6a9b6e docs: light-theme videos on nine guides, plus two rewritten pages
**Nine guide videos rebuilt in the light house style.** They were dark — a reader
clicking between sibling guides watched the theme flip. mcp, performance,
rendering, skills, html-in-canvas and remove-background were restyled to the bone
field; faceless-explainer, product-launch and voice-and-audio were re-narrated
with River, the house voice the user-journey films use. Measured luma is 177-230,
matching the journey films, and every one carries audio.

The restyle also surfaced a real bug in the frame itself: the house coral
#D96A4F is 2.95:1 on bone, under the WCAG large-text floor, and `hyperframes
check` failed on it. Darkened to #B44E33 (4.43:1) before any of these rendered.

**canary-rollouts trimmed from 2,443 words to 520.** The statistical-calibration
essay — pre-registered experiment design, a probability formula, rejected
hardware-fingerprinting alternatives — was never documentation for someone
shipping a staged rollout. Cut to Add one / Override one / Remove one / Where it
lives, the part that was already good.

**thirty-days now shows the films.** It was 431 words and 30 links to x.com for a
series entirely about videos, with no thumbnails. It now leads with real preview
clips where a film exists and marks the rest with a visual placeholder rather
than a dead off-site link.
2026-08-04 16:09:33 -07:00
ukimsanov f742084fa8 docs: put the ten workflows in one place, and connect the two tracks
There are ten creation workflows. The Workflows nav group held eight of them plus
`guides/export-and-share`, which is not a workflow at all — there is no such
skill, it is a finishing task. The tenth, the Remotion port, sat under
Developers > Composition, design & animation, a different tab entirely.

So: export-and-share moved to Build the project where the other task pages live,
the Remotion page moved into Workflows, and the chooser gained a tenth route. The
chooser rendered `<video>` unconditionally, which would have drawn a black
rectangle for a route with no clip — it now renders a neutral tile instead, and
custom.css styles it to the same box.

**The Prompt Guide and the workflow guides did not know about each other.** Level 1
is organised one page per workflow, and five of its six pages linked to no guide
at all. A reader learning to prompt for motion graphics never discovered
/guides/motion-graphics, and the traffic went nowhere in both directions. Each
Level 1 page now names its workflow and says what that page adds.

Four links pointed at redirects rather than the real page — /guides/claude-design,
/guides/common-mistakes, /guides/website-to-video, /guides/media-overlays. Seven
files now link to the destination directly.

Checked after: 0 broken links, 0 links via redirect.
2026-08-04 15:32:14 -07:00
ukimsanov c669616370 docs: name the two workflows the captions page covers
There are ten creation workflows — confirmed against both
skills/hyperframes/references/routes/ and the skill directories, which agree.
Nine were findable. Two were not.

This page covers `/embedded-captions` and `/talking-head-recut` and named neither
of them, anywhere, once. It described the difference well — "add captions" versus
"add designed overlays" — but a reader who wanted to invoke one directly had no
way to learn what it was called, and neither name appears in the navigation.

Both are now named where the distinction is drawn, with what each actually does:
words on screen versus designed cards on screen, and the reminder that both leave
the footage untouched.

remotion-to-hyperframes checks out — it is named on three pages including the
Remotion comparison, so it was reachable already.
2026-08-04 15:24:43 -07:00
ukimsanov af5ebf09ce docs: four more the source contradicts, one of them mine
**`declareVariable`'s field is `declaration`, not `decl`.** Documented wrong in
three places across two SDK reference pages. This one fails *silently* — the
handler reads `op.declaration`, gets undefined, and returns EMPTY at
sdk/src/engine/mutate.ts:991. No throw, no patch, no error. A reader copies the
example and nothing happens. types.mdx opens by claiming it was verified against
types.ts, which is where the correct name is (line 134).

**Colour grading says eighteen presets; the Grade panel has thirteen.** Eighteen
is the combined total. The other five carry effects and live in the Effects
panel — colorGrading.ts derives GRADE_PRESETS by filtering EFFECT_PRESETS out,
and colorGrading.test.ts:25 asserts VHS Playback is *not* in Grade. I wrote that
sentence today and used VHS Playback as the example, so the page sent a reader to
Grade looking for something that is not there. The visual brief further down had
the same error baked in; fixed before anyone shoots it.

**`hyperframes render` on a slideshow does not fail — it lies.** The page said
export "is not currently supported", which reads like the command errors. The
skill is emphatic: a deck has no master root composition, so render resolves only
the first slide and writes a silently truncated MP4 — six seconds of a
forty-second deck, no warning. That is now a Warning saying exactly that.

I also swept every documented edit-operation field against sdk/src/types.ts.
Six looked wrong and five were my regex not stripping the `?` optional marker;
`easeEach` and `resolvedSelector` are real, at types.ts:178-179. One genuine
error out of that sweep, the one above.
2026-08-04 14:27:52 -07:00
ukimsanov 592301248e fix(scripts): drop the catalog poster instead of guarding it, and cut the encode pass down
**The poster guard I added twice was unworkable and I never ran it.** It called
existsSync on docs/images/catalog/<name>.png. That directory is gitignored —
previews are generated locally, uploaded to the CDN and never committed — so the
check is false on every clean checkout and in CI. It would have stripped the
poster from all 168 pages, not the 13 with a missing file. It also referenced
REPO_ROOT, which does not exist in that file, so the script crashed on the first
item. I described this guard in two commit messages without once executing the
generator.

The poster is now gone entirely, which is the smaller and more honest fix. These
previews are autoPlay muted loop, so the poster is visible for a few hundred
milliseconds; 13 of the 168 files do not exist and the browser fetches the poster
before the video. Removing the attribute kills 13 x 403 and 168 needless image
requests, and there is nothing to keep in sync.

Also applied a complexity pass to the delivery encode:
- hasAudio() deleted. 17 lines and a spawnSync per item to choose between
  "-c:a aac" and "-an". ffmpeg ignores -c:a when the input has no audio stream;
  checked, exit 0, output carries no audio track.
- The 40-line spawn + Promise wrapper is execFileSync. Everything around it in
  that script is already synchronous.
- The duplicated poster lookup is gone with the poster itself.

Net 76 lines lighter. Generator runs clean, emits 168 pages, carry-forward intact.
2026-08-04 12:52:26 -07:00
ukimsanov 199885b85e docs: put the slideshow demo on the slideshow page
Removing the eight-second teaser left this page with nothing at all, and the
replacement had been rendered and published hours earlier without ever being
wired in.

Twenty-seven seconds of a live deck: fragments arriving one key-press at a time,
a hotspot jumping to a branch slide, the back control returning to the exact
fragment it left, and presenter mode with real notes and a running timer. It is a
capture of the deck running, not a video of slides.

Guides with nothing to look at: 5.
2026-08-04 12:15:35 -07:00
ukimsanov b4648e0e5a docs: six pages get a real film instead of a six-second clip
Product launch, colour grading, faceless explainer, captions and recuts, motion
graphics, and voice and audio.

Five of these led with a six-to-eight second teaser. A six-second clip on the
page that teaches you to make explainers does not teach anything — it is a
placeholder that looks like content. Colour grading, the most visual feature in
the product, had no video at all.

The new films are 34 to 41 seconds and each shows a RANGE rather than one look:
four ways to tell the same idea with no footage; five caption treatments on one
untouched clip; correction, presets, curves, selective colour, all four scopes
and a LUT; narration with music ducking under it and effects on the beat. The
launch film is built by running the workflow on huly.io for real, so the page
that sells the workflow is showing its actual output.

They share one visual system, measured from HyperFrames own light-theme launch
films rather than invented: bone field, near-black type, coral accent, and the
product always a dark panel inset on the light field. Every one carries audio at
about -20 dB and is 1-2 MB.

The old teasers are removed from the three pages where they became a duplicate.
They stay in the workflow picker, which is where a six-second tile belongs.

Guides with nothing to look at: 15 at the start of this work, 5 now.
2026-08-04 12:14:26 -07:00
ukimsanov 158d0fea1f docs: stop serving render masters as if they were deliverables
Thirty-two published videos were the raw render output. One 20-second catalog
preview was 60 MB at 25 Mbps — Blu-ray bitrate for a screen capture in a docs
page. Nothing autoplays, so no page shipped that silently, but a reader who
pressed play paid for it.

Re-encoded at 1280 wide, CRF 28, faststart, keeping audio where it existed.
Measured PSNR against the originals at ~38.5 dB, which is visually identical —
the first check I ran returned nothing and I mistook that for a pass, so this
number comes from a filter that actually worked.

Worth correcting one thing I said earlier: the catalog is not systemically
over-encoded. 190 videos, 0.36 GB, about 1.9 MB each. Six were over 10 MB. I
found two monsters and generalised from them.

Pages carrying more than 8 MB of video: 26 before, and the heaviest went from
140 MB to under 15 MB.

The durable fix is in generate-catalog-previews.ts, which had no delivery encode
at all — it published the render master directly. It has a web pass now, so the
next preview is born small instead of being cleaned up later.
2026-08-04 11:33:52 -07:00
ukimsanov f595a7d683 docs: the last five blind guides now show the thing
Rendering, Performance, Deploy, Figma and Bring in a design.

**Figma is the one worth reading about.** The first attempt failed honestly — no
token, no file key, so it refused to fake an import and built a generic design
handoff instead, where both panels looked identical and nothing was demonstrated.
A real token and file key were sitting in bug-bash-figma-hf. With genuine access
it makes the actual point: the same cursor and the same clicks land on a PNG export
and on the imported HTML, and only the HTML responds — the field takes text, the
button runs through its states. One side is pixels, the other is a component. It
replaces the placeholder comment on Bring in a design too.

**Performance is measured, not asserted.** 25.0s against 9.8s as the median of
three runs at 1920x1080 over 300 frames, one CSS declaration responsible for the
whole gap, and a pixel comparison at 43.7 dB PSNR confirming the picture did not
change. Real numbers from real runs on this machine.

**Rendering** shows the loop end to end. **Deploy** shows the four places a render
can run and labels anything not executed live.

All four carry their own audio at about -19 dB. Each was checked on a contact
sheet and for dead frames before publishing.

Guides with nothing to look at: 15 at the start of this work, 5 now. The five
left are pages where a video would be decoration — authentication, feedback, a
decision table, a comparison that carries code, and 4K, which is a paragraph
about a flag.
2026-08-04 03:14:43 -07:00
ukimsanov 8762887a5b docs: four more guides show the thing instead of describing it
HTML in Canvas, MCP, Skills, and Images and video all had nothing to look at.

**HTML in Canvas** puts a real HTML dashboard beside the same pixels running as a
live WebGL texture, bent by a shader while the DOM underneath keeps updating and
the texture re-captures every frame. That is the whole feature in one shot.

**MCP** is an honest reconstruction and says so on screen for its full length —
"Illustration of the flow". It could not reach a live connector, so it does not
pretend to have one. Describe the video, revise it in plain language, ask for a
format, get the render.

**Skills** shows a real run of the install command, then the router reading two
different requests and picking a different workflow for each.

**Images and video** walks the five basic moves — import, place, crop, trim, swap
the source — adding one attribute at a time on the left with the result on the
right. It serves both /guides/media and /guides/video-components.

All four carry a music bed from HeyGen's own catalogue at about -22 dB. Each was
checked on a contact sheet before publishing, not taken on the session's word.

Blind guides: 15 down to 9.
2026-08-04 03:08:04 -07:00
ukimsanov 60f09dfcfd docs: give every click-to-play video an audio track
Ten videos on the docs site played in silence. The player is click-to-play, so a
reader deliberately starts them and gets nothing — which reads as broken rather
than as a stylistic choice.

The music is from HeyGen's own catalogue, pulled through the media-use skill's
resolve path with the account already authenticated on this machine. That matters
more than convenience: the launch projects on disk carry a `launch-music-trimmed.mp3`
with no licence, no attribution and no metadata, and three Pixabay tracks. Neither
belongs on the official docs CDN, where objects cache immutably for a year.

Mixed at roughly -22 to -24 dB mean so it sits under the picture. Getting there
took two corrections worth recording: proportional fades, because a 1.2s fade-out
on a 5s clip eats half the audio; and seeking 11s into the bed, because its first
ten seconds are a quiet intro that dragged every short clip down to -27 dB.

The three Examples tiles are left silent on purpose — they are muted autoplay
loops, so an audio track would only be weight.

Verified: every video referenced anywhere under docs/ that is not a muted
autoplay tile now has an AAC stream, checked against the live CDN URL rather than
the local file.
2026-08-04 03:08:04 -07:00
ukimsanov 61cd3f1da2 docs: keep the audio on the Huly launch film
I stripped the audio track with -an when encoding for the web. The source has a
real mix at -19.2 dB mean / -3.1 dB peak, and the docs player is click-to-play,
so there is no reason for it to be silent. Re-encoded with AAC at 128k; levels
match the source exactly.

The other published films have no audio track at all to preserve — they were
rendered silent at source.
2026-08-04 03:08:04 -07:00
ukimsanov ef99809482 docs: show GSAP keyframes moving, and drop the weakest video on Examples
The animation guide had nothing to look at, which is a strange state for a page
about motion. It now opens with the keyframes launch film — real `gsap.to()`
source with a `keyframes` array on screen, and the same motion being reshaped by
dragging an ease curve in Studio instead of guessing numbers.

Examples loses the ten-second reference-project render. It sat directly beneath
four finished films and lost the comparison every time; the four cards above it
already carry the point that the project is open end to end. The cards stay, the
render goes.
2026-08-04 03:08:04 -07:00
ukimsanov 2a7d9bd0ad docs: rebuild the catalog generator, and show what background removal and HDR do
**The catalog generator destroys hand-written documentation on every run.** It
does `rmSync(dir, { recursive: true })` on both catalog folders before
regenerating, so any section a human added to a generated page is deleted the
next time anyone runs it. There is now a carry-forward pass that reads those
sections off the existing pages first and re-emits any heading the template does
not own. Note this prevents future loss only — the earlier regeneration in this
stack already removed what was there, and it is recoverable from git if wanted.

It was also clobbering `docs.json`: rebuilding the Catalog tab dropped its icon
and unlinked `catalog/index.mdx` from the sidebar entirely.

And 27 of 36 component pages told the reader to "see the comment header in the
file" when no such header exists. The generator reads the file now and only says
it when true.

On presentation: 1,365 table rows across 168 pages became 0. `## Details` was a
table whose rows were "Type: Block" and the duration; `## Files` was three
columns where most items have exactly one file. Both are one sentence now. The
preview leads the page instead of sitting under title, description, tags, a
warning and a credit — four of which Mintlify already renders from frontmatter.

**Two guides that showed nothing now show the thing.** Background removal plays a
real cutout with the matte magnified at the hair edge and at a shoulder a plant
overlapped, because those are where matting fails. HDR is the honest one: you
cannot show HDR brightness on an SDR page, so it shows the `ffprobe` verdict and
a measured round-trip — 4000 nits authored, 4012 back out — and says on screen
for all 26 seconds that it is not simulating anything.

Both films were checked before publishing: no black or frozen stretches, and read
at the real 590px docs column width rather than full size. Nav after regeneration:
307 pages, 0 dangling, 0 redirect collisions.
2026-08-04 03:08:04 -07:00
ukimsanov 72d4016f5f docs: rewrite four pages that told instead of showed
**Color grading** was the worst page in the docs by the measure that matters:
33 table rows against one image, for the most visual feature in the product.
Four of the five tables were restating jargon in a grid — "Waveform | Brightness
by horizontal image position" tells you nothing you could act on. Those are prose
now, and the scopes section explains what each one is *for* (skin tones land on
one known line of a vectorscope, so faces are quick to check). The control lookup
stays a table, because that one is a genuine lookup.

Four factual errors in the old page, each verified at source before changing:
Studio's group is **Grade** in the flat Inspector, not "Color grading" in Design;
the controls are `whites`/`blacks`, not "white point/black point"; LOG and HDR
were conflated into one sentence when they are different cases; and the
project-wide LUT copy refuses a relative path in favour of a URL or data URL, not
a "project-root path".

Two things the page never mentioned and should have: `hyperframes grade-compare`,
which renders every candidate look onto one reference frame, and the fact that
`data-color-grading` nests under adjust/details/effects — a flat object renders
nothing at all.

**vs Remotion** argued for 851 words with nothing to look at and no code. It now
carries the same three-second title card written both ways. The HyperFrames one
is a real composition, not illustrative markup: extracted and run through lint
(0/0) and the full browser gate — runtime, layout, motion and contrast all pass.
It also concedes what Remotion genuinely does better, because a comparison page
that only flatters us is not worth reading.

**Choose how to create** never said what the four options were before comparing
them. It opens with the actual decision now — they all give you a video, the
difference is what you keep — and a four-row table of what you walk away with.

**Bring in a design** loses the 7-row keep-vs-rebuild table for one plain rule,
and the bullet list that used semicolons.

Every internal link on all four checked against the real page set. Where a visual
belongs but no approved asset exists, there is a JSX comment marking the spot
rather than a URL that would 404.
2026-08-04 03:08:03 -07:00
ukimsanov 84b5160b66 docs: show the work on Media effects, Product launch, and Examples
Three pages that described capability instead of showing it.

**Media effects** named eighteen effects as a bulleted list of words and showed
four of them in one small PNG. A reader could not see what Kuwahara Paint,
Ordered Dither or CRT Curvature actually look like. It now leads with the
finished media-treatments showcase film — every effect family on real footage,
with labelled before/afters — and the "Choose the effect" table is gone, because
six rows were saying what four sentences say better.

**Product launch** embedded a 44-second cut of small UI panels on black. Replaced
with the film this workflow's own skill produced from huly.io: the product's real
screens, its type and colour, bold editorial layout. Verified before publishing —
no black stretches over 0.4s, no freezes over 1s, captions clean at full
resolution.

**Examples** gets its four project cards back. Cutting them to one link was my
misreading of the actual complaint, which was about the reference-project video
sitting underneath four finished films and losing to them.

Both new assets are published to the docs CDN under versioned filenames and
return 200.
2026-08-04 03:08:03 -07:00