Commit Graph
3803 Commits
Author SHA1 Message Date
ukimsanov fb6a82c505 docs(examples): hover-to-hear on the four launch films that have audio masters
HF x Stripe, SpaceX, Cloud rendering and Timeline had audio-baked render masters
in hyperframes-launches (via LFS); re-encoded those with sound to -s.mp4 keys and
wrapped their cards in HoverVideo. The rest of the grid has no ready audio master
(silent renders; audio only as unmixed assets) and stays muted.
2026-08-05 00:53:28 -07:00
ukimsanov 5361b9e10c docs(30-days): hover a card to hear the film's audio
Adds a HoverVideo snippet (autoplay muted, unmute + play on hover, re-mute on
leave) and points the 18 days whose source has real audio at new -s.mp4 keys
re-encoded with sound (CDN is immutable, so new keys not overwrites). The other
12 days have genuinely silent sources and stay muted. Honours prefers-reduced-motion.
2026-08-05 00:36:07 -07:00
ukimsanov 8298bd4390 docs(examples): drop three dead source links (no such launch folder)
skills-launch-video, keyframes-launch and music-to-video-launch have no folder
in hyperframes-launches. The films are real but their source was never published,
so these cards were linking to 404s and falsely claiming 'source included'. Made
them non-linked cards labelled '100% HyperFrames', like the other sourceless ones.
2026-08-05 00:16:29 -07:00
ukimsanov 407a5fd513 docs(examples): bind reference video muted to state so unmute sticks
React was re-applying the static muted={true} on every re-render, instantly
re-muting the reference after a click. Bind muted to state instead.
2026-08-05 00:12:12 -07:00
ukimsanov bde03484ae docs(examples): sync the recreate pairs with a tap-to-unmute button
The reference and replica play on one shared clock; a single Instagram-style
button unmutes the reference (its original audio), the replica stays silent.
Reference videos re-encoded with audio on the CDN. The component honours
prefers-reduced-motion on both edges, per the docs-snippet-motion guard.
2026-08-05 00:04:26 -07:00
ukimsanov 98ae45e066 docs(examples): add 1:1 recreate-any-video comparison block
Two reverse-engineering campaigns (THE OBLIST, Srinika x Mercury), each
showing the reference original beside its frame-matched HyperFrames replica.
Assets on the docs CDN; kept separate from the '100% HyperFrames' grid since
the reference films are the originals.
2026-08-04 23:42:39 -07:00
ukimsanov ddbd547fad fix(scripts): restore the texture instruction phrase a core test pins
I misdiagnosed this as a pre-existing Windows CRLF failure. It was not. The core
test catalogGeneratorInstructions.test.ts reads scripts/generate-catalog-pages.ts
and asserts it contains three exact phrases. My catalog rewrite reworded the
texture snippet instruction from "paste the real `<style>` element near the
bottom" to "Near the bottom is a real `<style>` element — copy it into", which
dropped the third pinned phrase.

Restored the wording to contain "paste the real `<style>` element near the
bottom". The test now passes (3/3 expects), and it was failing on the Windows
runner only because the Ubuntu run tripped an unrelated player port flake first
and masked the same core failure there.

Not Windows, not pre-existing, not line endings — my text change. Caught because
the reviewer questioned the diagnosis.
2026-08-04 22:33:43 -07:00
Miguel Ángel 29f004cfc0 chore: release v0.7.94 (#3045) v0.7.94 2026-08-04 20:42:17 -07:00
Miguel Ángel b9233525b2 feat(cli): add device authorization login (#2836)
* feat(cli): add device authorization login

* refactor(auth): simplify device authorization flow

* fix(cli): harden device authorization flow

* refactor(cli): simplify device auth validation tests
2026-08-04 20:36:42 -07:00
Miguel Ángel a4eb602ee2 chore: release v0.7.93 (#3044) v0.7.93 2026-08-04 20:27:07 -07:00
Varo b390b71bde fix(engine): keep BeginFrame time monotonic across frame rates (#3011)
Fixes #3012

## What

Fixes `HeadlessExperimental.beginFrame` hanging at 60fps and other high frame rates.

## Why

Warmup always advances Chrome in 33ms steps, but the first capture timestamp was calculated from the output fps.

At 60fps, the last warmup tick is 1947ms, while the next commit tick used to jump back to about 1067ms. Chrome sees time moving backwards and `beginFrame` can stall.

## How

Keep the old timestamps when they are already safe. If the commit tick would go backwards, move the capture baseline past warmup first.

The init commit and both producer probes now use the same timestamp helpers, so they cannot calculate different values.

## Test plan

- [x] Added tests for 24, 30, 31, 32, 33, 60, 120, 240, and 59.94fps
- [x] Added coverage for the actual session baseline, commit parameters, and both producer probe paths
- [x] Engine tests: 1383 passed, 3 skipped
- [x] Producer unit tests passed
- [x] Engine and producer typechecks and builds passed
- [ ] Manual render test
2026-08-04 19:22:33 -07:00
Miguel Ángel bce2140ff2 test(studio): relax large fixture timeout (#3040) 2026-08-04 18:58:24 -07:00
James Russo bbdfee1166 fix(engine): validate remote download integrity (#2938) 2026-08-04 18:02:01 -07:00
ukimsanov b33974cf72 chore: quiet fallow complexity on the catalog generator's grown functions
My catalog rebuild grew five functions past fallow's CRAP threshold —
carriedSectionsFrom and its flush (the carry-forward parser), plus generateItemMdx,
generateParams and main which took on the poster and carry-forward wiring.

These are build-time scripts with no unit coverage, and CRAP penalises exactly
that: the inherited functions in the same two files score 300-600 for the same
reason and pass only because they predate the gate. Refactoring into smaller
functions made it worse — more uncovered functions, not fewer findings. So each
grown function carries a `// fallow-ignore-next-line complexity`, the tool's
documented mechanism, applied consistently with the file's existing shape.

Fallow verdict is pass: 0 introduced complexity, 0 introduced duplication.
Generator runs clean and emits 168 pages.
2026-08-04 17:43:49 -07:00
Vance IngallsandClaude Opus 5 f3689c1481 fix(producer): scope and harden the markup-payload sniff
Review follow-up on the STUDIO-5433 defense.

Correctness

- The sniff ran above the documented video/audio failure split, so an
  <audio> src that resolved to an HTML payload aborted the whole render
  instead of degrading to duration 0. It now runs inside the same try, so
  video surfaces the typed error while audio still drops out, with a
  warning naming the element.
- Raw fs errors (EISDIR on a directory src, EACCES, the existsSync->open
  ENOENT race, EMFILE) escaped and failed the compile with an unclassified
  error carrying an unredacted temp path. The sniff is now a classifier that
  never throws: an unreadable file reports "not markup" and the real probe
  produces the real error.
- Elements whose duration the compiler never resolves (a data-end video, a
  looping audio) skipped the sniff entirely, so the original ffprobe error
  still escaped, and looping audio was reported as owner "system" after
  every frame had been captured. Video is now caught in the asset preflight,
  which sees every local src regardless of authored timing; audio is
  classified per-element in audioMixer as source/invalid_media/owner "user",
  keeping audio failures non-fatal as they already were.
- Detection is a byte-level check for a leading "<" (BOM-, whitespace- and
  NUL-tolerant, looped read) instead of a <!doctype|<html|<?xml string
  prefix, which missed a NUL-prefixed payload, >256B of leading whitespace,
  UTF-16-encoded HTML, and a prolog-less <svg. No supported container starts
  with "<", so the allowlist no longer grows per payload shape.
- finally { await fh.close() } could replace the in-flight typed error with
  the close error.

Routing and privacy

- MARKUP_NOT_MEDIA is now in SAFE_RENDER_ERROR_CODES, the Lambda terminal
  name map, the CDK and SAM non-retryable plan lists, and the Cloud Run
  non-retryable set, and the class carries owner/retryable. Previously the
  API emitted errorCode: undefined and a deterministic authoring bug burned
  the full distributed retry budget.
- The message no longer carries 32 raw payload bytes or the src.
  redactTelemetryString preserves host and path for HTTP srcs, so
  per-tenant CDN paths reached a message the server forwards to clients.
  Correlation is a sha256 element fingerprint, matching
  AssetMediaTypeMismatchError.
- The message names both causes (unresolved nested-composition URL, or an
  HTML/XML error page served as 200) rather than misdiagnosing an S3 403
  body as an authoring bug.

Tests

- Byte-level detection is unit-tested in engine: markup shapes, BOMs,
  UTF-16, nine container signatures, unreadable inputs.
- Replaced the tautological assertions. The old checks for "html" in and
  "moov" absent from a fixed message template could not fail for any input.
- New coverage for audio degradation, the audioMixer classification, the
  preflight video/image/audio split, and the API error metadata.
- The sibling htmlCompiler.mediaType failure was a vitest-under-bun runner
  mismatch, not a missing ffmpeg binary. It passes, including the 4-wide
  probe-semaphore invariant the sniff now runs inside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:38:51 -07:00
ukimsanov a46bc9d565 docs: 30 Days shows all thirty films, the real ones
The user downloaded the actual daily posts and matched each to its day. Every
card now plays the real Day-N film — install, PR-to-video, keyframes in Studio,
background removal on a checkerboard, color grading, components, the lot — over
its own X-post link, no borrowed docs clips.

Day 7's five motion-graphics pieces are concatenated into one film. Day 26 had no
dedicated post, so it uses the media-effects showcase. Day 30 is built here: a
second from every one of the twenty-nine lessons, back to back — thirty for thirty.

All thirty are web-encoded (1280 wide, CRF 28) and live on the CDN. 0 dead URLs.
2026-08-04 17:31:38 -07:00
Miguel Ángel 7bf425b7a9 docs(studio): document timeline keyboard navigation (#3031)
* feat(studio): expose timeline treegrid semantics

* feat(studio): coordinate logical timeline focus

* feat(studio): add timeline keyboard controls

* docs(studio): document timeline keyboard navigation
2026-08-04 17:09:07 -07:00
Miguel Ángel 0f1333c80d feat(studio): model logical timeline navigation (#2712) 2026-08-04 16:48:39 -07:00
ukimsanov b5509602fa docs: remove the Reference Project — nobody wants to be sent to GitHub
The "One project, open end to end" section on Examples was four cards linking into
a GitHub folder. A reader on the docs site does not want to leave for a repo tree
to read a BRIEF.md. The section is gone, and so is the examples/docs-reference-project
folder it pointed at.

Also removed the prose references that leaned on it: the Reference Project
paragraph on Go further, the two GitHub-inspect links on Developers, and the
mention on the Studio landing. Each was reworded to talk about "a project"
generally rather than that specific folder.

Examples is now purely the nineteen finished films plus Start from a template.
The changelog entry recording #2977 is left as history. 0 broken links.
2026-08-04 16:46:45 -07:00
ukimsanov 06d2d6f24c docs: 30 Days keeps its structure but borrows no videos
The user is supplying the real thirty daily films. Until then this page must not
substitute launch or docs-demo clips for them — that is exactly the reuse the
whole video pass was meant to end.

Removed every borrowed clip: the four hero Frames that pulled the launch film and
the Day 3/4/23 demo cuts, and the docs thumbnails standing in for day posters.
What stays is honest and complete-looking: the thirty day cards, each linking its
real X post, each with a named slot — thirty-days-day-NN.mp4 — so the real files
drop straight in.
2026-08-04 16:36:23 -07:00
ukimsanov 08fadcef41 style: format the house-narrator note in tts.md, resync manifest
Preflight failed on skills/media-use/audio/references/tts.md — the section I added
was not oxfmt-clean. Formatted, and regenerated skills-manifest.json since the
media-use hash changed.

The other files oxfmt flags (package.json files, several skill .md files,
studio/parsers sources) are not touched by this branch and fail on main too — a
pre-existing whole-repo format debt, not introduced here.
2026-08-04 16:33:57 -07:00
ukimsanov 8483b51d4b docs: Examples shows 19 launch films grouped by what they prove, and 30 Days moves into Explore
**Examples was four films borrowed from other pages. It is nineteen now**, all
freshly rendered, grouped by capability rather than dumped in one wall: product &
launch films, the tooling shown working, motion/sound/effects, and workflows &
integrations. Every one is 100% HyperFrames; where the source project is public
the card links to it. This is the page a reader lands on to see the spread of
what the framework can do, so it should show the spread.

**30 Days of HyperFrames was its own top-level nav group for a single page.**
Moved into Explore alongside Examples, Product updates, Weekly updates and the
Changelog, where a once-a-day series belongs.
2026-08-04 16:31:11 -07:00
Miguel Ángel 19b8a1f3c5 refactor(studio): extract live timeline clock (#2711) 2026-08-04 16:27:41 -07:00
ukimsanov 618f73c266 Merge remote-tracking branch 'origin/main' into docs/pages-show-not-tell
# Conflicts:
#	skills-manifest.json
2026-08-04 16:16:58 -07:00
ukimsanov 00a4f454ee chore: regenerate skills manifest after the media-use voice rule
Added the house-narrator section to media-use/audio/references/tts.md and did not
regenerate skills-manifest.json, so 'Skills: manifest in sync' failed. Regenerated;
only the media-use content hash changed.
2026-08-04 16:13:57 -07:00
Miguel Ángel ebdd1893c4 fix(studio): reconcile external edits before reload (#2993)
* fix(studio): reconcile external edits before reload

* fix(ci): retry transient workspace installs

Make external reload retry behavior honest and isolate reload listeners.

Remove the dead SDK timestamp parameter.
2026-08-04 23:13:19 +00: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
Miguel Ángel 8a0dccfc72 feat(studio): add external conflict recovery UI (#2992) 2026-08-04 22:43:20 +00:00
Vance Ingalls (via Via) 8a11e9776d feat(producer): sniff HTML payload before ffprobe in resolveMediaDuration
STUDIO-5433 defense: when the downloaded media file begins with
<!DOCTYPE, <html, or <?xml, throw a typed HtmlNotVideoError naming
the offending src instead of letting ffprobe emit an inscrutable
moov-atom-not-found on a plain HTML page.

Complements #3033 diagnosability layer. Root-cause EF fix ships
separately.

Signed-off-by: Via <vance@heygen.com>
2026-08-04 22:42:36 +00: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
Miguel Ángel a99caad581 feat(studio): coordinate external file changes (#2991) 2026-08-04 22:20:44 +00:00
ukimsanov 18c0b8319d docs: the examples gallery now tells you what it is
The page promised "every prompt below has been run end-to-end", then a Note
admitted the Level 3 motion grammar was "stated once here rather than repeated in
all of them". So the printed prompts are not the prompts that produced the
embedded videos, and a reader copying one gets a different result with no way to
know why.

Rather than pretend, the page now says it plainly: each video came from the prompt
beneath it plus a shared preamble, copy both, and the preamble is labelled as the
thing to paste first. The title stays honest because the prompts *were* run — just
not alone.

Five prompts also ended on a frozen frame — "Hold on the card at the end.",
"settles on white, holds.", "then holds.", "hold.", "Hold the last 1.5s." — while
the same Note claimed every one asks for an ambient idle. motion.mdx:62 calls a
motionless hold the tell to avoid, and motion-graphics.mdx already carries the
corrected form of two of these. All five now end the way the rule says.
2026-08-04 15:08:28 -07:00
ukimsanov d3331aad28 docs: E_NO_GSAP_TIMELINE is not a missing feature
Five places across two SDK pages said this error means the GSAP parser engine "is
not yet available" / "ships in a later phase" / "until it ships". The parser is
shipped — sdk/src/document.ts:13 and engine/mutate.ts:58 both import and call
parseGsapScriptAcornForWrite.

What the code actually returns (mutate.ts:1745-1751):

  "No gsap.timeline() declaration found in the GSAP script."
  hint: "addGsapTween / addLabel require a timeline variable
         (e.g. var tl = gsap.timeline(...))."

It is a fixable authoring problem in the reader's own composition, and the fix is
one line. Told it is an unshipped feature, a reader waits instead — and the Note
listing six ops as unavailable made a working part of the SDK look absent.

All five sites now say what the error means and how to clear it, including the
code comment in the guide's example.
2026-08-04 14:46:04 -07:00
ukimsanov 9a659053db docs: an Inspector map that matches the Inspector, and an example that can run
**canvas.mdx pointed at a Design section that does not exist.** The table sent
readers to "3D". PropertyPanelFlat.tsx renders Text, Style, Layout, Motion,
Grade, Effects, Overlays, Media — no 3D. 3D Transform is a labelled block inside
Layout (propertyPanelFlatLayoutSection.tsx:302). The table also had no row for
Motion or Effects, while the approved media-effects guide tells readers to "open
Effects in the Inspector" — so the canonical map of the Inspector said that
section did not exist. Four rows fixed or added.

**The two-card reuse example could not do what its prose promised.** Both
concepts/compositions.mdx and concepts/variables.mdx omitted data-duration on the
nested-composition hosts, then said `data-start="card-pro"` means "start when
that one ends". With no duration, resolveDurationForElement returns null and
startResolver.ts:180-184 falls back to targetStart + offset = 0 — both cards
stacked at t=0 on one track, which html-schema.mdx:104 says must never happen and
:99 lists data-duration as required for nested-composition clips. Both copies now
carry it.
2026-08-04 14:45:02 -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
Miguel Ángel b30a23402e feat(studio): preserve external file conflicts (#2990)
* fix(studio): drain pending edits before reload

* fix(studio): address drain review feedback (#2989)

- prioritize conflicts and clear recovered DOM queue errors
- cover delayed blur effects and missing drain branches
- document stacked consumers and extend write-token retention

* test(studio): satisfy drain audit gate (#2989)

- share the editor-save hook harness across drain regressions
- extract settled failure inspection from the drain loop

* feat(studio): preserve external file conflicts

* fix(studio): isolate retry write receipts

* test(studio): cover external conflict recovery safety
2026-08-04 21:19:42 +00:00
ukimsanov d01253771f docs: three things the source contradicts, found by reading the pages
**`bun unlink hyperframes` does not exist.** Bun answers "error: bun unlink
{packageName} not implemented yet" — I ran it. The command takes no package name;
it unregisters whatever directory you are standing in. The reader arrives at that
block having just been told to cd into a video project, so even the correct form
would unregister the wrong thing and leave the global link in place — which is
exactly the failure the same page's troubleshooting section then explains. The
page created the bug it diagnosed. Now `cd packages/cli && bun unlink`.

**`data-media-start` and `data-playback-start` are one value, not two features.**
The schema page gave them separate rows with different-sounding meanings, so a
reader would reasonably think they compose. Six read sites in core are all
`playbackStart ?? mediaStart` — runtime/media.ts:18 and :92, runtime/init.ts:751,
1993, 2928, 3010. One row now, alias named, and it says which wins.

**Canary rollouts contradicted itself.** Line 109 called enrolment a pure function
of `(feature, installId, percentage)`; line 195 said canaries bucket on a
dedicated `bucketSeed`, not the telemetry id. The second is right —
cli/src/telemetry/canary.ts:98 is `unitId: config.bucketSeed ?? config.anonymousId`.

Also on the Studio shortcuts page earlier: J/Shift+J for keyframe navigation,
Backspace to delete, and Ctrl+Y for redo were all missing.
2026-08-04 14:14:38 -07:00
ukimsanov 1a22d6542a docs: a compare example that works, and prose instead of a semicolon list
`npx hyperframes compare` was shown with no arguments. The command needs two or
more paths ("pass 2+ paths" in its own help), so a reader copying that line gets
an error on their first try. It now shows two.

The closing section was six bullets joined by semicolons — a sentence pretending
to be a list. It is a sentence now.

While writing that sentence I claimed every command can print JSON. It cannot:
`snapshot` and bare `skills` have no --json, though `skills check` does. Changed
to "most commands", which is true. Checked twelve commands against their own
--help rather than against the page.
2026-08-04 14:12:21 -07:00
ukimsanov f28b6d2bf7 docs: three Studio shortcuts the page never mentioned
Read against packages/studio/src rather than against the page's own prose.

- `J` / `Shift + J` jump to the previous and next keyframe
  (useKeyframeKeyboard.ts:60). The keyframe table had no row for J at all.
- `Backspace` deletes a keyframe, not only `Delete` (same file, line 56).
- `Ctrl + Y` is redo (useAppHotkeys.ts:44). Only Ctrl+Shift+Z was listed, and
  Ctrl+Y is the first thing a Windows user reaches for.

The page already warned that arrow keys mean different things depending on
selection. `J` and `K` do exactly the same and it did not say so — `K` is stop
during playback and add-keyframe with keyframes selected, which is a surprising
way to lose your place. That note now names them.

Everything else on the page checks out: 20 single-letter bindings in the source,
all now accounted for.
2026-08-04 14:05:30 -07:00
Miguel Ángel 4713138544 fix(studio): drain pending edits before reload (#2989)
* fix(studio): drain pending edits before reload

* fix(studio): address drain review feedback (#2989)

- prioritize conflicts and clear recovered DOM queue errors
- cover delayed blur effects and missing drain branches
- document stacked consumers and extend write-token retention

* test(studio): satisfy drain audit gate (#2989)

- share the editor-save hook harness across drain regressions
- extract settled failure inspection from the drain loop
2026-08-04 20:58:19 +00:00
Miguel Ángel 552419c52d fix(studio): make inspector commits transactional (#2987)
* fix(studio): make inspector commits transactional

* fix(studio): make inspector persistence atomic

* fix(studio): preserve synchronous gesture semantics
2026-08-04 20:26:43 +00:00
Miguel Ángel cde5bae4c5 test(studio): pin text-field Backspace routing (#2988) 2026-08-04 13:23:25 -07:00
Miguel Ángel 57d0056813 Merge pull request #3029 from heygen-com/fix-browser-gpu-verify
fix(engine): verify explicit browserGpuMode=hardware instead of trusting it
2026-08-04 13:19:51 -07:00
ukimsanov bb7b0c899f docs: write down the house narrator, and stop the videos sounding like two products
Every user-journey film on the docs site is narrated by ElevenLabs River
(SAz9YHcvj6GT2YYXdXww) at 145-155 wpm with music about -31 LUFS under it. That
was recorded in one launch project's notes and nowhere an agent would look.

So when I briefed six new docs videos I asked for "a music bed plus SFX" and said
nothing about voice. Two fell back to local Kokoro (am_michael, bm_george) and one
used an unspecified ElevenLabs take. Three films, three narrators, none of them
the one the rest of the site uses. Being re-voiced now.

The rule is in skills/media-use/audio/references/tts.md, next to the provider
table an agent already reads before generating a voiceover, including the reason:
falling back to a local voice because a key was not to hand produces a film that
sounds wrong beside the others. If ElevenLabs cannot be reached, say so and stop
rather than substituting.

Also on this branch: the superseded Huly film is gone from the product-launch
page, and three pages that ended up with two hero videos stacked now lead with
one. Where the older clip still showed something different — a finished motion
graphic, the same edits done in Studio — it moved below under its own heading
instead of being deleted.
2026-08-04 13:16:24 -07:00
Miguel Angel Simon Sierra f69c4a0e3a fix(engine): distinguish probe failure from a genuinely absent GPU
A probe that could not run is no evidence about the GPU, so pointing the
operator at GPU passthrough hid broken Chrome installs behind a phantom
problem. Carry a cause off the probe and emit the matching remediation.

Also un-exports buildUnverifiedHardwareGpuWarning (Fallow: engine test
files are not audit entry points, so a test-only import would not have
counted as a consumer) and covers the non-linux branch via the spy.
2026-08-04 13:14:23 -07:00
ukimsanov 74ba97b7ed fix(scripts): the repo already knew which items have no poster
Thirteen manifests declare `preview: { video }` with no `poster` key, and that
omission is deliberate — no .png was ever produced for them. main reads it in one
line: `if (manifest.preview) return manifest.preview.poster`.

I deleted that line as "config nobody varies", having checked whether any poster
DIFFERED from the generated URL (none did, 100 of 100) and never having checked
whether any was ABSENT (thirteen were). Then I replaced it with a filesystem
check against docs/images, which is gitignored, so it was false everywhere and
would have stripped all 168. Then I removed the poster entirely. Three wrong
answers to a question the repo had already answered.

Restored, and the page's video tag now asks the same function rather than a
second source of truth. 155 of 168 carry a poster, matching main exactly: 154
video pages plus texture-mask-text, which renders mask images instead of a video.

The 13 remaining dead URLs are in catalog-index.json only, which no code in
docs/ or packages/ reads, and no rendered page requests them.

The complexity cuts to the encode pass stand: hasAudio deleted (ffmpeg ignores
-c:a with no audio stream), and the spawn+Promise wrapper is execFileSync.
2026-08-04 12:59:50 -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 fffc56c33b style: format changelog-weekly.ts
Left unformatted by the catalog-generator commit, which is the one file
Preflight was failing on. oxfmt --check is clean on it now.

Note for anyone reading the earlier claim: I said main fails oxfmt on 30 files.
That was my local run picking up no repo config and using its own defaults. CI
found exactly one file, and it was mine.
2026-08-04 12:24:43 -07:00
ukimsanov 4f1ace9f47 fix(scripts): actually commit the delivery encode
An earlier commit here said generate-catalog-previews.ts "has a web pass now".
It did not. I made that edit in a second checkout while investigating and never
brought it onto the branch, so the claim shipped in a commit message while the
code stayed as it was.

The fix itself is unchanged from what was described: the render output is a
master, and publishing it directly is what put 25 Mbps files on the docs CDN.
generateVideo now renders to <name>.master.mp4, runs one delivery pass at 1280
wide, CRF 28, faststart, keeping audio only when the source has it, then deletes
the master and logs the delivered size.

Formatted and linted clean. The Format check failing on this PR is pre-existing:
origin/main fails oxfmt --check on the same 30 files, none of which this branch
touches.
2026-08-04 12:21:43 -07:00