Commit Graph
1204 Commits
Author SHA1 Message Date
Miguel Ángel f5a348e92d chore: release v0.8.95 (#4751) 2026-09-30 03:47:02 +00:00
Miguel Ángel daa44fcd75 chore: release v0.8.94 (#4744) 2026-09-29 17:41:30 -07:00
Miguel Ángel 2eeaefb3e5 chore: release v0.8.93 (#4733) 2026-09-29 13:14:35 -07:00
Miguel Ángel 6d00d94b46 chore: release v0.8.92 (#4729) 2026-09-29 15:47:29 +00:00
Miguel Ángel dcff54749c chore: release v0.8.91 (#4726) 2026-09-29 06:33:17 +00:00
Miguel Ángel e4fbc84874 chore: release v0.8.90 (#4721) 2026-09-28 19:29:59 -07:00
Miguel Ángel ea4893602a chore: release v0.8.89 (#4720) 2026-09-28 19:10:32 -07:00
Miguel Ángel 343cf381f7 chore: release v0.8.88 (#4719) 2026-09-28 18:33:19 -07:00
Miguel Ángel 82ae73a031 chore: release v0.8.87 (#4717) 2026-09-28 18:02:10 -07:00
Miguel Ángel 663b02297d feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk (#4701)
* feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk

`hyperframes clean` lists leftover render work dirs, debug renders, the history of projects
that no longer exist, idle video proxies and extracted video frames, with sizes, and removes
them. `--dry-run` removes nothing, `--json` is for agents, `--snapshots` also removes
snapshot folders (otherwise listed as reclaimable). It never touches outputs, sources or
anything a running render or preview still owns.

* docs(cli): say that clean does not search a project's subfolders

* fix(cli): clean test compares folder names on Windows and the skills manifest is regenerated

* fix(studio-server): the proxy cache sweep removes only files the transcoder named

It removed any idle .mp4 or .webm in .transcode-cache, so a user file placed there, or reached through
a symlinked cache folder, could go. Proxies are always <sha256>.<ext> and their temp files
.tmp-<uuid>-<proxy name>; nothing else is swept.

* style(producer): keep a renderDirOwner doc comment within 120 columns
2026-09-29 00:43:42 +00:00
Miguel Ángel f7ef18dca8 fix(producer): renders leave no captured frames behind when interrupted (#4694)
Ctrl+C during a render left its whole work dir of captured frames (and a
staging dir) in the renders folder. Puppeteer's default SIGINT handler kills
Chrome and exits the process before the render's own cancel path can remove
them. The engine keeps that default unless the host owns Ctrl+C; the CLI's
render cancellation scope claims it while a render runs, so the render aborts
and cleans up after itself.

A render killed outright (SIGKILL, crash) cannot clean up, so each work dir and
staging dir now records its owner process (pid, hostname, boot, and on Linux
the pid namespace), and the next render into the same folder removes
the ones whose owner has exited.

Cancelling a render while its browsers start no longer leaves anything
running that kept a batch render alive after Ctrl+C: the BeginFrame warm-up
loop, the probe browser and file server, or a capture browser whose launch
finished after the cancel (now closed before the worker settles, since
Puppeteer's exit hook skips every other live browser).
2026-09-28 21:58:06 +00:00
Miguel Ángel 1004b69829 chore: release v0.8.86 (#4695) 2026-09-28 13:37:47 -07:00
Miguel Ángel a46095f1cc chore: release v0.8.85 (#4676) 2026-09-28 10:52:18 -07:00
Miguel Ángel 52f849e2b4 chore: release v0.8.84 (#4671) 2026-09-28 10:00:31 -07:00
Miguel Ángel 8fd5fd5a32 chore: release v0.8.83 (#4668) 2026-09-28 09:27:18 -07:00
Miguel Ángel 5257dd581f fix(studio): tone-map HDR preview proxies with the render tone map (#4606) 2026-09-28 15:48:58 +00:00
Miguel ÁngelandKenneth Shortrede 3a64ea867c fix(engine): forced-SDR renders show tone-mapped HDR footage as Studio does (#4600)
* fix(engine): forced-SDR renders show tone-mapped HDR footage as Studio does

Forced-SDR extraction tone-maps HDR frames with zscale, but the frames kept the
BT.709 transfer tag, so Chrome re-curved them (a tone-mapped 388d96 painted as
2d8790), and JPEG frames kept the BT.709 matrix every JPEG reader decodes as
BT.601. Tone-mapped frames now get the same sRGB tag and BT.601 JPEG conversion
as other SDR frames.

macOS used VideoToolbox, which does not tone-map: its frames stay PQ and Chrome
tone-maps them itself, darker than the hable curve Linux and Studio use. macOS
now takes the zscale path whenever ffmpeg has zscale, found through the shared
ffmpeg filter probe; without it the old path stays and the render warns once.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): forced-SDR renders tone-map HDR footage that lacks matrix or primaries tags

zscale finds no path between colourspaces without them, so the extraction
failed. Untagged HDR footage is BT.2020 in practice, and now gets those tags.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): forced-SDR tone map also tags the transfer ffprobe read from the container

From ffmpeg 8.1 ffprobe reports a container-only PQ or HLG tag; the frames
still lack it, so zscale failed where macOS VideoToolbox used to render.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* test(engine): gate the container-only transfer case on the ffprobe that reads it

* docs(engine): shorten the filter-probe comment to the comment limit

* fix(engine): keep valid per-frame colour tags when tone-mapping HDR footage

* test(engine): build the mixed HLG and PQ fixture without B-frames so its timestamps are valid

* test(engine): build the trimmed mixed fixture without B-frames too

---------

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>
2026-09-28 08:21:13 +00:00
Miguel Ángel 5969c6894b chore: release v0.8.82 (#4641) 2026-09-28 08:04:14 +00:00
Miguel Ángel e8313b2b39 fix(engine): renders show the video frame the preview shows at each output frame (#4596)
* fix(engine): renders show the video frame the preview shows below the source frame rate

* refactor(engine): one condition owns CFR sampling at the output rate

* fix(engine): webm and mkv clips keep every frame when rendered at their own rate

* fix(engine): only fine timebases get the half-tick sampling shift, so coarse ones stay exact

* docs(engine): the sampling comment states the half-tick bound it keeps

* fix(engine): move the extraction cache to v6 so main v5 entries are not reused

* docs(engine): keep extraction comments within their share

* test(producer): regenerate four 25-to-30 fps baselines for the exact frame pick

* test(engine): name the v6 schema prefix in its test
2026-09-28 01:38:09 +00:00
Miguel Ángel c1bf4aefff chore: release v0.8.81 (#4605) 2026-09-27 16:41:42 -04:00
Miguel ÁngelandKenneth Shortrede c9b3d9c962 fix(engine): SDR renders keep footage and page colours as in the source (#4564)
* fix(engine): declare SDR extracted frames as sRGB so Chrome does not darken footage

* fix(engine): encode SDR mp4 frames with the BT.709 matrix they are tagged with

Chrome hands the SDR encoder BT.601 full-range JPEG captures (or RGB PNGs).
The encoders only converted the range, so swscale kept the BT.601 matrix under
the BT.709 tag (ffmpeg 5.1 to 7.0: CSS #C83C28 delivered as #D44824) or
converted YUV to YUV through a low-precision step (ffmpeg 8+: #101010 delivered
as #0B0D0D). Decode to RGB with the capture matrix, then encode with BT.709.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): tag SDR frames plain sRGB so every ffmpeg writes the sRGB chunk

ffmpeg writes a PNG sRGB chunk only for BT.709 primaries and writes cICP only
from 6.1. On ffmpeg 5.1 (Debian bookworm) a BT.601-tagged source therefore got
cHRM plus gAMA 1/2.2, which Chrome decodes as a pure 2.2 power: 0x10 shadows
rendered as 0x07. Declaring BT.709 primaries too gives the sRGB chunk on every
version, so Chrome shows the frame code values for every SDR source. The test
now reads the PNG chunks instead of ffprobe, which reports no transfer for an
sRGB chunk on ffmpeg 5.1.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): extract SDR jpg frames as the BT.601 full range JPEG readers assume

The default frame format for opaque SDR sources is JPEG. ffmpeg converted the
BT.709 source to full-range YUV without changing the matrix, and every JPEG
reader (Chrome included) decodes BT.601, so footage shifted: 0xC83C28 showed as
0xBB2D29. The old mp4 encode made the inverse mistake and hid it; with the
encode fixed, it would reach the output. Decode through RGB with the source
matrix, then encode BT.601 full range, keeping 4:2:0.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): round the SDR mp4 colour conversion accurately

swscale's default rounding moved saturated colours up to 3 codes (pure red 254
delivered as 251 on ffmpeg 5.1 to 7.0); accurate_rnd brings that to 1. The
chunk encoder now decides the HDR-or-SDR filter once, and the 10-bit GPU pad
test sets hdr, which its name always claimed.

* style(engine): keep the colour filter reasons in names and tests, not source comments

The comment ratchet caps a file's comment share: the new filter module and the
extractor carry their intent in constant names, and the encode test states why
the conversion goes through RGB.

* fix(engine): keep colour edges sharp through the SDR colour conversions

Going through RGB upsampled chroma with swscale's default filter, so ffmpeg 4.4
to 7.x smeared colour across edges by about 2 px (red 254 next to blue came out
8 inside the blue). Nearest-neighbour resampling maps the half-resolution chroma
back one to one, keeps the flat-colour results, and makes accurate_rnd redundant.
The pixel tests now also check a red to blue edge.

* test(producer): regenerate goldens whose colours the old mp4 matrix bent

These four fixtures compare at 40 to 45 dB, and their goldens were rendered
with the BT.601-under-BT.709 encode, so the corrected colours score 34 to 37 dB.
Regenerated in the Dockerfile.test image. Each new golden is closer to the
colour its page declares, e.g. #00d4aa was 00bcab and is now 00d3aa, and
#087443 was 006643 and is now 087241.

* fix(engine): keep VFR frame timing when extraction applies colour filters

ffmpeg before 6.1 ignores frame durations in CFR resampling once any -vf is
set, so a VFR window ending on a still lost frames. Filtered VFR extraction
now resamples in one process exactly as before and filters in a second.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): encode raw SDR layered frames in BT.709

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* test(engine): keep the new colour tests inside the format, comment and argv checks

* fix(engine): keep HDR and unusual pixel formats out of the two-process VFR path

The raw handoff dropped HDR10 light levels and misread semi-planar formats, and
decoder options rejected some ffprobe colour names. Only SDR sources in formats
nut carries unchanged now take it, their colour tags come back through setparams,
and a process that cannot start releases the other.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): read untagged footage with the matrix Chrome plays it with

Chrome decodes an untagged matrix as BT.709 from 720 coded lines up and as
BT.601 below; extraction assumed BT.601 at every size, so HD footage without
colour tags shifted hue against Studio's own preview.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): untagged VP9 and AV1 footage keeps BT.601, as Chrome plays it

Chrome applies the 720-line rule only to codecs it decodes through ffmpeg
(H.264, VP8); untagged VP9, VP9 alpha and AV1 play as BT.601 at every size.
Also widens the raw-pipe pixel formats to all verified ones, and reports the
pipeline process that failed first by settle order, not by per-process clocks.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* test(producer): regenerate goldens for corrected page colours and untagged SD footage

* docs(engine): say which untagged codecs the matrix rule was measured on

---------

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>
2026-09-27 11:16:35 +00:00
Miguel Ángel fcfcafa445 chore(release): cut v0.8.80 (#4591) 2026-09-27 07:26:39 -04:00
Miguel Ángel 3def3514d1 fix(engine): export draws a video in exactly the frames it is shown in (#4565)
* fix(engine): export draws a video in exactly the frames export visibility shows it

* test(producer): guard that chunk frame planning snaps clip windows like export

* refactor(engine): set the render fps once per frame table and name its clip window

* chore(engine): trim frame table comments
2026-09-27 03:05:57 +00:00
Miguel Ángel 984ad28c7c fix(core): clips that meet within float rounding count as meeting in edits and playback (#4535)
* fix(core): clips that meet within float rounding count as meeting in edits and playback

* fix(engine): hand a shared clip boundary to the incoming clip in export, as preview does
2026-09-26 22:14:14 +00:00
Miguel Ángel 0ae223773a chore(release): cut v0.8.79 (#4548) 2026-09-26 21:05:52 +00:00
Miguel Ángel 23937497a9 chore(release): cut v0.8.78 (#4515) 2026-09-25 22:58:59 -04:00
Miguel Ángel dd88b5f5b7 chore: release v0.8.77 (#4489) 2026-09-25 12:51:35 +00:00
Miguel Ángel 8b4c3a1d3d chore: release v0.8.76 (#4485) 2026-09-25 11:20:26 +00:00
Miguel Ángel fba943ea3a fix(cli): render --quiet stops printing producer info and trace lines (#4478) 2026-09-25 10:39:01 +00:00
Miguel Ángel f421851bdd fix(producer): give each parallel segmented-capture worker its own browser (#4475)
* fix(producer): give each parallel segmented-capture worker its own browser

Segmented capture with two or more workers failed at the first frame with
"Invalid JPEG input at frame 0: empty frame". Every worker opened its page in
one pooled Chrome, and beginFrame capture does not survive a shared browser:
a worker's first beginFrame returned no screenshot. Fresh worker sessions now
follow shouldDisableBrowserPoolForParallelWorker, the rule the parallel disk
path already uses.

The default session factory was also built once per worker, so with a probe
session every worker reused that same page. It is now built once per run, so
only the first worker takes the probe.

Workers are now created inside the try, so when a later worker fails to
launch, the finally still closes the browsers opened before it.

* fix(producer): close a segmented worker session whose initialization fails

The default segmented session factory opened a fresh browser and then
initialized it with nothing owning it on failure, so a page-load or init
timeout leaked that Chrome. It now closes the session and rethrows, as the
streaming stage does.
2026-09-25 06:42:59 +00:00
Miguel Angel Simon Sierra a08433acef chore: release v0.8.75 2026-09-24 21:51:48 -04:00
Miguel Angel Simon Sierra 3a47b46057 chore: release v0.8.74 2026-09-24 17:04:02 -04:00
WaterrrForeverandClaude Opus 5 9d4836902b fix(engine): name the data-no-timeline opt-out in the timeline-timeout warning (#4410)
* fix(engine): name the data-no-timeline opt-out in the timeline-timeout warning

When the sub-composition timeline wait times out, two things are emitted:
the stderr line inside pollSubCompositionTimelines, which names the pending
composition ids and the data-no-timeline opt-out, and the structured warning
on the session, which said only "Sub-composition timelines did not become
ready within 45000ms".

The structured one is what a programmatic caller reads: render re-prints it
in the completed_with_warnings summary and returns it in the result's
`warnings` array, so a CI consumer or an SDK caller sees the symptom with no
remedy attached. It also reads as a fault to diagnose, when the commonest
cause is a composition that legitimately has no GSAP timeline -- CSS
animations or rAF -- and is paying the full playerReadyTimeout on every
render because nobody told the author the wait is theirs to switch off.

The message now says the wait can be intentional, names data-no-timeline as
the opt-out, and keeps the async-setup case as the other branch. The pending
composition ids were already computed for the stderr line and thrown away;
an optional onPending callback carries them to the session so the structured
warning and its details can name them too, matching the script-failure
branch, which already names its failed sources. The stderr text is unchanged.

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

* fix(engine): normalise the pending-id result instead of casting it

The enumerate step cast `page.evaluate`'s result with `as string[]`, which is a
claim the call site cannot enforce: `evaluate` is loosely typed, and anything
that returns a non-array turned the cast into a TypeError thrown on the very
path that exists to REPORT a problem. Normalise the result instead.

`pendingCompositionIds` was also never declared on `CaptureWarning.details`,
whose type is closed, so the engine build failed and every downstream package
failed with it.

Adds coverage for the poll's own `onPending` handoff, which the existing tests
in this PR could not see: they exercised `recordSubTimelineWarning` (the
message text) and never `pollSubCompositionTimelines` (where the throw was).
The non-array case is the one that discriminates this fix from the tempting
alternative of leaving the cast and adjusting the stubs.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 02:29:35 +08:00
WaterrrForeverandClaude Opus 5 250c9bd509 fix(engine): resolve the version by walking to our own package root (#4409)
readEngineVersion resolved ../../package.json relative to import.meta.url.
That path is correct from packages/engine/src/utils/ in the repo and
overshoots the package root from <pkg>/dist/ in the published CLI, which
bundles the engine. The require threw MODULE_NOT_FOUND, a bare catch
swallowed it, and every render stamped the 0.0.0-dev fallback.

No single relative literal is correct in both layouts, so resolve by walking
up to our own package root instead of by a fixed depth.

The name check is the fix, not a refinement. A bare "first package.json
found while walking up" resolves to whatever sits above in a hoisted install
and returns a confident wrong version - the same class of defect as the
sentinel, and harder to notice because the answer looks plausible. Only a
package.json whose own name is hyperframes or @hyperframes/* may answer.
A test pins this: removing the name check fails that case and only that case.

Also replaces the sentinel. 0.0.0-dev parses as a version, compares against
one, and sorts below every release, so a total resolution failure was
indistinguishable from a dev build - which is why it shipped unnoticed. The
fallback is now "unresolved", deliberately not semver-shaped, and every
failure path warns with the reason. A failed lookup still never breaks a
render.

The old comment claimed the engine ships as raw TS and exports
./package.json. The published package has no exports field and bundles the
engine, so that premise does not hold where the code runs; it is replaced
with the two layouts and why they conflict.

Refs #4365

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 02:29:30 +08:00
Miguel Ángel 6f7f463fc5 chore: release v0.8.73 2026-09-24 11:53:31 -04:00
Miguel Ángel cbaddec51a fix(engine): download tests stop racing real deadlines, and failures name their cause (#4419)
* test(engine): hold the download attempt deadline so slow runners cannot trip it

urlDownloader tests passed a real 1s deadline that a slow Windows attempt outlasted.

The deadline timer is now held; the one test that needs it fires it by hand.

* fix(engine): name the underlying error when a download fails, without its URL

* test(engine): fire the stalled-body deadlines by hand instead of racing a real 20 ms timer
2026-09-24 10:58:53 -04:00
Miguel Ángel 6a0fd68584 chore: release v0.8.72 2026-09-24 07:23:30 -04:00
Miguel Ángel 58c873d816 chore: release v0.8.71 (#4395) 2026-09-23 23:32:31 -07:00
xiayewang-heygen 7d1d9ddf70 Merge pull request #4368 from heygen-com/fix/ffprobe-nb-frames-duration-crosscheck
fix(engine): cross-check nb_frames when video stream duration is absent
2026-09-23 21:30:11 -07:00
Miguel Ángel fc9071c1fc chore: release v0.8.70 (#4389) 2026-09-23 22:10:27 -04:00
Miguel Ángel 2c4bf075b5 chore: release v0.8.69 (#4376) 2026-09-23 20:13:08 -04:00
xiayewang-heygenandClaude Opus 4.6 b021cb77be fix(engine): reconcile metadata duration after extraction delivers fewer frames (#4371)
When the probed video stream duration is inflated (container duration
fallback includes a longer audio track), FFmpeg delivers far fewer
frames than expected — typically exactly 50% for stock clips whose
audio is 2x the video length. The coverage gate then aborts the
render because it sees 50% coverage against the inflated expectation.

After extraction, if FFmpeg delivered <=60% of the predicted frames,
correct `metadata.videoStreamDurationSeconds` to the actual video
extent. This reconciliation feeds through to the coverage check via
the same `metadata` reference, so `expectedFramesForVideo` sees the
true duration without any interface changes.

This is a corrective complement to the preventive nb_frames
cross-check in extractMediaMetadata — it catches cases where
nb_frames is unavailable (MKV, live streams, fragmented MP4).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-23 23:06:37 +00:00
xiayeandClaude Opus 4.6 ee4c00db6a fix(engine): cross-check nb_frames when video stream duration is absent
When ffprobe omits the video stream's own `duration` field,
`extractMediaMetadata` falls back to the container duration
(`format.duration`), which includes the longest stream in the
container. Stock and looping clips commonly have an audio track
2x the video length, inflating `videoStreamDurationSeconds` and
causing the downstream coverage gate to expect twice the frames
FFmpeg can actually produce — the "50% cluster" failure pattern.

Cross-check with `nb_frames / fps` when the stream duration is
absent. If the frame-derived duration is >10% shorter than the
container, use it instead. This corrects the inflation at probe
time without requiring a second ffprobe pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-23 15:37:35 -07:00
Miguel Ángel 86c5c53d25 chore: release v0.8.68 (#4361) 2026-09-23 17:16:00 -04:00
Miguel Ángel 9cee2bd345 chore: release v0.8.67 (#4354) 2026-09-23 16:14:31 -04:00
Miguel Ángel 4efe5f206d chore: release v0.8.66 (#4344) 2026-09-23 13:29:38 -04:00
Val 30f548f62b fix: a stalled remote asset download no longer hangs a Cloud Run render (#4343)
* fix(gcp-cloud-run): run the Cloud Run image on Bun 1.4.2 so remote downloads cannot hang

* fix(engine): make the download deadline abort the body pipeline
2026-09-23 16:38:58 +00:00
Miguel Ángel aabfba5445 chore: release v0.8.65 (#4336) 2026-09-23 11:03:11 -04:00
Vance Ingalls ed75203cb6 chore: release v0.8.64 (#4326) 2026-09-23 04:03:02 -07:00
miga-heygenandMiguel Angel Simon Sierra 99221c50a5 chore: release v0.8.63 (#4312)
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-09-23 05:49:08 -04:00