Compute the illustration camera deterministically, raise clip resolution - #28
Merged
Conversation
added 2 commits
August 2, 2026 11:38
…resolution Two live renders proved pixverse-v6-i2v cannot be steered on camera motion or resolution by prompting alone (#26, #27): it exposes no motion/camera param at all (verified against the SDK's param_allowlist), and asking for "gentle parallax" with a negative_prompt naming "zoom out" still drifted into an unusable extreme close-up. So we stop asking the model to move the camera and compute the move ourselves from the case's own factual Timeline. - claimscene/camera.py (new): compute_camera_path derives a slow push toward the reconstructed contact point from Timeline geometry alone, sized and positioned so every impact participant's full footprint stays inside the frame for the whole clip; applied via an ffmpeg zoompan filter built from the case's own numbers, resolution-agnostic (targets ffmpeg's iw/ih, probed from the actual clip via ffprobe). Runs on the raw model output, before the disclosure caption is burned in, so the caption is never cropped or distorted; fails safe to the unmoved clip on any error, never failing the render. - report.py: the clip prompt now asks for a locked-off, static shot (no camera movement of any kind) instead of "gentle parallax"; the negative prompt now suppresses camera motion in both directions, not just the pull-out direction measured in #27. - pipeline.py: quality raised from 360p to 720p (the SDK performs no value validation for this field; 720p is the standard next tier in the SDK's own portable resolution vocabulary), aspect_ratio pinned to 4:3 to match the seed raster so the camera math's coordinate mapping holds. - provenance.py / readiness.py: seal + verify the camera move's own provenance (source, note, applied/error) alongside the existing watermark-burn honesty checks; success is never required, only that the outcome is honestly recorded. schematic.py's pixel-projection formula is extracted into a public world_to_pixel() (pure refactor, golden SVG unchanged) so camera.py reuses the exact same mapping the seed image was drawn with, instead of duplicating it.
_probe_video_params now prefers ffprobe's avg_frame_rate (frames over duration -- the actual playback rate) over r_frame_rate (the lowest rate every timestamp divides into exactly, which only equals the real rate for constant-frame-rate content). A variable-frame-rate or odd-timestamp clip from a live provider could otherwise hand zoompan's fps= a stream timebase like 90000/1 and silently blow up the encode. Falls back to r_frame_rate when avg_frame_rate is ffprobe's own "unknown" sentinel. apply_camera_push now treats an end_zoom clamped all the way to 1.0 (containment needed the whole frame -- only reachable for a pathologically large or spread-out footprint) as a fail-safe no-op instead of re-encoding a filter that would visually do nothing and sealing a truthful-sounding but empty applied=True. Also corrects a comment in report.py that described the camera push's containment padding as "letterboxed framing" -- it is a crop-and-scale in the seed image's own coordinate space, not letterboxing. Adds a real-ffmpeg test proving the push is visibly meaningful by the end of the clip (not just "bytes changed", which a re-encode alone would already satisfy), calibrated against that same clip's own first-frame re-encode noise floor.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two live renders proved
pixverse-v6-i2vcannot be steered on camera motion or resolution by prompting alone (#26, #27): it exposes no motion/camera parameter at all (verified against the SDK's ownparam_allowlist), and asking for "gentle camera parallax and a slow push" plus anegative_promptnaming "zoom out" still drifted into an unusable extreme close-up on a follow-up render. This PR stops asking the model to move the camera and computes the move ourselves, deterministically, from the case's own factualTimeline— the same sealed record the schematic itself is drawn from.src/claimscene/camera.py:compute_camera_pathderives a slow push toward the reconstructed contact point purely fromTimelinegeometry, sized and positioned (via a clamped-centering construction, proven non-empty by construction) so every impact participant's full footprint stays inside the frame for the whole clip. Applied via an ffmpegzoompanfilter built from the case's own numbers — spiked and verified against real ffmpeg locally — expressed in terms of ffmpeg's owniw/ihso it's resolution-agnostic, with the actual clip's dimensions/frame-rate probed viaffprobe(JSON output, not positional CSV — verified empirically that ffprobe's CSV writer does not preserve request field order). Runs on the model's raw output, before the disclosure caption is burned in, so the caption is never cropped or distorted by the push. Fails safe to the unmoved clip on any error (no contact point, a computed push that would be a visual no-op, ffmpeg/ffprobe absent, undecodable input, subprocess failure) — never raises, never fails the render.report.py: the clip prompt now asks for a locked-off, static shot with no camera movement of any kind, replacing "gentle camera parallax and a slow push." The negative prompt now suppresses camera motion in both directions (it previously only named the pull-out failure mode from feat(illustration): suppress the runaway camera, pin a reproducible seed #27's render).pipeline.py:qualityraised from"360p"to"720p"(module constant,ILLUSTRATION_CLIP_QUALITY);aspect_ratiopinned to"4:3"to match the seed raster's own canvas, so the camera math's coordinate assumption holds. See the constant's docstring for exactly how "720p" was determined (the SDK performs no value validation for this field —param_schemasis empty — so this is the standard next tier in the SDK's own portable resolution vocabulary, not a live-verified value).provenance.py/readiness.py: seal and verify the camera move's own provenance (camera_move_source,camera_move_note,camera_move_applied,camera_move_error) alongside the existing watermark-burn honesty checks. Unlike the watermark check, success is never required — only that the outcome is honestly recorded (the camera push is an intentional best-effort, not a second hard requirement).schematic.py: the pixel-projection formula (_View.px) is extracted into a publicworld_to_pixel()— a pure, behavior-preserving refactor (golden SVG test unchanged) — socamera.pyreuses the exact mapping the seed image was drawn with instead of duplicating it.Follow-up hardening (commit 6598937, after self-review)
Two behavioral fixes on top of the initial version above — both change what a real render can honestly seal, so calling them out explicitly:
_probe_video_paramsnow prefersavg_frame_rateoverr_frame_rate.r_frame_rateis ffprobe's own BASE rate — the lowest rate every frame timestamp can be expressed at exactly — which only equals the real playback rate for constant-frame-rate content. A live provider's clip is not guaranteed to be CFR: for variable-frame-rate or odd-timestamp content,r_frame_ratecan read back as something unrelated to the actual rate (e.g. a raw stream timebase like90000/1), which would tellzoompan'sfps=to emit tens of thousands of frames per second — the fail-safe would still catch the resulting timeout/error, but with nothing pointing at why.avg_frame_rate(total frames / duration — "what frame rate does this clip actually play at") is now probed alongsider_frame_ratein the same call and preferred whenever it's itself usable;r_frame_rateremains the fallback for the rarer case whereavg_frame_rateis ffprobe's own"0/0"unknown sentinel. Confirmed empirically that this project's own CFR test fixtures report identical values for both fields, so no existing assertion needed to change.applied=True.compute_camera_path'send_zoomis clamped to at most1.0(only reachable when containment needs the entire frame just to hold both impact participants — a pathologically large or spread-out footprint). At exactly1.0the zoompan filter is a constantz=1for the whole clip: nothing would actually move.apply_camera_pushnow detects this before touching ffmpeg and returnsapplied=Falsewith an explicit reason, the same as "no contact point at all" — instead of re-encoding for zero visual change and recording a technically-truthful-sounding manifest note ("a slow zoom toward...") that would not correspond to anything visible in the pixels.report.py'sILLUSTRATION_NEGATIVE_PROMPTdocstring previously described the camera push's containment padding as "letterboxed framing" — there is no letterboxing anywhere in this design (no black bars; it's a crop-and-scale in the seed image's own coordinate space). Corrected to describe what the padding actually does: tolerance for minor drift in exactly where the model draws the vehicles, not a guarantee against a changed field of view.test_apply_camera_push_succeeds_on_a_real_tiny_clip) passesduration_s=5.0against a clip that only plays for 1 real second, so the zoom ramp barely leaves its starting point (u≈0.2) — any re-encode changes bytes regardless of whether the filter did anything visible. The newtest_apply_camera_push_visibly_moves_the_frame_by_the_end_of_the_clipbuilds a clip whose real duration matchesduration_s, decodes the pushed clip's last frame and the original's last frame via a real ffmpeg call, and requires the pixel-level difference to be well beyond that same pair's own first-frame re-encode noise floor (self-calibrated, not an arbitrary constant). Measured empirically on this exact fixture: noise floor ≈1.6, moved diff ≈43 (≈27x) — the assertion's threshold (3xthe floor, minimum5.0) has wide margin either direction.Determining "720p" was valid
GMICloudVideoProvider.create_registry().get("pixverse-v6-i2v").param_schemasis an empty dict — the SDK performs no value validation forqualityorresolutionoffline, so the accepted set genuinely cannot be confirmed without a live render. What is confirmed: the Pixverse family's own docstring statesqualityis "required by the upstream API," andgenblaze_core.providers.canonical_params.RESOLUTIONS_TIERED(the SDK's own portable vocabulary) lists"720p"alongside 480p/1080p/1440p/4k — notably,"360p"is not in that set despite working today."720p"is the standard next tier and the sensible target; it is a module constant, trivial to change.Constraints honored
SceneGraphschema,layout.py's geometry maths, theDISCLOSURE/WATERMARKconstants, and everything intests/security/are untouched (verified via diff).git difffiltered to added lines, re-checked after the follow-up commit).Tests touched (existing tests edited, with why)
tests/integration/test_genblaze_contract.py::test_pixverse_v6_i2v_routes_image_to_native_slot— updated the examplequalityvalue from"360p"to"720p"(and addedaspect_ratioto the allowlist/payload assertions) purely for consistency with the pipeline's new default; this test exercises SDK passthrough plumbing with its own literal params, not pipeline.py's actual value, so it would have passed unmodified either way.No other existing test was modified — the new prompt wording, negative-prompt terms, and manifest fields are all covered by new tests instead (
test_camera.py, new tests appended totest_pipeline.py/test_report.py/test_review_provenance.py/test_schematic.py, and the newtest_illustration_camera_move.py).Test plan
pytest tests/(497 tests, excluding 3 files broken by a pre-existing local pytest-9.0.2 environment quirk unrelated to this change — confirmed viagit stashthat they fail identically on unmodifiedmain) — all green, 96% combined coverage (gate: 90%),camera.pyitself at 99% (the one uncovered line mirrorswatermark.py's own identical uncovered branch).ruff check src tests scripts— clean.pip-audit -r requirements.txt --strict— clean.python scripts/readiness.py— 100% automatable (gate: 95%), including thehonest_media.camera_move_sealedcheck.pytest tests/security(65 tests) — clean, includingtest_provenance_integrity.py.camera.pyunit level and end-to-end through the real pipeline (camera fails → watermark still burns onto the pre-camera bytes → render still succeeds).avg_frame_rate, falls back tor_frame_ratewhenavg_frame_rateis unusable, returnsNonewhen both are, tolerates a missing field.