Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions agent/api/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class GenerateVideoRequest(BaseModel):
# Backward compatible: legacy requests remain Veo unless explicitly set.
model_family: Literal["veo", "omni_flash"] = "veo"
duration_s: int = 8
resolution: Literal["360p", "720p"] = "720p"


class GenerateVideoRefsRequest(BaseModel):
Expand All @@ -52,6 +53,7 @@ class GenerateVideoRefsRequest(BaseModel):
# explicitly opt into Omni Flash.
model_family: Literal["veo", "omni_flash"] = "veo"
duration_s: int = 8
resolution: Literal["360p", "720p"] = "720p"


class GenerateOmniFlashVideoRequest(BaseModel):
Expand All @@ -60,6 +62,7 @@ class GenerateOmniFlashVideoRequest(BaseModel):
project_id: str
scene_id: str = ""
duration_s: int = 8
resolution: Literal["360p", "720p"] = "720p"
aspect_ratio: str = "VIDEO_ASPECT_RATIO_PORTRAIT"
user_paygate_tier: str = "PAYGATE_TIER_ONE"

Expand Down Expand Up @@ -173,8 +176,8 @@ async def generate_video(body: GenerateVideoRequest):
request uses Omni First frame. When ``end_image_media_id`` is also present,
it uses Omni First+Last frames.

Omni responses include ``flowkitPolling.workflows`` and must use workflow
media polling rather than legacy operation polling.
On the migrated batch transport, Omni frame-conditioned responses return
``flowkitPolling.mode=batch_operation`` and are polled through ``/check-status``.
"""
client = get_flow_client()
if not client.connected:
Expand All @@ -188,6 +191,7 @@ async def generate_video(body: GenerateVideoRequest):
project_id=body.project_id,
scene_id=body.scene_id,
duration_s=body.duration_s,
resolution=body.resolution,
aspect_ratio=body.aspect_ratio,
user_paygate_tier=body.user_paygate_tier,
)
Expand All @@ -202,7 +206,7 @@ async def generate_video(body: GenerateVideoRequest):
raise HTTPException(400, str(exc)) from exc
else:
result = await client.generate_video(
**body.model_dump(exclude={"model_family", "duration_s"}, exclude_none=True)
**body.model_dump(exclude={"model_family", "duration_s", "resolution"}, exclude_none=True)
)

if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
Expand All @@ -216,8 +220,8 @@ async def generate_video_refs(body: GenerateVideoRefsRequest):

Existing requests default to ``model_family=veo``. Set
``model_family=omni_flash`` and ``duration_s`` to 4/6/8/10 to use Omni.
Omni responses include ``flowkitPolling.workflows``; poll those workflows,
not the operation-looking handles in the raw Flow response.
Migrated Omni Ingredients/R2V returns ``flowkitPolling.mode=batch_operation``;
poll its operations through ``/check-status``.
"""
client = get_flow_client()
if not client.connected:
Expand All @@ -231,14 +235,15 @@ async def generate_video_refs(body: GenerateVideoRefsRequest):
project_id=body.project_id,
scene_id=body.scene_id,
duration_s=body.duration_s,
resolution=body.resolution,
aspect_ratio=body.aspect_ratio,
user_paygate_tier=body.user_paygate_tier,
)
except ValueError as exc:
raise HTTPException(400, str(exc)) from exc
else:
result = await client.generate_video_from_references(
**body.model_dump(exclude={"model_family", "duration_s"})
**body.model_dump(exclude={"model_family", "duration_s", "resolution"})
)

if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
Expand Down
97 changes: 97 additions & 0 deletions agent/services/flow_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
RPC_GEN_IMAGE = "ogiZ0b"
RPC_GEN_VIDEO = "eb1hJf"
RPC_GEN_VIDEO_TEXT = "YhhmEf"
RPC_GEN_VIDEO_FIRST_LAST = "nprQif"
RPC_GEN_VIDEO_REFERENCES = "MZZa6b"
RPC_OPERATION = "jwpduf"
RPC_PROJECT_MEDIA = "Zzl0ze"
RPC_MEDIA = "as29s"
Expand Down Expand Up @@ -411,6 +413,101 @@ def video_request(prompt: str, project_id: str, source_media_id: str,
return build_envelope(RPC_GEN_VIDEO, inner)


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:
"""Build current Omni first-frame I2V (RPC ``eb1hJf``).

Live-captured from Flow on 2026-09-14. 720p uses ``abra_i2v_<N>s``;
360p appends ``_360p`` and carries the UI's low-resolution option slot.
"""
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 "")
request = [
[None, None, [[[prompt]]]],
model,
resolve_video_aspect(aspect),
None,
[None, source_media_id, None, None, None,
FULL_FRAME_CROP if crop is None else crop],
[None, None, None, None, _client_uuid(), _client_uuid()],
]
if res == "360p":
request.extend([None, None, None, [4]])
return build_envelope(RPC_GEN_VIDEO, [
[request],
_context(project_id),
[_client_uuid(), 2],
])


def omni_first_last_request(prompt: str, project_id: str,
start_media_id: str, end_media_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")
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 "")
request = [
[None, None, [[[prompt]]]],
model,
resolve_video_aspect(aspect),
None,
[None, start_media_id, None, None, None,
FULL_FRAME_CROP if start_crop is None else start_crop],
[None, end_media_id, None, None, None,
FULL_FRAME_CROP if end_crop is None else end_crop],
[None, None, None, None, _client_uuid(), _client_uuid()],
]
return build_envelope(RPC_GEN_VIDEO_FIRST_LAST, [
[request],
_context(project_id),
[_client_uuid(), 2],
])


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:
"""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):
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 "")
request = [
[None, None, [[[prompt]]]],
[[None, mid] for mid in refs],
model,
resolve_video_aspect(aspect),
None,
[None, None, None, None, _client_uuid(), _client_uuid()],
]
if res == "360p":
request.extend([None, None, None, None, None, [4]])
return build_envelope(RPC_GEN_VIDEO_REFERENCES, [
[request],
_context(project_id),
[_client_uuid(), 2],
])


def text_video_request(prompt: str, project_id: str,
aspect: Any = VIDEO_ASPECT_LANDSCAPE,
model: str = "abra_t2v_4s") -> str:
Expand Down
Loading