Skip to content
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ flow video "a dragon flying over mountains" --resolution 1080p
flow video "a dragon flying over mountains" --resolution 4k -o /absolute/path/dragon.mp4
```

Draft quickly, upscale for quality. `--draft` generates at 360p, the faster,
lower-credit mode Google Flow added with Gemini Omni 1.1 Flash (~half the
credits of the 720p default: 4s=4, 6s=5, 8s=6, 10s=7 vs 7/10/12/15):

```bash
flow video "a dragon flying over mountains" --draft
```

Draft a concept, then regenerate the keeper with `--resolution 1080p` or `--resolution 4k`.

The upsampled file is written to `--output`; the 720p original stays in the
output directory and in `history.json`. If the upsample pass fails, the 720p
video is still delivered and the CLI prints a note.
Expand Down
1 change: 1 addition & 0 deletions flow-agent/flow_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class VideoGenerationRequest(BaseModel):
"the same step behind the Flow UI's high-resolution download."
),
)
draft: Optional[bool] = Field(False, description="Generate a faster, lower-credit 360p draft (about half the credits)")


class VideoUpsampleRequest(BaseModel):
Expand Down
14 changes: 10 additions & 4 deletions flow-agent/flow_server/routes/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ def _request_payload(req, operation: str, x_client_id: Optional[str]) -> Dict[st
return {"operation": operation, "request": request_data, "client_id": x_client_id}


def _video_model_key(duration: int, draft: bool, video_model: Optional[str] = None) -> str:
"""Video model key; _360p suffix requests a fast, lower-credit 360p draft."""
return video_model or f"abra_t2v_{duration}s" + ("_360p" if draft else "")


def _header_string(value: Any) -> Optional[str]:
"""FastAPI Header defaults remain Header objects in direct MCP calls."""
if not isinstance(value, str):
Expand Down Expand Up @@ -789,6 +794,7 @@ async def _generate_video(req: VideoGenerationRequest, x_client_id: Optional[str

try:
# Submit generation
video_model = _video_model_key(req.duration, req.draft, req.video_model)
if is_video_input and image_media_id:
from flow_engine.generators.v2v import edit_video
media_ids = await edit_video(active_bridge, req.prompt, aspect_key, project_id, image_media_id, duration=req.duration, ref_media_ids=ref_media_ids or None)
Expand All @@ -804,17 +810,17 @@ async def _generate_video(req: VideoGenerationRequest, x_client_id: Optional[str
duration=req.duration,
count=generation_count,
seed=req.seed,
video_model=req.video_model,
video_model=video_model,
)
elif ref_media_ids:
from flow_engine.generators.i2v import generate_video_r2v
media_ids = await generate_video_r2v(active_bridge, req.prompt, aspect_key, project_id, ref_media_ids, duration=req.duration, count=generation_count, seed=req.seed, video_model=req.video_model)
media_ids = await generate_video_r2v(active_bridge, req.prompt, aspect_key, project_id, ref_media_ids, duration=req.duration, count=generation_count, seed=req.seed, video_model=video_model)
elif image_media_id:
from flow_engine.generators.i2v import generate_video_i2v
media_ids = await generate_video_i2v(active_bridge, req.prompt, aspect_key, project_id, image_media_id, duration=req.duration, count=generation_count, seed=req.seed, video_model=req.video_model)
media_ids = await generate_video_i2v(active_bridge, req.prompt, aspect_key, project_id, image_media_id, duration=req.duration, count=generation_count, seed=req.seed, video_model=video_model)
else:
from flow_engine.generators.t2v import generate_video
media_ids = await generate_video(active_bridge, req.prompt, aspect_key, project_id, duration=req.duration, count=generation_count, seed=req.seed, video_model=req.video_model)
media_ids = await generate_video(active_bridge, req.prompt, aspect_key, project_id, duration=req.duration, count=generation_count, seed=req.seed, video_model=video_model)
except Exception as e:
if temp_img_path and os.path.exists(temp_img_path):
try:
Expand Down
3 changes: 3 additions & 0 deletions flow-agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ def cmd_video(argv):
parser.add_argument("--end", metavar="IMAGE", help="End image path or media ID (use with --start)")
parser.add_argument("--ref", "-r", nargs="+", metavar="IMAGE", help="Reference image paths or media IDs")
parser.add_argument("--project-id", "-p", help="Deprecated; configure DEFAULT_PROJECT on the backend")
parser.add_argument("--draft", action="store_true", help="Generate a faster, lower-credit 360p draft (~half the credits)")
parser.add_argument(
"--resolution",
"-R",
Expand Down Expand Up @@ -586,6 +587,8 @@ def cmd_video(argv):
"duration": args.duration,
"n": args.count,
}
if args.draft:
payload["draft"] = True
if args.resolution != "720p":
payload["resolution"] = args.resolution
if args.edit:
Expand Down
57 changes: 57 additions & 0 deletions flow-agent/tests/test_video_draft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Draft-at-360p generation: --draft / draft:true request the _360p model key."""

import io

import main
from flow_server.routes.generation import _video_model_key


def test_draft_appends_360p_model_key():
assert _video_model_key(4, True) == "abra_t2v_4s_360p"
assert _video_model_key(6, False) == "abra_t2v_6s"
assert _video_model_key(10, True, "custom_model") == "custom_model"


READY = {"status": "healthy", "extension_connected": True, "has_flow_key": True}


class DownloadResponse(io.BytesIO):
status = 200

def __enter__(self):
return self

def __exit__(self, exc_type, exc, traceback):
self.close()


def test_cli_draft_flag_sends_payload(monkeypatch, tmp_path):
requested = tmp_path / "draft.mp4"
captured = {}
media_bytes = bytes([0, 0, 0, 24]) + b"ftypisomvideo"
monkeypatch.setattr(main, "_wait_for_generation_ready", lambda: READY)

def fake_post(path, payload, key, timeout):
captured.update(path=path, payload=payload, key=key)
return {"job_id": "job-draft", "status": "processing", "data": []}

def fake_request(path, **kwargs):
return {
"job_id": "job-draft",
"status": "succeeded",
"data": [{"url": "https://media.invalid/video", "media_id": "video-1"}],
}, 200

monkeypatch.setattr(main, "_post_generation", fake_post)
monkeypatch.setattr(main, "_request_json", fake_request)
monkeypatch.setattr(main.time, "sleep", lambda _seconds: None)
monkeypatch.setattr(
main.urllib.request,
"urlopen",
lambda *args, **kwargs: DownloadResponse(media_bytes),
)

main.cmd_video(["draft it", "--draft", "--output", str(requested)])

assert captured["payload"]["draft"] is True
assert requested.read_bytes() == media_bytes