diff --git a/README.md b/README.md index ca4ca5fd..18d9c6d2 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,7 @@ Three capabilities have no captured payload, so they fail with | Veo start+end-frame chaining (`/fk-gen-chain-videos`) | unported | Omni first+last (`model_family=omni_flash`), or `FLOW_ALLOW_DEGRADED=1` → i2v off the start frame | | Omni Flash text-to-video | ported | `POST /api/flow/generate-video-omni-text` (4/6/8/10s) | | Omni Flash frame / first+last / reference modes | ported | `eb1hJf`, `nprQif`, `MZZa6b` — `POST /api/flow/generate-video` with `model_family=omni_flash` | +| Omni 1.2 Flash (all four modes above) | ported | add `"omni_version": "1.2"` to the same endpoints — `macaron_cookie_{t2v,i2v,i2v_fl,r2v}_s[_360p]`, any whole second 3–15 | Restoring one starts with a capture, not a guess: [`docs/CAPTURE.md`](docs/CAPTURE.md). diff --git a/agent/api/flow.py b/agent/api/flow.py index 2d0590c1..66d25cc9 100644 --- a/agent/api/flow.py +++ b/agent/api/flow.py @@ -55,6 +55,8 @@ class GenerateVideoRequest(BaseModel): model_family: Literal["veo", "omni_flash"] = "veo" duration_s: int = 8 resolution: Literal["360p", "720p"] = "720p" + # Omni only: "1.2" = Omni 1.2 Flash (3-15s; first+last only for frames). + omni_version: Literal["1.1", "1.2"] = "1.1" class GenerateVideoRefsRequest(BaseModel): @@ -80,6 +82,7 @@ class GenerateOmniFlashVideoRequest(BaseModel): resolution: Literal["360p", "720p"] = "720p" aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT" user_paygate_tier: str = "PAYGATE_TIER_ONE" + omni_version: Literal["1.1", "1.2"] = "1.1" class GenerateOmniFlashTextVideoRequest(BaseModel): @@ -90,6 +93,7 @@ class GenerateOmniFlashTextVideoRequest(BaseModel): resolution: Literal["360p", "720p"] = "720p" aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT" user_paygate_tier: str = "PAYGATE_TIER_ONE" + omni_version: Literal["1.1", "1.2"] = "1.1" class UpscaleVideoRequest(BaseModel): @@ -271,6 +275,7 @@ async def generate_video(body: GenerateVideoRequest): resolution=body.resolution, aspect_ratio=body.aspect_ratio, user_paygate_tier=body.user_paygate_tier, + omni_version=body.omni_version, ) if body.end_image_media_id: result = await generate_omni_flash_first_last_video( @@ -283,7 +288,8 @@ async def generate_video(body: GenerateVideoRequest): raise HTTPException(400, str(exc)) from exc else: payload = body.model_dump( - exclude={"model_family", "duration_s", "resolution"}, exclude_none=True + exclude={"model_family", "duration_s", "resolution", "omni_version"}, + exclude_none=True, ) payload["project_id"] = project_id result = await client.generate_video(**payload) diff --git a/agent/services/flow_batch.py b/agent/services/flow_batch.py index 852478ec..75442481 100644 --- a/agent/services/flow_batch.py +++ b/agent/services/flow_batch.py @@ -443,18 +443,21 @@ def video_request(prompt: str, project_id: str, source_media_id: str, def omni_first_frame_request(prompt: str, project_id: str, source_media_id: str, *, duration_s: int = 8, resolution: str = "720p", aspect: Any = VIDEO_ASPECT_LANDSCAPE, - crop: Optional[list] = None) -> str: + crop: Optional[list] = None, + model: Optional[str] = None) -> str: """Build current Omni first-frame I2V (RPC ``eb1hJf``). Live-captured from Flow on 2026-09-14. 720p uses ``abra_i2v_s``; 360p appends ``_360p`` and carries the UI's low-resolution option slot. + ``model`` overrides the key (Omni 1.2). """ - if duration_s not in (4, 6, 8, 10): - raise ValueError("Omni duration must be 4, 6, 8 or 10 seconds") res = str(resolution).strip().lower() if res not in {"360p", "720p"}: raise ValueError("Omni resolution must be 360p or 720p") - model = f"abra_i2v_{duration_s}s" + ("_360p" if res == "360p" else "") + if model is None: + if duration_s not in (4, 6, 8, 10): + raise ValueError("Omni duration must be 4, 6, 8 or 10 seconds") + model = f"abra_i2v_{duration_s}s" + ("_360p" if res == "360p" else "") request = [ [None, None, [[[prompt]]]], model, @@ -478,14 +481,20 @@ def omni_first_last_request(prompt: str, project_id: str, *, duration_s: int = 8, resolution: str = "720p", aspect: Any = VIDEO_ASPECT_LANDSCAPE, start_crop: Optional[list] = None, - end_crop: Optional[list] = None) -> str: - """Build Omni First+Last frames submit (RPC ``nprQif``).""" - if duration_s not in (4, 6, 8, 10): - raise ValueError("Omni duration must be 4, 6, 8 or 10 seconds") + end_crop: Optional[list] = None, + model: Optional[str] = None) -> str: + """Build Omni First+Last frames submit (RPC ``nprQif``). + + ``model`` overrides the 1.1 key; Omni 1.2 uses the same payload with + ``macaron_cookie_i2v_fl_s[_360p]`` (live-captured 2026-11-06). + """ res = str(resolution).strip().lower() if res not in {"360p", "720p"}: raise ValueError("Omni resolution must be 360p or 720p") - model = f"omni_flash_i2v_{duration_s}s_first_last" + ("_360p" if res == "360p" else "") + if model is None: + if duration_s not in (4, 6, 8, 10): + raise ValueError("Omni duration must be 4, 6, 8 or 10 seconds") + model = f"omni_flash_i2v_{duration_s}s_first_last" + ("_360p" if res == "360p" else "") request = [ [None, None, [[[prompt]]]], model, @@ -507,17 +516,18 @@ def omni_first_last_request(prompt: str, project_id: str, def omni_reference_video_request(prompt: str, project_id: str, reference_media_ids: list[str], *, duration_s: int = 8, resolution: str = "720p", - aspect: Any = VIDEO_ASPECT_LANDSCAPE) -> str: + aspect: Any = VIDEO_ASPECT_LANDSCAPE, + model: Optional[str] = None) -> str: """Build Omni Ingredients/reference-to-video submit (RPC ``MZZa6b``).""" refs = [str(mid) for mid in reference_media_ids if str(mid)] if not refs: raise ValueError("Omni reference-to-video requires at least one reference image") - if duration_s not in (4, 6, 8, 10): + if model is None and duration_s not in (4, 6, 8, 10): raise ValueError("Omni duration must be 4, 6, 8 or 10 seconds") res = str(resolution).strip().lower() if res not in {"360p", "720p"}: raise ValueError("Omni resolution must be 360p or 720p") - model = f"abra_r2v_{duration_s}s" + ("_360p" if res == "360p" else "") + model = model or f"abra_r2v_{duration_s}s" + ("_360p" if res == "360p" else "") request = [ [None, None, [[[prompt]]]], [[None, mid] for mid in refs], diff --git a/agent/services/omni_flash.py b/agent/services/omni_flash.py index 277a8736..d35f80d6 100644 --- a/agent/services/omni_flash.py +++ b/agent/services/omni_flash.py @@ -28,6 +28,22 @@ _MODELS_FILE = Path(__file__).parent.parent / "models.json" OMNI_FLASH_VALID_DURATIONS = (4, 6, 8, 10) +# Omni 1.2 Flash (Flow codename ``macaron_cookie``): any whole second 3-15. +# t2v / first+last keys live-captured from flow.google.com on 2026-11-06; +# first-frame (i2v) and Ingredients (r2v) follow the 1.1 naming by analogy. +OMNI_VERSIONS = ("1.1", "1.2") +OMNI_12_DURATIONS = tuple(range(3, 16)) + + +def omni12_model_key(mode: str, duration_s: int, resolution: str) -> str: + if duration_s not in OMNI_12_DURATIONS: + raise ValueError(f"Omni 1.2 duration must be 3-15 seconds, got {duration_s}") + stem = {"t2v": "t2v", "first_last": "i2v_fl", "first_frame": "i2v", "r2v": "r2v"}.get(mode) + if stem is None: + raise ValueError( + f"Omni 1.2 mode {mode!r} is not captured yet; use omni_version='1.1'" + ) + return f"macaron_cookie_{stem}_{duration_s}s" + ("_360p" if resolution == "360p" else "") OMNI_FLASH_VALID_ASPECTS = { "VIDEO_ASPECT_RATIO_PORTRAIT", "VIDEO_ASPECT_RATIO_LANDSCAPE", @@ -178,15 +194,19 @@ async def generate_omni_flash_text_video( aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT", user_paygate_tier: str = "PAYGATE_TIER_ONE", seed: int | None = None, + omni_version: str = "1.1", ) -> dict: - """Submit Omni 1.1 Flash text-to-video on the migrated Flow batch API.""" - _validate_duration(duration_s) + """Submit Omni Flash text-to-video (1.1 ``abra`` or 1.2 ``macaron_cookie``).""" resolution = _validate_resolution(resolution) + if omni_version == "1.2": + model_key = omni12_model_key("t2v", duration_s, resolution) + else: + _validate_duration(duration_s) + model_key = f"abra_t2v_{duration_s}s" + ("_360p" if resolution == "360p" else "") _validate_aspect(aspect_ratio) client = get_flow_client() try: pid = client._batch_project_id(project_id) - model_key = f"abra_t2v_{duration_s}s" + ("_360p" if resolution == "360p" else "") freq = fb.text_video_request( prompt, pid, @@ -235,6 +255,7 @@ async def _submit_omni_frame_video( aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT", user_paygate_tier: str = "PAYGATE_TIER_ONE", seed: int | None = None, + omni_version: str = "1.1", ) -> dict: """Submit Omni first-frame or First+Last generation. @@ -242,8 +263,16 @@ async def _submit_omni_frame_video( first-frame uses ``eb1hJf``; First+Last uses ``nprQif``. Both return normal batch operations and are polled through ``/api/flow/check-status``. """ - _validate_frame_inputs(start_image_media_id, end_image_media_id, duration_s, aspect_ratio) resolution = _validate_resolution(resolution) + model_override = None + if omni_version == "1.2": + model_override = omni12_model_key( + "first_last" if end_image_media_id is not None else "first_frame", + duration_s, resolution, + ) + _validate_frame_inputs(start_image_media_id, end_image_media_id, 4, aspect_ratio) + else: + _validate_frame_inputs(start_image_media_id, end_image_media_id, duration_s, aspect_ratio) mode = "start_end_frame_to_video" if end_image_media_id is not None else "frame_to_video" client = get_flow_client() @@ -252,17 +281,18 @@ async def _submit_omni_frame_video( if end_image_media_id is None: freq = fb.omni_first_frame_request( prompt, pid, start_image_media_id, duration_s=duration_s, - resolution=resolution, aspect=aspect_ratio, + resolution=resolution, aspect=aspect_ratio, model=model_override, ) rpcid = fb.RPC_GEN_VIDEO - batch_model = f"abra_i2v_{duration_s}s" + ("_360p" if resolution == "360p" else "") + batch_model = model_override or f"abra_i2v_{duration_s}s" + ("_360p" if resolution == "360p" else "") else: freq = fb.omni_first_last_request( prompt, pid, start_image_media_id, end_image_media_id, duration_s=duration_s, resolution=resolution, aspect=aspect_ratio, + model=model_override, ) rpcid = fb.RPC_GEN_VIDEO_FIRST_LAST - batch_model = f"omni_flash_i2v_{duration_s}s_first_last" + ( + batch_model = model_override or f"omni_flash_i2v_{duration_s}s_first_last" + ( "_360p" if resolution == "360p" else "" ) payload = await client._batch_payload( @@ -284,6 +314,7 @@ async def generate_omni_flash_first_frame_video( aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT", user_paygate_tier: str = "PAYGATE_TIER_ONE", seed: int | None = None, + omni_version: str = "1.1", ) -> dict: """Submit Omni Flash First frame -> video.""" return await _submit_omni_frame_video( @@ -297,6 +328,7 @@ async def generate_omni_flash_first_frame_video( aspect_ratio=aspect_ratio, user_paygate_tier=user_paygate_tier, seed=seed, + omni_version=omni_version, ) @@ -311,6 +343,7 @@ async def generate_omni_flash_first_last_video( aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT", user_paygate_tier: str = "PAYGATE_TIER_ONE", seed: int | None = None, + omni_version: str = "1.1", ) -> dict: """Submit Omni Flash First + Last frame -> video.""" return await _submit_omni_frame_video( @@ -324,6 +357,7 @@ async def generate_omni_flash_first_last_video( aspect_ratio=aspect_ratio, user_paygate_tier=user_paygate_tier, seed=seed, + omni_version=omni_version, ) @@ -337,21 +371,27 @@ async def generate_omni_flash_video( aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT", user_paygate_tier: str = "PAYGATE_TIER_ONE", seed: int | None = None, + omni_version: str = "1.1", ) -> dict: """Submit Omni Flash Ingredients/reference-to-video generation. The migrated UI uses RPC ``MZZa6b`` with ``abra_r2v_s`` (720p) or ``abra_r2v_s_360p``. Batch jobs use normal operation polling. """ - refs = _validate_reference_inputs(reference_media_ids, duration_s, aspect_ratio) resolution = _validate_resolution(resolution) + model_override = None + if omni_version == "1.2": + model_override = omni12_model_key("r2v", duration_s, resolution) + refs = _validate_reference_inputs(reference_media_ids, 4, aspect_ratio) + else: + refs = _validate_reference_inputs(reference_media_ids, duration_s, aspect_ratio) client = get_flow_client() try: pid = client._batch_project_id(project_id) freq = fb.omni_reference_video_request( prompt, pid, refs, duration_s=duration_s, - resolution=resolution, aspect=aspect_ratio, + resolution=resolution, aspect=aspect_ratio, model=model_override, ) payload = await client._batch_payload( fb.RPC_GEN_VIDEO_REFERENCES, freq, fb.CAPTCHA_VIDEO, timeout=120, @@ -360,7 +400,7 @@ async def generate_omni_flash_video( client._remember_operation(operation.operation_id, pid) except Exception as exc: return {"status": 502, "error": f"{type(exc).__name__}: {exc}"} - batch_model = f"abra_r2v_{duration_s}s" + ("_360p" if resolution == "360p" else "") + batch_model = model_override or f"abra_r2v_{duration_s}s" + ("_360p" if resolution == "360p" else "") return _batch_operation_result(operation, pid, batch_model, duration_s, resolution) async def _check_omni_batch_media( diff --git a/extension/background.js b/extension/background.js index d2eaf5cb..a04c5895 100644 --- a/extension/background.js +++ b/extension/background.js @@ -664,7 +664,15 @@ const MAX_RPC_TEXT = 32000000; // the project listing alone is past 17 MB async function runBatchRpc(cmd) { const tabs = await chrome.tabs.query({ url: flowUrls }); + // With several Flow tabs open (possibly on different Google accounts), use + // the one whose project id appears in this request. + const freqText = String(cmd.freq || ''); + const projectTab = tabs.find((t) => { + const m = t.url && t.url.match(/\/project\/([0-9a-f-]{36})/i); + return m && freqText.includes(m[1]) && !t.discarded; + }); let candidate = + projectTab || tabs.find((t) => t.url && t.url.includes('/project/') && !t.discarded) || tabs.find((t) => t.active && !t.discarded) || tabs.find((t) => !t.discarded) || @@ -710,9 +718,12 @@ async function runBatchRpc(cmd) { // image generation when source-path is missing even though Lite may not. const sourcePath = location.pathname || '/'; const hl = (document.documentElement.lang || navigator.language || 'en').split('-')[0]; + // Multi-account: a tab on /u// must hit /u//_/..., otherwise the + // cookie jar resolves to the default account and its projects 404. + const acct = (location.pathname.match(/^\/u\/\d+(?=\/)/) || [''])[0]; const url = customPath - ? `${customPath}?bl=${encodeURIComponent(bl || '')}&f.sid=${encodeURIComponent(sid || '')}&hl=${encodeURIComponent(hl)}&_reqid=${reqid}&rt=c` - : `/_/AiSandboxAngularFrontend/data/batchexecute?rpcids=${encodeURIComponent(rpcid)}` + + ? `${customPath.startsWith('/_/') ? acct : ''}${customPath}?bl=${encodeURIComponent(bl || '')}&f.sid=${encodeURIComponent(sid || '')}&hl=${encodeURIComponent(hl)}&_reqid=${reqid}&rt=c` + : `${acct}/_/AiSandboxAngularFrontend/data/batchexecute?rpcids=${encodeURIComponent(rpcid)}` + `&source-path=${encodeURIComponent(sourcePath)}` + `&bl=${encodeURIComponent(bl || '')}&f.sid=${encodeURIComponent(sid || '')}` + `&hl=${encodeURIComponent(hl)}&_reqid=${reqid}&rt=c`; diff --git a/tests/unit/test_omni_flash_12.py b/tests/unit/test_omni_flash_12.py new file mode 100644 index 00000000..d9cdde67 --- /dev/null +++ b/tests/unit/test_omni_flash_12.py @@ -0,0 +1,150 @@ +"""Omni 1.2 Flash (Flow codename ``macaron_cookie``) wire contract. + +t2v and First+Last keys were live-captured from flow.google.com on +2026-11-06; first-frame (i2v) and Ingredients (r2v) follow the 1.1 naming and +were confirmed by real 3s/360p generations. 1.2 reuses the 1.1 envelopes, so +these tests lock the key per mode plus the 3-15s duration range, and that +omitting ``omni_version`` keeps the 1.1 keys. +""" + +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import agent.services.omni_flash as omni_flash +from agent.services.omni_flash import ( + generate_omni_flash_first_frame_video, + generate_omni_flash_first_last_video, + generate_omni_flash_text_video, + generate_omni_flash_video, + omni12_model_key, +) + +PID = "11111111-2222-3333-4444-555555555555" + + +def _request(freq: str) -> list: + return json.loads(json.loads(freq)[0][0][1])[0][0] + + +def _operation_client(): + client = MagicMock() + client._batch_project_id.return_value = PID + client._batch_payload = AsyncMock(return_value=[None, 50, [["op-12", PID, "s", None]]]) + return client + + +@pytest.mark.parametrize( + ("mode", "duration", "resolution", "expected"), + [ + ("t2v", 3, "360p", "macaron_cookie_t2v_3s_360p"), + ("t2v", 15, "720p", "macaron_cookie_t2v_15s"), + ("first_frame", 5, "720p", "macaron_cookie_i2v_5s"), + ("first_last", 3, "360p", "macaron_cookie_i2v_fl_3s_360p"), + ("r2v", 12, "360p", "macaron_cookie_r2v_12s_360p"), + ], +) +def test_omni12_model_keys(mode, duration, resolution, expected): + assert omni12_model_key(mode, duration, resolution) == expected + + +@pytest.mark.parametrize("duration", [2, 16]) +def test_omni12_rejects_durations_outside_3_to_15(duration): + with pytest.raises(ValueError, match="3-15"): + omni12_model_key("t2v", duration, "720p") + + +@pytest.mark.asyncio +async def test_omni12_first_last_uses_nprqif_with_macaron_key(): + client = _operation_client() + with patch("agent.services.omni_flash.get_flow_client", return_value=client): + result = await generate_omni_flash_first_last_video( + start_image_media_id="media-start", + end_image_media_id="media-end", + prompt="run across the bridge", + project_id=PID, + duration_s=3, + resolution="360p", + aspect_ratio="VIDEO_ASPECT_RATIO_LANDSCAPE", + omni_version="1.2", + ) + assert result["status"] == 200 + assert result["data"]["model"] == "macaron_cookie_i2v_fl_3s_360p" + rpcid, freq = client._batch_payload.await_args.args[:2] + assert rpcid == omni_flash.fb.RPC_GEN_VIDEO_FIRST_LAST + request = _request(freq) + assert request[1] == "macaron_cookie_i2v_fl_3s_360p" + assert request[4][1] == "media-start" + assert request[5][1] == "media-end" + + +@pytest.mark.asyncio +async def test_omni12_first_frame_uses_eb1hjf_with_macaron_key(): + client = _operation_client() + with patch("agent.services.omni_flash.get_flow_client", return_value=client): + result = await generate_omni_flash_first_frame_video( + start_image_media_id="media-start", + prompt="p", + project_id=PID, + duration_s=7, + aspect_ratio="VIDEO_ASPECT_RATIO_LANDSCAPE", + omni_version="1.2", + ) + assert result["data"]["model"] == "macaron_cookie_i2v_7s" + rpcid, freq = client._batch_payload.await_args.args[:2] + assert rpcid == omni_flash.fb.RPC_GEN_VIDEO + assert _request(freq)[1] == "macaron_cookie_i2v_7s" + + +@pytest.mark.asyncio +async def test_omni12_reference_uses_mzza6b_with_macaron_key(): + client = _operation_client() + with patch("agent.services.omni_flash.get_flow_client", return_value=client): + result = await generate_omni_flash_video( + reference_media_ids=["ref-a", "ref-b"], + prompt="p", + project_id=PID, + duration_s=3, + resolution="360p", + aspect_ratio="VIDEO_ASPECT_RATIO_LANDSCAPE", + omni_version="1.2", + ) + assert result["data"]["model"] == "macaron_cookie_r2v_3s_360p" + rpcid, freq = client._batch_payload.await_args.args[:2] + assert rpcid == omni_flash.fb.RPC_GEN_VIDEO_REFERENCES + request = _request(freq) + assert request[1] == [[None, "ref-a"], [None, "ref-b"]] + assert request[2] == "macaron_cookie_r2v_3s_360p" + + +@pytest.mark.asyncio +async def test_omni12_text_video_uses_macaron_key(): + client = MagicMock() + client._batch_project_id.return_value = PID + client._batch_payload = AsyncMock(return_value=[ + None, 10, [], [["22222222-3333-4444-5555-666666666666", PID, "x", "CAE"]], + ]) + with patch("agent.services.omni_flash.get_flow_client", return_value=client): + result = await generate_omni_flash_text_video( + prompt="p", project_id=PID, duration_s=15, + aspect_ratio="VIDEO_ASPECT_RATIO_LANDSCAPE", omni_version="1.2", + ) + assert result["data"]["model"] == "macaron_cookie_t2v_15s" + assert _request(client._batch_payload.await_args.args[1])[1] == "macaron_cookie_t2v_15s" + + +@pytest.mark.asyncio +async def test_default_version_keeps_omni_11_keys_and_durations(): + client = _operation_client() + with patch("agent.services.omni_flash.get_flow_client", return_value=client): + result = await generate_omni_flash_first_last_video( + start_image_media_id="a", end_image_media_id="b", prompt="p", + project_id=PID, duration_s=4, aspect_ratio="VIDEO_ASPECT_RATIO_LANDSCAPE", + ) + with pytest.raises(ValueError, match="unsupported"): + await generate_omni_flash_first_last_video( + start_image_media_id="a", end_image_media_id="b", prompt="p", + project_id=PID, duration_s=3, aspect_ratio="VIDEO_ASPECT_RATIO_LANDSCAPE", + ) + assert result["data"]["model"] == "omni_flash_i2v_4s_first_last"