Commit Graph
3504 Commits
Author SHA1 Message Date
James Russo 2339757377 fix: bound HDR and video extraction resources (#2955)
* fix: bound HDR and video extraction resources

* fix: trim negative video extraction preroll

* fix: skip invisible video extraction windows

* fix: preserve negative-start loop and held tails

* fix: cap finite video slots to source duration

* fix: bound held-tail frame extraction

* fix: plan from playable video duration

* fix: preserve open-ended held video tails

* fix: resolve held tails from decoded frames

* fix: normalize final-frame probe timestamps

* fix: handle unseekable final-frame sources

* fix: dedupe final-frame probes per render

* refactor: clarify output dynamic range contract
2026-08-02 20:21:54 -07:00
Miguel Ángel 67ffafb11c docs(contributing): refresh the catalog contribution guide (#2954)
The guide still described a 52-block registry, told contributors to run the
deprecated `validate` command, and listed gaps that have since shipped.

- Correct the counts: 113 blocks, 25 components
- `validate` -> `check` in the quick version
- Document the `demo.html` requirement for components (CI fails without it)
- Document `params` (drives the Studio customization panel) and the other
  optional registry-item fields
- Add the monospace caption floor and `fitTextFontSize()` to the quality bar
- Add a motion-review checklist: rules paired with a self-check question
- Replace the manual preview-MP4 step with what catalog-previews CI does
- Rewrite "What's Needed Right Now" by the job a shot does in a video, and
  drop the gaps that have shipped (karaoke, lower thirds, maps, news ticker)
2026-08-02 22:02:55 +02:00
Miguel Ángel 411ada0d90 chore: release v0.7.89 (#2953) v0.7.89 2026-08-02 20:25:14 +02:00
Miguel Ángel 59fe32b633 Merge pull request #2952 from heygen-com/fix/2951-ignore-generated-cache-watcher
fix(cli): ignore generated caches in project watcher
2026-08-02 19:51:43 +02:00
Miguel Ángel 1d01b9f2cf fix(studio): exclude generated caches from project metadata 2026-08-02 17:37:51 +00:00
Miguel Ángel 3eb7b1ffd5 test(cli): clarify watcher exclusion coverage 2026-08-02 17:25:17 +00:00
Miguel Ángel bf739a4db2 fix(cli): ignore waveform cache in project watcher 2026-08-02 17:19:51 +00:00
Miguel Ángel 7ea8250f50 fix(cli): ignore generated caches in project watcher 2026-08-02 17:13:19 +00:00
Vance Ingalls 74fadf69c4 chore: release v0.7.88 v0.7.88 2026-08-01 16:53:08 -07:00
Vance Ingalls 3f30de7f09 Merge pull request #2911 from heygen-com/docs/color-grading-chapter
docs(prompting): add colour grading and film effects chapter
2026-08-01 16:50:50 -07:00
Vance IngallsandClaude Opus 5 0c7c9bbdb0 docs: make the intensity explanation order-agnostic
Both pages explained intensity's independence by saying details and
effects are applied after the u_intensity mix. The conclusion was right
but the mechanism was wrong for several families.

Verified in the shader: applyCrtWarp runs on the uv before sampling;
sampleMedia itself carries pixelate, chromaBleed and the tape family;
sampleChromaticMedia and applyDigitalGlitch shape sampleColor — all
before the mix at runtime/colorGrading.ts:1234. Only grain,
filmArtifacts, monoScreen, engraving, crosshatch, halftone, twoInkPrint,
bloom, scanlines and vignette run after it.

Pre-mix effects are already present on both sides of
mix(sampleColor.rgb, applyColorGrade(sampleColor.rgb), u_intensity), so
intensity does not scale them either. Both pages now say details and
effects sit outside the mix — some before, some after — without making
the ordering the reason.

Reported by miguel-heygen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:36:12 -07:00
Vance Ingalls 343c025188 Merge pull request #2916 from heygen-com/ffprobe-5-invocation
fix(engine): reject stdin input, decode stdout correctly, bound its size
2026-07-31 20:07:03 -07:00
Vance Ingalls b5640a5464 Merge pull request #2915 from heygen-com/ffprobe-4-audio
fix(engine): make the AAC duration refinement safe, cancellable and LC-only
2026-07-31 19:58:27 -07:00
Vance Ingalls 97564b0268 Merge pull request #2914 from heygen-com/ffprobe-3-framerate
fix(engine): reject non-finite, negative and malformed frame rates
2026-07-31 19:58:13 -07:00
Vance Ingalls e794227908 Merge pull request #2913 from heygen-com/ffprobe-2-metadata
fix(engine): merge colour per field, detect alpha correctly, defer PNG parse
2026-07-31 19:57:33 -07:00
Vance IngallsandClaude Opus 5 9e275423e2 fix(engine): reject stdin input, decode stdout correctly, bound its size
Three issues in runFfprobe's process and stream handling.

A filePath of exactly "-" hung for 30 seconds. `--` stops option parsing,
so "-intro.mp4" is safe, but ffprobe rewrites "-" to `fd:` AFTER option
parsing and reads stdin — and stdin was an inherited pipe the parent
never writes to and never ends. The probe ran to the deadline and failed
with an empty diagnostic, because ffprobe never errored so stderr was
blank: 30010 ms and no message, against 28 ms for a normal missing-file
error. Rejected up front, and the child now gets stdio ["ignore", ...]
so no future invocation can block on stdin either.

stdout was decoded per chunk. `stdout += data.toString()` decodes each
64 KiB pipe chunk independently, so a multi-byte character straddling a
boundary became U+FFFD on both sides — verified: 200 KB of 3-byte
characters produced 15 replacements and a string 9 characters longer
than the source. -show_format output above ~64 KiB with non-ASCII tag
text returns silently mangled values, since JSON.parse still succeeds.
Now accumulated through StringDecoder.

Note on testing that one: U+FFFD is valid JSON string content, and
nothing on extractMediaMetadata's public surface exposes a tag value, so
there is no assertion that fails against the old implementation. Rather
than add a test that cannot fail, it is stated here and the bound below
is what the new test covers.

stdout was unbounded. stderr is capped by ManagedChildProcess but stdout
was not, and analyzeKeyframeIntervals emits one line per frame — an
all-intra ProRes proxy can produce an arbitrarily large string. Capped
at 8M characters, which real -show_streams JSON is nowhere near.

Tests: "-" rejected without spawning, the stdio shape, and the size
bound. Reverting the stdin guards fails 1. The first draft of the bound
checked before appending, so a single oversized chunk passed — the test
caught it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:53:01 -07:00
Vance IngallsandClaude Opus 5 361fd49926 fix(engine): allowlist AAC-LC for the packet refinement, not deny HE-AAC
The previous gate was a HE-AAC DENYLIST, so every other profile still
got the 1024-sample formula. ffprobe reports codec_name "aac" for all of
them; the framing lives in the profile:

  LC            1024 samples/frame   <- the only one this maths fits
  HE-AAC v1/v2  2048 output samples against a doubled sample_rate
  LD            512
  ELD           480
  Main/SSR/LTP  1024 nominally, unverified here
  xHE-AAC       variable

LD and ELD therefore had their already-correct container duration
overwritten with a value 2x / ~2.13x too large, and an unknown or
missing profile fell through — so an unrecognised HE spelling preserved
the exact truncation the previous commit set out to close.

Now an affirmative match on LC. Skipping the refinement is harmless:
format.duration is already correct before it runs.

Tests: 11 non-LC profiles (including LD, ELD, xHE-AAC, empty and
unrecognised) assert the container duration is kept AND that the second
probe is not launched; LC still refines, with whitespace tolerated. The
pre-existing duration table asserted that an UNPROFILED "aac" stream
refines — the behaviour under review — so it now states LC explicitly
and adds an unprofiled row that must not refine.

Also strengthened the `--` separator test while it was failing: it
compared a flattened count of 3 across three spawns, which one call
emitting three terminators would satisfy. Now asserts the last two argv
entries per call.

Reverting the allowlist fails 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:52:26 -07:00
Vance IngallsandClaude Opus 5 242a42f6c9 fix(engine): make the AAC duration refinement safe, cancellable and LC-only
The packet-count probe is a refinement — durationSeconds is already
correct from format.duration before it runs — but it was written as if
it were load-bearing.

It could fail the whole call. No try/catch, and `-count_packets` demuxes
the entire container against runFfprobe's fixed 30s deadline, so a long
AAC file on slow or network storage timed out and extractAudioMetadata
rejected. htmlCompiler catches that under the comment "Source file has
no audio stream", returns duration 0, drops the audio element, and the
render ships silent with no warning. Now caught, keeping the container
duration.

It ignored the caller's AbortSignal. Only the first probe received it,
so aborting during the packet probe let the child run to completion and
the call resolved with full metadata after cancellation — while
audioPadTrim's comment claims the wrapper preserves cancellation. The
signal is forwarded, and an abort still propagates rather than being
swallowed as a refinement failure.

It halved HE-AAC durations. ffprobe reports codec_name "aac" for
HE-AAC v1/v2 as well — the marker is in the profile field — and with SBR
each packet carries 2048 output samples against the doubled output
sample_rate, so the 1024 assumption computed exactly half. A 10:00
podcast became 5:00 and htmlCompiler truncated the audio there. Gated on
profile, with `profile` added to FFProbeStream.

Tests: probe failure, junk output, three HE-AAC profile spellings (which
also assert the second probe is not attempted), and that plain AAC-LC is
still refined. Reverting the guards fails 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:52:25 -07:00
Vance IngallsandClaude Opus 5 4d563fa752 fix(engine): guard the rounded frame rate and strict-parse rationals
Two paths the previous guards still let through.

Rounding could recreate Infinity after the finite check. `raw * 100`
overflows for a finite-but-huge rate — "1e307", "1e307/1" — so `rounded`
became Infinity and passed the positivity check, reaching exactly the
`-r Infinity` failure the finite guard exists to prevent. The rounded
result is now checked too.

The rational operands still used parseFloat. The plain-number path
switched to Number() so trailing garbage fails the whole string, but the
numerator and denominator did not, so "60fps/1", "60/1fps" and
"30garbage/1garbage" returned valid rates while the contract says
malformed frame rates fail closed. Both operands are now parsed strictly,
and an empty operand ("/", "/1", "30/") is rejected rather than coerced.

Tests: 8 malformed inputs and 3 overflow cases in the direct table.
Reverting either fix fails 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:52:17 -07:00
Vance IngallsandClaude Opus 5 19dc83bc2b fix(engine): reject non-finite, negative and malformed frame rates
parseFrameRate guarded its operands but not its result, so several
inputs produced values that are not usable frame rates — and nothing
downstream catches them, because callers use `meta.fps || 30`, which
only rescues 0 and NaN. Everything below was truthy and flowed into
buildEncoderArgs as `-r <value>` (rejected by ffmpeg mid-render) and
into frameCount arithmetic.

  "1e308/1e-10", "2/1e-320"  -> Infinity  (finite operands, infinite quotient)
  "-30/1", "30/-1", "-60"    -> negative  (sign never checked)
  "30/1/2"                   -> 30        (parts.length !== 2 fell through)
  "60fps"                    -> 60        (parseFloat stops at garbage)

Now: the quotient is checked rather than the operands, non-positive is
rejected, more than two parts is rejected, and the single-part path uses
Number() rather than parseFloat so trailing garbage fails the whole
string.

Separately, 2dp rounding collapsed any rate below 0.005 to exactly 0,
and the caller's 30fps default then re-encoded a 300-second 1/300-fps
timelapse as a ~1/30-second clip with frameCount 9000 for a 1-frame
file. Those floor to 0.01 instead.

parseFrameRate is now exported and tested directly. The previous table
drove it through extractMediaMetadata behind a spawn mock, costing a
vi.resetModules() plus a re-import of core's 238-file barrel per row
(74.9 ms vs 0.094 ms) — and 4 of its 7 rows produced identical values
against the pre-fix implementation, so it could not fail for the bugs it
existed to catch. The replacement fails 9 against that implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:52:17 -07:00
Vance IngallsandClaude Opus 5 62b96c227e fix(engine): merge colour per field, detect alpha correctly, defer PNG parse
Three defects in how ffprobe output and the PNG fallback are combined.

The cICP fallback was unreachable. `ffprobeColorSpace ?? stillImageMeta
?.colorSpace` discarded the PNG result whenever ffprobe returned ANY
colour field — and ffprobe emits color_space "gbr" for every PNG,
including a plain rgb24 with no colour metadata. So on the build the
parser exists for (reports gbr, does not decode cICP) an HDR PQ PNG
resolved colorTransfer "" , isHdrColorSpace() returned false, and the
still graded SDR. Now merged per field.

hasAlpha's anchor bound to one alternative. In
/(^|[^a-z])yuva|rgba|.../ the `|` is looser than concatenation, so
(^|[^a-z]) guarded `yuva` and nothing else. The list also omitted abgr,
ya8, ya16 and ayuv64, and `gray[a-z0-9]*a` matched only gray8a/gray16a —
names FFmpeg renamed to ya8/ya16 in 2013, so dead against modern builds.
A ya8 grayscale-plus-alpha PNG reported hasAlpha:false, resolveFrameFormat
picked jpg and the overlay flattened to an opaque rectangle. Replaced
with the start-anchored form studio-server already uses, extracted as
exported pixelFormatHasAlpha so the test asserts the shipped predicate
rather than a copy of the pattern.

The PNG parse ran eagerly and was discarded. It sat before the first
await, so readFileSync plus the CRC walk executed for every file before
a single ffprobe was spawned — a caller fanning out over
composition.images with Promise.all serialised entirely: 12 4K PNGs took
2649 ms against 170 ms probe-only, 2.5 s of event-loop stall that also
blocks Puppeteer IPC. On the happy path the value was then thrown away.
Now lazily memoized behind the paths that actually consult it.

Tests: 18 pix_fmt cases against the real predicate. Reverting the regex
fails 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:51:28 -07:00
Vance IngallsandClaude Opus 5 dd9c86d07a docs: correct the --hf-color-grading-intensity claim
Both pages said intensity does not scale a grade and that 0.5 renders the
same as 0. That is wrong, and the error was mine: I tested intensity only
against twoInkPrint and tapeDamage, then generalised from an effects-only
result.

The shader mixes ungraded against graded at u_intensity
(runtime/colorGrading.ts:1234), so it does scale adjust, wheels, curves,
hueCurves, secondaries and the LUT. Runtime tests pin 0.25 and 0.75
reaching the uniform. What it does not scale is details and effects —
grain, filmArtifacts, monoScreen, engraving, crosshatch, halftone,
twoInkPrint and the tape/CRT families are all applied after that mix
(:1235-1262), which is exactly what I had measured.

Both pages now say intensity ramps the primary grade only, and to animate
the specific effect when the look is effect-based.

Reported by miguel-heygen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:49:55 -07:00
Vance Ingalls e0c6fdfe0a Merge pull request #2912 from heygen-com/ffprobe-1-png
fix(engine): stop the PNG walk at cICP, anchor IHDR, use native crc32
2026-07-31 19:49:36 -07:00
Vance IngallsandClaude Opus 5 e4eac0c215 docs: address review on the color grading chapter
- Repoint the generated-artwork exit bridge at color-grading. The nav
  inserted the new page between generated-artwork and vfx-and-liquid-glass
  but the "Next" link still skipped it (miga-heygen, blocking).
- Correct the LUT custom property name: --hf-color-grading-lut-intensity,
  not --hf-color-grading-lut. Real transcription error, surfaced while
  checking the CSS-variable review comment against the source table in
  packages/core/src/colorGrading.ts:921-931.
- Add #t=0.1 posterframe hints and a contextual italic caption to all ten
  videos, matching the sibling prompting chapters.
- Reconcile British spelling to American throughout. The file path, nav
  slug, page title and every cross-reference were already "color".
- De-duplicate the animatable-effect caveat list: the chapter now links to
  the guide's Animating a Grade section instead of restating which effects
  are verified working, so the list is maintained in one place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:57:33 -07:00
Vance IngallsandClaude Opus 5 96a6e8bd95 fix(engine): keep the PNG CRC working on Node 22.0/22.1
zlib.crc32 landed in Node 22.2.0, but engine and cli both declare
`"node": ">=22"` and the runtime gate is major-only, so 22.0 and 22.1
are supported. A NAMED import of a missing export throws at module
EVALUATION — ffprobe.ts would have failed to load at all on those
runtimes, before any PNG was touched, taking every probe with it.

Namespace import plus a capability check, with the previous
bit-at-a-time implementation retained as the fallback. Modern runtimes
keep the 210ms -> 1.3ms win; older ones keep working.

Raising the floor to >=22.2.0 was the alternative, but that is a
user-facing support change and does not belong in a PNG bug fix.

Tests: the same HDR PNG parses identically with the native export
absent, and a corrupt chunk still rejects on the fallback path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:54:46 -07:00
Miguel Ángel 89e970fbd7 chore: release v0.7.87 (#2935) v0.7.87 2026-07-31 22:36:46 +02:00
Miguel Ángel ebea530806 Merge pull request #2933 from heygen-com/docs/studio-5419-capture-skill-guards
docs(skills): gate blocked website captures
2026-07-31 22:32:51 +02:00
Miguel Ángel 30f3830741 docs(skills): gate blocked website captures 2026-07-31 20:30:07 +00:00
Miguel Ángel 9ae0007261 fix(cli): preserve capture failure diagnostics 2026-07-31 20:30:07 +00:00
Miguel Ángel ac9458888c fix(cli): omit skipped Lottie previews 2026-07-31 20:30:07 +00:00
Miguel Ángel b38e907404 fix(cli): narrow blocked page titles 2026-07-31 20:30:07 +00:00
Miguel Ángel dfc60797a2 fix(cli): report bounded vision failures 2026-07-31 20:30:07 +00:00
Miguel Ángel 22e3cca966 fix(cli): reject blocked website captures 2026-07-31 20:30:07 +00:00
Miguel Ángel d3607606ee fix(cli): validate capture budget milliseconds 2026-07-31 20:30:07 +00:00
Miguel Ángel 49091e6142 fix(cli): propagate live capture budget 2026-07-31 20:30:07 +00:00
Miguel Ángel 45aead84c9 test(cli): cover bounded capture stages 2026-07-31 20:30:07 +00:00
Miguel Ángel 765a5ae83f fix(cli): bound capture runtime stages 2026-07-31 20:30:07 +00:00
杨慎 1738b5a11f docs: add THU-MAIC as HyperFrames adopter (#2761) 2026-07-31 16:11:17 -04:00
Miguel Ángel c6925e471a feat(studio): enable timeline virtualization by default (#2926)
* feat(studio): enable timeline virtualization by default

* fix(ci): measure timeline performance in production React
2026-07-31 19:43:47 +02:00
Miguel Ángel 723d3381c4 fix(studio): keep dense keyframes readable (#2925)
* perf(studio): define timeline viewport budgets and fixtures

* test(studio): gate timeline viewport performance in Chromium

* refactor(studio): isolate clip drag lifecycle

* refactor(studio): extract timeline render contracts

* perf(studio): centralize timeline viewport geometry

* perf(studio): follow playhead across virtualized rows

* perf(studio): add timeline clip-window index primitive

* perf(studio): virtualize timeline clip windows

* perf(studio): stop timeline scroll work when row virtualization is off

The row virtualization stack made the timeline publish a viewport snapshot
on every scroll frame and swap `renderClipContent` across every mounted clip
at gesture start and settle. Both are windowing concessions, and neither was
gated on the flag, so the build users actually run paid for them while
mounting all 1,000 clips anyway. Measured on a 3,000-clip project: median
scroll step 16.6ms to 76.9ms, p95 17.9ms to 189.4ms, 40 long tasks to 247.

Gate both on the row virtualization flag. The scroll path now stops at the
door when the flag is off, so `isScrolling` stays false and resize-driven
and programmatic syncs still publish through the immediate path.

The flag moves into its own module: the scroll-viewport hook needs to read
it, and the virtualization hook already imports the viewport snapshot type
back, which would have closed an import cycle.

Also release the perf fixture lease from the fixture rather than from the
test-hook effect. Loading a fixture writes player state, which changed that
effect's dependency identities and tore it down on the next frame, so the
lease was revoked moments after it was taken and live iframe discovery
overwrote the fixture before the gate could measure it.

The e2e gate gains a flag-off arm (`test:timeline-default`, 1,000 elements)
next to the existing flag-on one. It refuses the 50,000-element combination,
verifies from the mounted DOM that the server under test matches the
requested flag, and skips the DOM-size budgets for the unvirtualized build
rather than relaxing them, so a skipped budget never reads as a passed one.

Verified against a live Studio dev server on the fixture project:

  flag off, before: interactionP95 303.1ms, longest task 194ms, 0/5 runs pass
  flag off, after:  interactionP95  33.6ms, longest task   0ms, 5/5 runs pass
  flag on,  after:  interactionP95  33.2ms, 4/5 runs pass, exit 0

The flag-on arm's fourth run reproducibly reports a 55-58ms long task
against a 50ms budget. That is the residual tail of the window swap itself,
tracked separately and not addressed here.

* ci(studio): run the timeline viewport gate on studio changes

The gate has existed since the row virtualization stack landed but nothing
under `.github/` referenced it, so it only ever ran when someone ran it by
hand. That is how the flag-off scroll regression reached eight merged-ready
PRs without anything noticing.

Adds a `studio-timeline-viewport` job that boots two Studio dev servers, one
per flag state, and runs both arms of the gate against them. Two servers are
needed because row virtualization is read from `import.meta.env` at module
load, so one process cannot serve both builds.

Scoped to a new `studio` paths filter rather than the broad `code` one: the
gate only says anything about `packages/studio`, `packages/core` and
`packages/studio-server`.

Adds a `ci` tier. It applies the constrained budgets without any emulation,
because a hosted runner is already slower and noisier than the machine the
strict numbers were recorded on, while the existing `low-resource` tier would
throttle it a further 4x and measure the throttle rather than the build.

The fixture composition is tracked under `tests/e2e/fixtures` but Studio
resolves projects from the gitignored `data/projects`, so the job copies it
into place instead of a project directory being committed.

Both arms run in about 7 seconds each locally, so the job cost is almost
entirely dependency install and the workspace build it shares with
`studio-load-smoke`.

* fix(ci): preserve both timeline gate evidence arms

* ci(studio): report timeline gate arm statuses

* ci(studio): require timeline gate evidence artifacts

* fix(studio): keep dense keyframes readable

* fix(ci): resolve timeline stack audit findings
2026-07-31 18:05:22 +02:00
Miguel Ángel fbfffb1aa7 fix(studio): release retained preview resources (#2924) 2026-07-31 14:31:43 +02:00
Somansh ReddyandClaude Opus 4.8 a3c8f897f2 docs: unlist the Send-to guide from the docs nav (tool-served, not web-searched) (#2918)
The "Send to HyperFrames" authoring guide is an internal contract for the Claude
Design import flow, delivered exclusively by the get-send-to-hyperframes-guide MCP
tool (raw server-side fetch of the .md, returned as tool-result data). The llms.txt /
web_search discovery path it was published for is no longer used. Drop it from
docs.json so it stops appearing in the public product docs nav + llms.txt; keep the
.md file at its path (the tool's raw fetch URL depends on it) and the redirect note in
claude-design-hyperframes.md (points at the raw URL, not the docs page). Orphan guide
files build cleanly here (claude-design-hyperframes.md already is one).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 01:00:28 -07:00
Vance IngallsandClaude Opus 5 2af3f4d0ed fix(engine): stop the PNG walk at cICP, anchor IHDR, use native crc32
Three defects in the PNG metadata fallback, all introduced when the
cICP early return became an accumulator.

Corrupt trailing chunk nulls a good result. cICP must precede IDAT, so
continuing past it only visits chunks this parser ignores — while making
whole-file integrity a precondition for returning anything. A truncated
or bad-CRC chunk after cICP in an otherwise-good HDR PNG returned null,
and extractMediaMetadata then re-throws the ffprobe error it had
swallowed instead of using the fallback it just computed: the render
dies on a host without FFmpeg, or grades SDR on a build that does not
decode cICP. Now stops once dimensions and colour are known.

A second IHDR overwrote the dimensions. PNG permits exactly one, first,
but nothing enforced that here — a trailing [IHDR 1x1] replaced a real
3840x2160 and the producer laid out a one-pixel image. Anchored to the
first. The length guard was also `>= 8` against a spec length of 13,
which accepted a truncated header and read height out of the CRC bytes.

crc32 was hand-rolled bit-at-a-time and fed a Buffer.concat per chunk.
Since the walk no longer stops early it CRC'd whole files: 210 ms on a
12 MiB PNG, 647 ms on a 35 MiB 4K one, synchronously on the event loop,
plus ~11 MB of garbage per parse from concatenating a 4-byte type tag
onto every chunk. node:zlib's crc32 is native and takes a running seed,
so type and data hash in sequence with no copy. 210.28 ms -> 1.291 ms.

Tests: 5 regressions — corrupt-after-cICP, truncation after cICP,
second IHDR, short IHDR, and that a corrupt IHDR/cICP still rejects.
Reverting the break or the anchor fails 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:57:17 -07:00
Vance IngallsandClaude Opus 5 2517f1773f docs(guides): link colour grading agent guidance to the prompt chapter
The guide covers the contract; the chapter covers the two failures the
contract cannot express — choosing a source that has something for the
treatment to remove, and separating a subject so part of the frame can
be graded while the rest is protected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:03:16 -07:00
Vance IngallsandClaude Opus 5 819ed632d9 docs(prompting): add colour grading and film effects chapter
Ten A/B demos, each with the plain-language prompt that produced it and
the payload it compiled to. Slots into Level 4 — Substance.

The chapter teaches technique and leaves the key/bound reference to the
Colour Grading guide rather than duplicating it.

Also adds two sections to docs/guides/color-grading.mdx:

- Animating a Grade — the nine CSS custom properties, plus driving the
  payload from the timeline for effects that have none. Documents that
  --hf-color-grading-intensity does not scale a grade at render time,
  and that payload-rewrite animation is effect-dependent: verified
  working for halftone and twoInkPrint, verified not working for
  crtCurvature, scanlines, chromaBleed and chromaticAberration.

- Limiting a Grade to Part of the Frame — grading qualifies by value,
  never by screen position, so a region has to become its own layer.
  Includes the three layer recipes and a worked face-redaction example.
  Previously the support matrix said "not supported" with nowhere to go.

Renders are served from the CDN; docs/images/ is gitignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:55:07 -07:00
James Russo 3a6b7f0612 fix: align local WebGPU capture behavior (#2907)
* fix: align local WebGPU capture behavior

* fix: address WebGPU capture review feedback

* fix: retain overlapping GPU seek work

* fix: satisfy runtime seek completion types

* fix: drain concurrent GPU seek work

* fix: prevent WebGPU capture barrier starvation

* fix: keep WebGPU presentation active during render seeks
2026-07-30 21:52:30 -07:00
James Russo 1d636f603c refactor(producer): share plan execution builder (#2906)
## What

Refactor distributed planning around one shared local execution-plan builder:

- `buildLocalExecutionPlan()` now owns compile/probe/extract/audio/freeze.
- Legacy `plan()` remains a deprecated v1 transport wrapper.
- Plan v2 calls the shared builder directly and publishes through the existing manifest/CAS contract.
- Add neutral `createPlanV2FromExecutionPlan()`, `publishPlanV2FromExecutionPlan()`, `getPlanV2ExecutionPlanHash()`, and `PLAN_PROTOCOL_V1` names.
- Retain deprecated v1-named exports and wire aliases.
- Recommend explicit Plan v2 opt-in for new producer, Lambda, and Cloud Run integrations.

## Why

Plan v2 previously looked like it invoked a v1 planner even though v1 and v2 share the same frozen local execution representation. This removes that migration-era coupling while preserving the public minor-version compatibility contract.

## How

The shared builder returns neutral internal execution-plan fields. The v1 wrapper maps those fields back to the existing `PlanResult`; the v2 publisher consumes them directly.

Compatibility is intentional and covered by exact shape tests:

- omitted `planProtocol` still serializes/selects `"v1"`;
- v1 layouts, descriptor-less decoding, event unions, workflow branches, and exports remain;
- the v1 descriptor JSON is byte-identical and `CURRENT_PLAN_PROTOCOL` is an identity-preserving alias;
- v2 manifest bytes, key order, hash framing, and `sourcePlanV1Hash` wire key remain unchanged;
- no enumerable neutral hash field was added to manifests or returned result objects;
- v1/v2 result objects, cloud event payloads, and SDK handle key sets remain unchanged.

## Test plan

- Focused Plan v1/v2/protocol/export/size compatibility: 141 passed
- `@hyperframes/core`: 1,419 passed
- `@hyperframes/producer` unit lane: 990 passed
- `@hyperframes/aws-lambda`: 140 passed
- `@hyperframes/gcp-cloud-run`: 101 passed
- Producer, Lambda, and Cloud Run typechecks
- Repository-wide lint, format check, workspace/package-subpath checks
- Full workspace build
- `git diff --check`

- [x] Unit tests added/updated
- [ ] Manual testing performed
- [x] Documentation updated (if applicable)
2026-07-30 17:41:02 -07:00
Vance Ingalls fa564547dc Merge pull request #2904 from heygen-com/07-30-fix_cli_move_install-state_into_the_config_dir_so_deleting_it_is_a_full_reset
fix(cli): move install-state into the config dir so deleting it is a full reset
2026-07-30 15:13:00 -07:00
Somansh ReddyandClaude Opus 4.8 5a6e4b1a8f refactor(lint): consolidate asset-src placeholder skip into a shared predicate (#2894)
* refactor(lint): route asset-src skips through a shared isUnresolvedAssetPlaceholder predicate

Follow-up to the templating-token fix. The __UPPER__ + templating-token skip was
copy-pasted across the asset-src sites and had drifted: two non-lint sites carried only
the __UPPER__ half, and htmlCompiler's comment still claimed it "matches lint's skip"
after lint's skip became a superset. Extract one isUnresolvedAssetPlaceholder(rawSrc) in
@hyperframes/parsers/asset-resolution (both placeholder shapes, checked on the raw value)
and route every site through it: the four project.ts lint sites, hevcPreviewLint, and the
two previously-missed post-substitution sites (studio-server mediaCodecMap, producer
htmlCompiler). Remote/inline handling stays per-site (audio uses a narrower check).
Behavior-preserving for the lint sites (full suite green); the two non-lint sites are
post-substitution so they don't false-positive today, but now share one definition and
can't drift again. Adds unit tests for the predicate.

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

* docs(parsers): refresh hasUnresolvedTemplatingToken aside for the shared predicate

The parenthetical said the __UPPER__ shape keeps its own inline check at each call
site; this branch folded it into isUnresolvedAssetPlaceholder, so point there instead.
Addresses review nit.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 14:52:10 -07:00
Miguel Ángel b813b17345 fix(studio): prevent negative timeline latency telemetry (#2905) 2026-07-30 22:29:34 +02:00