2783 Commits
Author SHA1 Message Date
Miguel Ángel 9aa90f6e3e chore: release v0.7.99 2026-08-07 15:31:57 +00:00
Miguel Ángel dd629697d3 fix(cli): harden publish retry behavior 2026-08-07 15:13:04 +00:00
Miguel Ángel f2d6ce3245 fix(cli): recover transient publish failures 2026-08-07 14:53:20 +00:00
Vance Ingalls 6114749d8e chore: release v0.7.98 2026-08-06 23:37:23 -07:00
Vance IngallsandClaude Opus 5 a3d13e2673 fix(producer): record routing state on the failure path
de_parallel_router is present on 95.4% of render_complete events and 0.83%
of render_error. Capture context itself survives failures fine (capture_mode
is on 98.6% of them), so this is not renders failing before capture — the
routing state specifically is being dropped.

Cause is ordering. deParallelRouter is assigned twice: once before the
capture-observability update, and again inside syncCapturePlan where routing
is actually resolved — including the 'reverted' case, which the earlier
assignment cannot know. The update in between recorded whatever was true
first, so a render that failed while routed reported no routing state at all.
The existing comment at the earlier call site says it is recorded there
precisely so hard failures carry it; that intent was correct and the value
just arrived too late.

This matters for the #2840 ramp specifically. The per-install circuit breaker
only arms on a revert, which requires the render to finish and self-detect —
it cannot catch a crash or hang. Those are exactly the failure modes a
percentage ramp exists to bound, and they were the ones telemetry could not
see.

Also makes the ffprobe contract sweep resilient per entry. A dangling symlink
under packages/studio/data/projects threw ENOENT on stat and aborted the whole
traversal, so every package sorting after 'studio' — both studio-server
callers included — silently stopped being checked. main is currently red on
this. The manifest assertion is what caught it, which is what it was added
for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 22:41:26 -07:00
Miguel Ángel c4b41072c7 chore: release v0.7.97 2026-08-07 05:16:27 +00:00
Miguel Ángel c7b2199650 Merge pull request #3076 from heygen-com/fix-studio-resize-box
fix(studio): resize an animated element to the size it was dropped at
2026-08-06 18:01:05 -07:00
Vance Ingalls 066af73c13 Merge pull request #3075 from heygen-com/release/v0.7.96
chore: release v0.7.96
2026-08-06 17:36:58 -07:00
Miguel Ángel 12e637fb25 fix(studio): cover legacy resize boxes 2026-08-07 00:31:27 +00:00
Miguel Angel Simon Sierra a693b12cca fix(studio): correct against the scale the resize actually commits
A near-uniform drag collapses to the `scale` shorthand, but the finalize step
measured the element at the per-axis pair it computed rather than the single
value the commit writes. The element was measured at a scaleY the file never
gets, so the position correction came out tilted by the difference.

Adds a sweep over the shapes a composition produces — shrink, grow, first
resize, rotated, steeply rotated, non-uniform, near-zero, inline-sized, no
position write, animated position, and two drags in a row — each checking the
element renders on its drop point from the PERSISTED scale and position. The
geometry model is calibrated against real gesture traces: the same inputs
reproduce the rects the browser reported to three decimals.
2026-08-06 17:21:51 -07:00
Miguel Angel Simon Sierra ee5ae9619c fix(studio): measure the resize correction from the pre-gesture position
A scale resize measured its drop-point correction while the gesture's own
translation was still applied, but the position commit adds that correction
onto the element's PRE-gesture position, which it reads from the gesture's
base attributes. The two disagreed by the whole drag distance, so the commit
persisted a position a drag-length from where the element was dropped: it
held the drop point for one frame and then slid off.

Move the element back to that base before measuring, so the residual and the
commit share one origin. For an element whose position is a static hold that
usually means no correction at all, which is the right answer: scaling about
the centre already leaves it on the drop point.
2026-08-06 17:05:37 -07:00
Miguel Angel Simon Sierra b18fd62e0e fix(studio): keep an animated element on the drop point when resized
An element whose position is animated left the drop point anyway. The
finalize step wrote its correction as a static position hold, and the
element's position tween rendered its own value a frame later and won.
Before that it stood down entirely on such elements, on the grounds that
a keyframed path has no single anchor to preserve, which had the same
visible result: the element moved.

It has an anchor, the frame the user is looking at. The correction now
goes into that tween at the playhead, through commitGsapPositionFromDrag,
which is the same commit a drag on the same element already uses. Static
holds keep the existing path.

This is the difference the debug log showed between an element carrying
position:to, which moved after release, and one carrying position:set,
which did not.
2026-08-06 17:02:14 -07:00
Miguel Angel Simon Sierra 7dc18771d1 fix(studio): pin the drop point on a first resize
The finalize step measures where the committed scale put the box and
shifts the position hold by the difference. Whether the commit had
actually rendered when it measured was luck: on a first resize the
timeline had not re-seeked, so it measured the element at its natural
size still sitting on the drop point, saw no residual, and skipped the
correction. The scale then landed, GSAP rendered it about the element's
centre, and the element jumped by the whole drag distance. Elements
resized before got a correction only because their previous scale made
the residual non-zero by accident.

The committed scale is now applied to the live element before measuring,
so the measurement means what its comment says either way, and a skipped
correction is logged rather than silent.

Confirmed against a real session: a first resize of a 630px chip now
reports residual -109.93 and lands on the drop point, where it previously
logged no scale-finalize at all.
2026-08-06 17:02:14 -07:00
Hblee 9769ba2c7b feat(sdk): expose a paint query for transparent-composition hit-testing (#3070)
* feat(sdk): expose a paint query for transparent-composition hit-testing

A host layering a transparent composition over other content has to know
whether a point carries ink before it decides to swallow a click. The adapter
only answered "what element is here", so AI Studio wrote its own answer and
could not reach the per-pixel alpha the adapter already samples for <img>.

Add PreviewAdapter.paintsAt, plus the pieces it is built from on a new
./adapters/iframe subpath so a host with different hit-test policy can compose
its own walk.

The walk is geometric rather than elementsFromPoint-based: that stack omits
pointer-events:none nodes, and a decorative overlay carrying it still paints,
so a z-stack query would report no ink over visible artwork — the direction
that makes a composition vanish from under the cursor.

fullBleedFraction is an option rather than a constant because "a layer covering
the whole frame is background, not artwork" is host policy, not a fact about
the composition.

* fix(sdk): scope the full-bleed frame to the root under the point

compositionFrameArea took the smallest [data-composition-id] in the whole
document, so an unrelated sub-composition sized the reference frame for points
nowhere near it: a 300x300 badge in a corner made every mid-size painter in a
1920x1080 outer frame read as full-bleed, and the composition went
click-through under artwork the user can plainly see. That is the direction the
fail-safe exists to avoid, and the docs already described the intended
behaviour — the innermost root CONTAINING the point.

Also read the alpha channel instead of matching known transparent spellings.
Only the `transparent` keyword computes to rgba(0, 0, 0, 0); a faded-out white
stays rgba(255, 255, 255, 0), which the set counted as painted. That erred
toward absorbing clicks rather than losing them, so it was a false positive
rather than a hazard, but it is wrong.

Both are pinned by tests that fail when the fix is reverted.

* fix(sdk): stop the paint query answering "no ink" over visible artwork

Four cases where the walk landed on the wrong side of its own fail-safe.

The full-bleed veto tested the winner's border-box area even when the alpha
sampler had just read an opaque pixel there, so a full-frame transparent PNG or
SVG overlay — the case this feature exists for — reported background over
visibly opaque artwork, with no fullBleedFraction that worked. Ink now carries
how it was established, and a measured pixel is never vetoed. An image whose
pixels could NOT be read stays inferred, so a tainted CDN overlay still yields
to the veto rather than absorbing every click.

Composition roots were excluded from candidacy outright, so a root carrying a
background answered false even at fraction 0, where the docs promise every
painting box counts. Roots are candidates now; the veto discounts them without
a special case, since a root's box is the frame.

An <img> with a clear pixel early-returned past its own background, padding
plate and border, which any other element would have counted.

A same-origin iframe mid-navigation exposes a readable but empty document, so
the !doc guard never fired and a loading composition answered a confident
"no ink" — the exact failure the null convention exists to prevent.

Also: the sort comparator's epsilon tie was intransitive, leaving the
smallest-first guarantee (and the lazy single-sample property that rides on it)
engine-dependent; the guide's pass-through recipe called a function that does
not exist and hand-waved the coordinate mapping that makes it correct; and the
reference now states the under-counts alongside the over-counts, the walk's
blindness to runtime-mounted content, and compositionPaintsAt's preconditions.

Each fix is pinned by a test that fails when the fix is reverted.

* refactor(sdk)!: invert the paint query to isProvablyEmptyAt

paintsAt handed callers three falsy bottom values with opposite safe readings:
false meant "no ink, pass the click through", null meant "not knowable, treat as
painted", and undefined from an adapter without the method also meant painted.
The idiomatic `if (!preview.paintsAt?.(x, y)) passThrough()` therefore did the
dangerous thing for two of the three, and the convention needed defending in the
interface docstring, the reference and the guide — plus a dedicated comment and
a pinning test on the headless adapter to stop null regressing to false.

Inverting the polarity collapses the tri-state to a plain boolean and makes the
safe reading structural: true only when the composition was readable and nothing
painted there, so ink, an unreadable or still-loading document, and a missing
implementation all land on "keep the composition clickable". The prose stays as
rationale, but nothing depends on a reader remembering it.

PaintsAtOptions becomes PaintQueryOptions, since it now describes the walk that
both the adapter method and the exported compositionPaintsAt share rather than
one method's arguments. compositionPaintsAt keeps its ink-positive name: it
answers the other question, and its docstring points callers who need the
fail-safe contract at the adapter.

Nothing is released yet, so no consumer is on the old name.
2026-08-06 16:27:38 -07:00
Miguel Angel Simon Sierra 20ef798620 fix(studio): stop a uniform resize writing a scale GSAP ignores
A uniform drag committed the `scale` shorthand. If the tween's keyframes
already stated `scaleX` and `scaleY`, the commit left both forms in the
same keyframe, and GSAP animates each property name independently, so the
longhands ran alongside the shorthand and won.

The resize therefore computed the right number, wrote it to the file, and
did nothing: the element snapped back to its old size the moment the
handle was released. Reproduced from a real session, where a drop at 384px
on a 630px element wrote {scaleX: 1, scaleY: 1, scale: 0.61} and rendered
at the original size.

The mixing hazard was already known in the other direction, where a
non-uniform drag takes a rewrite path that normalizes every keyframe to
the longhands. This makes the condition symmetric: whenever the tween
already speaks longhands, a uniform drag speaks them too.
2026-08-06 16:18:13 -07:00
Miguel Angel Simon Sierra 477f77629b fix(studio): resize from the element's real box, not a 200px guess
Resizing an element whose size is driven by a scale animation committed a
scale computed against a hardcoded 200px fallback, because the only
original size the draft recorded was the element's INLINE width, and a
composition sizes its elements from the stylesheet.

A 630px chip dropped at 1260px wide committed a scale of 6.3 instead of 2,
so it landed at over three times the size it was dropped at. The next drag
compounded it, because that wrong scale then counted as the element's live
one.

The draft now records the box it measured, once, before it writes a width
of its own, and the intercept reads that. The inline attributes keep their
own job of restoring an inline style, which is why they cannot answer this
question.
2026-08-06 15:53:36 -07:00
Vance Ingalls 026e6941ac chore: release v0.7.96 2026-08-06 14:53:26 -07:00
Vance IngallsandClaude Opus 5 b3990ac789 feat(studio): emit the canary reason on Studio events too
Review caught that the same anti-pattern was still live in the Studio binding:
canaryEventProperties destructured only `enabled` and dropped the reason. Its
own doc comment promised 'identical shape to the CLI, so a rollout spanning
both reads as one flag' — which the CLI-only fix had just made false.

This matters beyond symmetry. A CLI-launched Studio adopts the CLI's decisions
and shares its bucket seed, so a cohort flip can surface on either surface.
Emitting attribution on only one leaves Studio-observed flips unattributable
and makes the two flip counts irreconcilable — and Studio is the surface most
likely to expose a shared-seed-with-diverging-id pattern, which is the open
question the reason exists to answer.

Also adds the no_unit_id emission test the CLI side advertised but never
asserted, and a Studio pair pinning that a URL override and a cohort roll
produce the same assignment with different reasons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:39:56 -07:00
Vance IngallsandClaude Opus 5 076657a639 feat(core,cli): emit the canary decision reason alongside the assignment
The calibration contract deferred this until the stability check came back
dirty. It did: the first fleet read found 304 installs (1.08%) reporting both
values for a canary whose percentage never moved, and the genuinely anomalous
ones could not be separated from a developer toggling HF_CANARY_*, because the
assignment alone is identical in both cases.

resolveCanary has always computed the reason and canaryEventProperties dropped
it. Now every canary emits canary_reason_<name> beside its assignment.

Deliberately outside the $feature/ namespace: PostHog treats those as flag
values, and a non-boolean there would corrupt the flag's own breakdowns.

Two of the six wire values are immediately useful beyond override attribution.
'excluded' identifies CI installs, which today have to be dropped by joining
on is_ci — conflating them with out_of_cohort is what made the first accuracy
read look like a significant failure (9.22% against a 10% target) when it was
not. 'no_unit_id' surfaces the fails-closed corner.

The reason is optional on the core helper so existing callers are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:57:08 -07:00
Miguel Ángel fc0298de4a chore: release v0.7.95 (#3068) 2026-08-06 10:12:20 -07:00
WaterrrForever 3b65321a20 feat(cli): classify identity persistence on every telemetry event (#3065)
* feat(cli): classify identity persistence on every telemetry event

Install-grain metrics currently trust every anonymousId equally, but
ephemeral/isolated-HOME workloads mint a fresh id per run — one machine
produced 2,956 rotating render identities since Jul 30 (94.4% seen on a
single render command), inflating acquisition and diluting per-install
penetration while looking like real product usage.

Every event now carries:

- identity_persistence: durable (id loaded from a preexisting config —
  proven to survive a process boundary) | unknown (minted+persisted this
  run; an ephemeral HOME is indistinguishable from a genuine first run
  from inside one process) | process_only (persist failed). Sticky per
  process so a fresh install re-reading its own write cannot self-promote.
- config_write_outcome: ok | ok_unmirrored | failed for the identity-
  establishing write; absent when the id came from disk.
- invocation_id: random uuid per CLI process, so one invocation's events
  group even when the install identity is untrustworthy (unlike run_id,
  which needs an orchestrator to set HYPERFRAMES_RUN_ID).

Install metrics can then count only durable identities, and a daily
churn monitor can alert on the unknown share.

* fix(cli): require the anonymousId to come off disk before classifying durable

Review finding: materializeConfig mints a replacement anonymousId when a
hand-edited/image-baked config lacks one. That replacement only reaches
disk when the bucket-seed backfill happens to write; with a seed present
the read path performs no write at all, so the install re-mints a fresh
id every run while the unconditional durable branch stamped each of them
with the one label durable-only counting is allowed to trust.

durable now requires parseNonEmptyString(parsed.anonymousId): a minted
replacement classifies like a fresh mint — by the backfill write outcome
when that path runs (unknown/process_only), and process_only on the
no-write path where the id provably dies with the process. Two tests pin
both shapes.
2026-08-06 22:19:14 +08:00
Miguel Ángel 88853f170f fix(studio): route rooted timeline media through preview (#3061) 2026-08-05 22:14:28 -07:00
Miguel ÁngelandCodex 96861cbafc perf(studio-server): coordinate cancelable thumbnail generation (#2720)
* perf(studio): schedule adaptive timeline thumbnails

* perf(studio): bound thumbnail decoding resources

* perf(studio): virtualize timeline thumbnail media

* perf(studio): prioritize timeline thumbnail work

* perf(studio-server): coordinate cancelable thumbnail generation

---------

Co-authored-by: Codex <codex@local>
2026-08-05 20:41:35 -07:00
ukimsanov d373c3f4a0 fix(catalog): explicit section ownership, no body-sniffing heuristic
carriedSectionsFrom() decided whether a ## Usage section was generated by matching
its first line against a list of historical opener phrases — so a hand-written
Usage section that happened to open that way was classified as generated and
silently deleted on regeneration. Ownership is now purely set membership: a
section is generated iff its heading is one the template emits, and ambiguous
'usage' is no longer in that set (the template never emits it), so any ## Usage is
carried. Exported carriedSectionsFrom behind an entrypoint guard and added two
executable preservation fixtures. Flagged by Magi (#5).
2026-08-05 10:23:37 -07:00
ukimsanov 8e658bd6c9 docs(sdk): fix dispatch/can contract, second stale GSAP site, media-start layers
- edit-operations: dispatch() does not consult can() (session.ts:608 -> applyOp
  with no validation; a no-timeline addGsapTween/addLabel is a no-op but a missing
  target still writes via selector fallback). Use Rames's wording: call can() first
  and skip on failure — no false 'applies nothing' guarantee.
- timing-and-animation: the second E_NO_GSAP_TIMELINE site — gated setGsapTween on
  an error it cannot return and called shipped parser code 'a later phase'. Rewrote
  to gate addGsapTween (which can return it); dropped the stale can() comment in
  types.ts:595.
- html-schema: data-playback-start is read by runtime, Studio and CLI (Studio also
  writes it, timelineEditingHelpers.ts); only the compile path is media-start only.
  Document the layered precedence instead of calling it runtime-only.

Round-2 findings from Magi (#2/#3/#4) and Rames.
2026-08-05 10:13:17 -07:00
ukimsanov aa49f7d924 Merge remote-tracking branch 'origin/main' into docs/pages-show-not-tell 2026-08-05 05:56:59 -07:00
ukimsanov 6ff6601dd0 fix(catalog): restore the required Related topics section on generated pages
My generator rebuild replaced the '## Related topics' section (still required by
docs/AGENTS.md) with the provenance footer, dropping it from all 168 generated
Catalog pages. Emit it again as the final section so pages end with it, and stop
carriedSectionsFrom() breaking at the footer marker so a human section appended
below the generated tail survives a regeneration. Adds a per-page regeneration
assertion so a future drop fails CI. Flagged by Magi (P1) and Rames.
2026-08-05 05:38:58 -07:00
Miguel Ángel 29f004cfc0 chore: release v0.7.94 (#3045) 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) 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
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
Miguel Ángel 19b8a1f3c5 refactor(studio): extract live timeline clock (#2711) 2026-08-04 16:27:41 -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
Miguel Ángel 8a0dccfc72 feat(studio): add external conflict recovery UI (#2992) 2026-08-04 22:43:20 +00:00
Miguel Ángel a99caad581 feat(studio): coordinate external file changes (#2991) 2026-08-04 22:20:44 +00: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
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
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
Miguel Ángel cf45c98454 fix(studio): respect GSAP transform ownership (#2986)
* fix(studio): respect GSAP transform ownership

* fix(studio): enforce GSAP edit ownership consistently
2026-08-04 19:08:02 +00:00
Miguel Ángel 532f06158b fix(parsers): preserve safe GSAP helper defaults (#2985)
* fix(parsers): preserve safe GSAP helper defaults

* fix(parsers): make helper inlining call-safe
2026-08-04 19:08:02 +00:00
Miguel Angel Simon Sierra 6703ea7e04 fix(engine): warn once per process about unverified hardware GPU 2026-08-04 11:04:35 -07:00
Miguel Angel Simon Sierra 131780fe96 fix(engine): verify explicit browserGpuMode=hardware instead of trusting it
Chrome's hardware GL args are advisory: with no usable GPU it silently
falls back to software WebGL and the capture runs at CPU speed. Run the
existing WebGL probe for explicit hardware mode too and warn loudly with
the platform's remediation. The requested mode is still honoured.

Fixes #2967
2026-08-04 11:01:16 -07:00
Vance Ingalls f9ec93459f Merge pull request #2945 from heygen-com/ffprobe-6-argv-sweep
fix(cli,core,lint,producer,studio-server): terminate ffprobe options everywhere, pin the contract
2026-08-04 10:43:14 -07:00