Repository navigation
fix(engine): catch low-contrast drawElement self-verify misses - #3941
santhiprakash wants to merge 4 commits into
Conversation
3f38e4d to
346777e
Compare
fc8199f to
39eef86
Compare
Whole-frame magnitude-weighted PSNR cannot see a fully-missing low-contrast element: the issue-3345 repro (a ~4/255 band over ~15% of the frame) scores 44dB, 12dB clear of the 32dB floor, and ships a wrong MP4 with exit code 0. Add regionShift(): one ffmpeg pass (grainextract + area downscale) reporting the worst ~96px region's mean signed channel shift in 1/255 units. Encoder noise cancels inside a region while an absent element leaves its whole footprint shifted by its own delta, so a missing region can no longer be diluted by a correct background. Both the streaming drain guard and the parallel disk-path verify now require PSNR >= HF_DE_VERIFY_MIN_DB AND region shift <= HF_DE_VERIFY_MAX_SHIFT (default 3/255). Either breach throws DrawElementVerificationError (new kind "shift"), which the orchestrator already converts into the never-wrong screenshot fallback — matching the videoFrameCoverage fail-closed precedent. Closes heygen-com#3345
39eef86 to
2de53ea
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
I'm commenting rather than approving. The mechanism is sound, but the default threshold is a policy call, and nothing yet shows it won't trip on clean renders.
What works
packages/engine/src/utils/psnr.ts:57: computingregionShiftwithgrainextractplus an area downscale is a good way to get a signed per-region mean. It answers the question the issue raises ("did content go missing") instead of the averaged-magnitude question PSNR answers.- Both gates are wired the same way in the streaming drain (
captureStreamingStage.ts:431-441) and on the disk path (parallelCoordinator.ts:747). The newkind: "shift"survivesgetDrawElementVerificationDetails.
Concern: the default of 3/255 isn't calibrated on real drawElement-vs-screenshot pairs
Every measurement in the PR is synthetic. I ran the exact filter chain from psnr.ts:77 on a few image pairs:
| Pair | PSNR | regionShift |
|---|---|---|
| testsrc2 JPEG q2 vs q10 | n/a | 1 |
| testsrc2, 1px blur or 1px x-offset | n/a | 1 |
| testsrc2, gamma 1.02 / 1.04 | n/a | 2 / 4 |
| Same line of 28px text, +1px stroke (a font-weight/hinting difference) | 33.1 dB (passes) | 10 (trips) |
Encoder noise and subpixel offsets stay well under the ceiling, as claimed. But a small, systematic rasterization difference trips it while PSNR passes. That includes text weight and a slight gamma or color-management drift between the canvas path and the screenshot. If drawElement and the screenshot differ that way on real content, many renders fall back to the slower screenshot path. The output is still correct, but the speed cost is large.
Before this ships as a default-on gate, I'd like to see:
regionShifton clean renders of the producer regression fixtures, with drawElement on, so the distribution behind "3" is real data.- The failing shift added to telemetry.
deVerifyFallbackTelemetry(renderOrchestrator.ts:2814) recordsreason: "shift"and the frame, but notfailedShift/verifyMaxShift. Without them, a production false-positive rate can't be tuned.
A maintainer also needs to decide whether a new default-on gate and a new HF_DE_VERIFY_MAX_SHIFT env var are wanted, versus shipping it in observe-only mode first.
What I ran
vitest run src/utils/psnr.test.ts(real ffmpeg): 9/9 pass.- I couldn't run
frameCapture.test.tsorparallelCoordinator.test.tslocally, because they need a built core runtime. CI runs them and they're green.
— Rames
…elemetry Wire failedShift/verifyMaxShift from DrawElementVerificationError through deVerifyFallbackTelemetry into RenderCaptureObservability, the perfSummary drawElement aggregate, CLI render_complete/render_error props, and the crash-survival observability payload as de_fallback_failed_shift and de_fallback_max_shift. A production false-positive rate for the new gate can now be measured from telemetry instead of inferred from fallback counts alone.
Calibration on the producer regression fixtures found no threshold that separates a missing low-contrast region (needs ~3/255) from benign divergence: compositor-animated CSS content measured 18-27/255, and a truth-vs-truth screenshot pair of the same seek differed by 22/255. With the gate default-on at 3, clean renders would eat screenshot fallbacks. HF_DE_VERIFY_MAX_SHIFT now arms the fail-closed shift fallback only when set in [1, 64]; unset or invalid leaves the check observe-only. Every verified sample still logs its measured shift, and the streaming drain aggregates the observed max into verifyMaxShiftObserved on the perf summary and the de_verify_max_shift_observed telemetry property, so a real distribution can be collected before a ceiling is chosen. Also replaces the two em-dashes the studio copy-dashes check flags in the new verify error strings and trims the added field comments back under the comment-ratchet share.
|
Calibration data + gate-policy change pushed in Real-fixture calibrationMethod: drove the production regression fixtures through the same capture functions the verify path uses — compiled each fixture, rendered truth via
The finding that decides the policy
The CSS animation phase is nondeterministic under the capture sequence, so even truth-vs-truth disagrees far above the proposed ceiling. Combined with your synthetic results (gamma 1.04 → 4, +1px stroke → 10) and the #3345 repro needing ≥3–5, no default threshold separates the damage class from this benign class — default-on would cost screenshot fallbacks on legitimate renders. What changed
Recommendation: keep it observe-only until telemetry shows where production clusters land; the env escape hatch arms it immediately for any install that wants fail-closed now. Also fixed the two CI items this commit: the em-dashes |
What
Adds a complementary region-presence check to the drawElement self-verify path. The existing whole-frame PSNR check stays as the magnitude baseline; a new
regionShift()metric reports the worst ~96px region's mean signed channel shift (1/255 units) via one extra ffmpeg pass (grainextract+ area downscale). The shift is measured and logged on every verified sample, and the max lands inde_verify_max_shift_observedtelemetry.The shift gate ships observe-only by default. Fixture calibration on the producer regression suite (see thread below) found no threshold that separates the #3345 damage class (~3-5/255) from benign divergence — compositor-animated CSS content measured 18-27/255, and a truth-vs-truth screenshot pair of the same seek differed by 22/255. Setting
HF_DE_VERIFY_MAX_SHIFTto a value in [1, 64] arms the fail-closed fallback: a sample then fails ifdb < HF_DE_VERIFY_MIN_DBorshift > HF_DE_VERIFY_MAX_SHIFT, and either breach throwsDrawElementVerificationErrorwhich the orchestrator converts into the screenshot fallback, matching thevideoFrameCoverage.tsprecedent called out in the issue thread.Closes #3345.
Why
The magnitude-weighted PSNR gate cannot detect a fully-missing low-contrast element. Reproduced from the issue: a ~4/255 luma band covering ~15% of the frame scores 44.1 dB — 12 dB clear of the 32 dB floor — so a wrong MP4 ships with exit code 0. PSNR is the right answer to "how far off are the pixels on average"; the self-verify question is "did any content go missing", which needs a localized signal that a correct background cannot dilute.
How
packages/engine/src/utils/psnr.ts— newregionShift()(signed diff centered on mid-gray bygrainextract, downscaled to coarse cells by area average; the max |cell−128| is the worst region's mean shift) andresolveDeVerifyMaxShift()(envHF_DE_VERIFY_MAX_SHIFTin [1, 64] arms the gate; unset = observe-only).packages/producer/src/services/render/stages/captureStreamingStage.ts— streaming drain guard computes both metrics, recordsverifyMaxShiftObserved, and throwskind: "shift"only when the gate is armed and trips.packages/engine/src/services/parallelCoordinator.ts— same two-gate check on the parallel disk-verify path (assertDiskSampleVerified, exported for tests).packages/engine/src/services/frameCapture.ts—DrawElementVerificationDetails.kindgains"shift"withfailedShift/verifyMaxShiftstructured fields (no message parsing).renderOrchestrator.ts/observability.ts/perfSummary.ts— telemetry widened on both paths: a shift trip reportsde_fallback_reason = "shift"withde_fallback_failed_shift+de_fallback_max_shift, and clean renders surfacede_verify_max_shift_observedfor calibration.packages/cli/src/telemetry— both fields flow intorender_complete/render_errorproperties.Measured on the repro pair (1080p variant): missing band PSNR 44.1 dB / shift 5, identical pair shift 0, independent encoder noise ≤2, saturated damage 127 (clamped). Chroma-heavy damage is caught too (rgb24 diff, not luma-only).
Test plan
New regression coverage, all passing locally:
packages/engine/src/utils/psnr.test.ts(new): real-ffmpeg repro of drawElement self-verify PSNR gate cannot detect missing low-contrast content #3345 — the missing low-contrast element passes the old 32 dB gate (44 dB) but trips the region gate; identical/noisy pairs stay under the ceiling; saturated damage reads far above it. Plus mocked-execFileunit tests for the rawvideo scan and both env resolvers.frameCapture.test.ts:kind: "shift"details survivegetDrawElementVerificationDetailsround-trip without colliding with the dB fields.parallelCoordinator.test.ts:assertDiskSampleVerifiedthrowskind: "shift"at db=44/shift=13, still throwskind: "psnr"below the floor, and passes a clean sample.captureStreamingStage.test.ts: end-to-end drain-guard tests — a verified frame with PSNR 44 / shift 13 is rejected when the gate is armed (HF_DE_VERIFY_MAX_SHIFT=3), the same sample is accepted withverifyMaxShiftObserved = 13when unarmed, and PSNR 48 / shift 1 is accepted either way.Out of scope: the issue's second hole — verification only samples truth frames, so unsampled frames stay unchecked — is unchanged and worth a follow-up.
AI-assisted. I wrote and verified this change.