Repository navigation
fix(video): i2v silently routed to T2V, dropping frames (#125) - #128
Merged
Merged
Conversation
…2V (#125) omni-flash does NOT support i2v interpolation. When an i2v request carries start/end frame refs with model=omni-flash, Flow's frontend drops the bound mediaIds at submit time and routes the call to batchAsyncGenerateVideoText with image_inputs:null — charging a credit for a pure text-to-video generation that ignores the supplied frames entirely. omni-flash was the i2v default in most sessions, so every i2v paid run on v0.10.0 produced T2V output regardless of the start/end frames. Confirmed at zero credit via scripts/dev/capture_i2v_intercept_submit.py (page.route abort): omni-flash -> T2V/null (both start-only and start+end); veo-lite -> StartAndEndImage with bound mediaIds. Human-session HAR shows the manual UI flow uses veo_3_1_interpolation_lite and routes correctly. Fix: - VideoModel.supports_i2v_interpolation() (False only for omni-flash) and I2V_DEFAULT_MODEL = VEO_3_1_LITE in api/video.py (domain invariant). - ModelModeIncompatibilityError (exit code 17) in errors.py, registered in EXIT_CODE_MAP before its parent ConfigurationError. - cli_video: omni-flash dropped from the i2v --model Choice; omitted --model resolves to veo-lite; a stale --config naming omni-flash raises exit 17. - Transport defense-in-depth: _generate_video_locked raises the typed error (emitting ui_automation_video.model_mode_rejected) BEFORE any browser interaction, catching direct FlowApiClient callers that bypass the CLI. - CHANGELOG: Fixed + Changed entries. Placement and scope follow the 5-persona council review of the fix plan (.planning/issue-125-fix.md). 145 scoped tests pass; ruff + pyright clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two zero-credit Playwright probes that drove and verify the #125 diagnosis: - capture_i2v_post_bind_state.py — confirms FRAME_SLOTS_STRUCT.count() transitions 2->1->0 on bind, the submit button is unambiguous, and the slot stays bound after prompt typing (ruled out selector / wrong-textbox hypotheses). - capture_i2v_intercept_submit.py — the definitive reproducer. Intercepts the submit XHR via page.route(..., abort) and inspects the request body: --model omni-flash -> T2V/null; --model veo-lite -> StartAndEndImage with bound mediaIds. Supports --start-only and --reorder for scope probing. .planning/issue-125-fix.md is the council-reviewed fix plan. (capture_i2v_upload_dialog_dom.py — probe v1 that refuted the selector hypothesis — is intentionally NOT shipped per the Devil's Advocate review.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ting (#125) test_e2e_i2v_start_end_frame_attach defaulted to omni-flash, which silently routes i2v to T2V (issue #125). The test still passed because its assertions (frame_attached fires, terminal success, mp4 on disk) all hold for a T2V generation too — it never checked that the frames actually reached Veo. This is precisely why the bug shipped undetected. - Default GFLOW_CLI_E2E_VIDEO_MODEL is now veo-lite (was omni-flash). - New assertion #4: the captured generate request URL must contain batchAsyncGenerateVideoStartAndEndImage (not batchAsyncGenerateVideoText), and ui_automation_video.model_mode_rejected must NOT have fired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…125) The first fix closed "user picks omni-flash". A paid verify run exposed a SECOND silent-T2V path: even with a valid i2v model (veo-lite), the UI model-select can intermittently fail (`model_option_not_found`, logged as a warning) and Flow falls back to its default (omni-flash) -> i2v routes to T2V -> wasted credit. The model-option probe is flaky (a menu-render race after the trigger click). Three additional layers, all anchored on "i2v must never silently become T2V": - _select_video_model: new `required` param. For i2v the caller passes required=True; a model-select miss is now FATAL (raises RuntimeError with a screenshot) BEFORE any frame attach or submit — spending no credit. Plus a reliability retry: click the trigger + probe the option up to twice, pressing Escape between attempts to reset the dropdown. - _generate_video_locked: for i2v with frames and model=None, default to I2V_DEFAULT_MODEL (mirrors the CLI) so a direct caller can't leave the model unset and inherit Flow's omni-flash default. - Layer 2 post-submit backstop: after the generate response is captured, if an i2v request routed to batchAsyncGenerateVideoText, raise WireFormatError instead of returning a fake-success VideoResult (credit already spent, but no bogus "successful i2v" recorded). Fires `ui_automation_video.i2v_routed_to_t2v`. Tests: TestSelectVideoModelRequired (fatal-on-miss, non-fatal when not required, retry-succeeds-2nd-attempt) + TestI2vT2vRoutingBackstop (i2v->T2V raises, i2v->StartAndEndImage succeeds, t2v->T2V unaffected). The existing i2v routing test now feeds a StartImage url + asserts the model defaults to veo-lite with required=True. 151 scoped tests pass; ruff + pyright clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real root cause of the model-select failure (second silent-T2V path): the
VEO_3_1_LITE picker selector on develop is an EXACT match that hardcodes a
Material Symbols icon ligature — `:text-is('volume_upVeo 3.1 - Lite')`. The menu
item's accessible text no longer matches that literal, so the option was never
found, `_select_video_model` warned-and-continued, Flow stayed on omni-flash,
and i2v routed to T2V. (The avatar branch independently fixed this selector but
it never reached develop; introduced by PR #48 `aa047a3`.)
- Selector is now a robust substring match:
`[role='menuitem']:has-text('Veo 3.1 - Lite'):not(:has-text('[Lower Priority]'))`
— matches regardless of the icon prefix, excludes the Lower-Priority sibling.
Confirmed zero-credit via scripts/dev/capture_i2v_model_select_repro.py
(production call order): model_selected veo_3_1_lite, no retry needed.
- The required-failure now raises the typed `VideoModelSelectionError`
(exit code 18) instead of a bare RuntimeError, so the CLI surfaces the
remediation ("retry; if it persists the picker UI changed — see #125")
instead of a generic "UnexpectedError exit 1".
errors.py: VideoModelSelectionError(ConfigurationError) + EXIT_CODE_MAP[18]
(ordered before ConfigurationError per the isinstance-walk invariant).
Tests updated (exit-18 + VideoModelSelectionError expectation). CHANGELOG
documents the second path + the three guard layers. 152 scoped tests pass;
ruff + pyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Branch-review council (8 dimensions) returned YELLOW with 2 valid must-fixes (D13 credit-safety must-fix was a verified false positive — the abort glob 'video:batchAsync' IS a substring of the real route URL, and the probe empirically aborted every run). - docs/USAGE.md (D8): the shared-video-flags block advertised omni-flash + --duration 10 for all three video subcommands; that's wrong for i2v since #125. Documented i2v's narrowed --model choices (veo-lite default) and --duration [4|6|8]. Added exit-code table rows 17 (ModelModeIncompatibility) and 18 (VideoModelSelection). - CHANGELOG (D1 nice-to-have): note that i2v --duration 10 was dropped. - _select_video_model (D1/D6 nice-to-have): the unreachable `option_sel is None` + required branch now raises VideoModelSelectionError (exit 18) instead of a bare RuntimeError, consistent with the other required-misses; docstring corrected (was "raises RuntimeError"). D5 memory finding (supersede the wf-004 "Veo drift" diagnosis) applied to auto-memory out-of-band. 84 scoped tests pass; ruff + pyright clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tranzmatt
pushed a commit
to tranzmatt/gflow-cli-unofficial
that referenced
this pull request
Jun 3, 2026
omni-flash does not support i2v interpolation — it silently drops the start/end frames and produces a text-only video (gflow-cli ffroliva#125, fixed in ffroliva#128). The example's --video-model now defaults to veo-lite, and the help text documents the constraint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15990166686ll-crypto
pushed a commit
to 15990166686ll-crypto/gflow-cli
that referenced
this pull request
Aug 24, 2026
fix(video): i2v silently routed to T2V, dropping frames (ffroliva#125)
15990166686ll-crypto
pushed a commit
to 15990166686ll-crypto/gflow-cli
that referenced
this pull request
Aug 24, 2026
omni-flash does not support i2v interpolation — it silently drops the start/end frames and produces a text-only video (gflow-cli ffroliva#125, fixed in ffroliva#128). The example's --video-model now defaults to veo-lite, and the help text documents the constraint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Fixes #125 —
gflow video i2vsilently produced text-to-video output, ignoring the start/end frames and burning a credit on every call.There were two independent silent-T2V paths; both are closed, plus three defense-in-depth layers so an i2v can never again silently degrade to T2V.
Root causes
omni-flashdoes not support i2v interpolation. It was the i2v default. When an i2v request carried frame refs under omni-flash, Flow's frontend dropped the bound mediaIds at submit and routed tobatchAsyncGenerateVideoTextwithimage_inputs: null.Veo 3.1 - Litemodel-picker selector ondevelopwas broken — an exact match (:text-is('volume_upVeo 3.1 - Lite')) hardcoding a Material Symbols icon-ligature prefix. It never matched, so even when the user explicitly requested--model veo-lite,_select_video_modelwarned-and-continued and Flow stayed on omni-flash → T2V. (Introduced by PR feat(video): wire i2v/r2v + model/duration/count picker; fix image --model no-op #48aa047a3; thefeature/avatar-generationbranch fixed it but it never reached develop.)Every i2v paid run on v0.10.0 before this fix produced T2V output regardless of the supplied frames.
How it was diagnosed (all zero-credit)
page.route(..., abort)request interception (scripts/dev/capture_i2v_intercept_submit.py) proved omni-flash → T2V/null and veo-lite →StartAndEndImagewith bound mediaIds. A human-session HAR confirmed the manual UI usesveo_3_1_interpolation_lite.capture_i2v_model_select_repro.pyreproduced the selector failure in the production call order and confirmed the robust selector fixes it. The fix plan (.planning/issue-125-fix.md) went through the 5-persona predict council.Changes
Domain (
api/video.py)VideoModel.supports_i2v_interpolation()(False only for omni-flash) +I2V_DEFAULT_MODEL = VEO_3_1_LITE.Errors (
errors.py)ModelModeIncompatibilityError(exit 17) — incompatible model/mode.VideoModelSelectionError(exit 18) — model-picker UI failure for i2v.CLI (
cli_video.py)omni-flashremoved from the i2v--modelChoice; omitted--model→veo-lite; stale config with omni-flash → exit 17.Transport (
api/transports/ui_automation_video.py)ModelModeIncompatibilityErrorbefore any browser interaction.model=None→veo-liteso directFlowApiClientcallers can't inherit Flow's omni-flash default.[role='menuitem']:has-text('Veo 3.1 - Lite'):not(:has-text('[Lower Priority]'))._select_video_model(required=True)for i2v retries then raisesVideoModelSelectionError(exit 18) before submit — spends no credit.WireFormatError(firesui_automation_video.i2v_routed_to_t2v) rather than reporting a fake-success i2v.Tests
supports_i2v_interpolationmatrix, exit-17/18 mapping + ordering invariant, i2v Choice excludes omni-flash, default resolution, DTO guard, Layer 1 (fatal/non-fatal/retry-succeeds), Layer 2 (i2v→T2V raises / i2v→StartAndEnd succeeds / t2v→T2V unaffected).test_e2e_i2v_start_end_frame_attachdefaulted to omni-flash → it was a false positive (frame_attached + terminal-success both hold for T2V). Now defaults veo-lite and asserts the captured URL isStartAndEndImage.Dev tooling (
scripts/dev/)capture_i2v_intercept_submit.py,capture_i2v_post_bind_state.py,capture_i2v_model_select_repro.py— zero-credit reproducers for future regressions.Verification
model_selected→batchAsyncGenerateVideoStartAndEndImage→ SUCCESSFUL mp4 (720×1280, 8s). Start frame matches01-t2i.jpg(desk, pre-dawn), end frame matches02-i2i.jpglighting (same desk, golden sunrise) — same character + 2D ink style, a genuine interpolation, not a T2V scene-swap. Nomodel_mode_rejected, noi2v_routed_to_t2v.Notes
Closes #125
🤖 Generated with Claude Code