vrex-flow-engine 0.3.3__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/PKG-INFO +1 -1
- vrex_flow_engine-0.3.4/README.md +373 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/bridge/flow_rpc.py +51 -8
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/bridge/flow_sdk.py +44 -5
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/catalog.py +6 -3
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/jobs.py +1 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/openai/videos.py +46 -22
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/pyproject.toml +1 -1
- vrex_flow_engine-0.3.4/tests/test_catalog_resolution.py +81 -0
- vrex_flow_engine-0.3.4/tests/test_flow_rpc.py +302 -0
- vrex_flow_engine-0.3.4/tests/test_rpc_method_map.py +48 -0
- vrex_flow_engine-0.3.4/tests/test_video_mode_selection.py +36 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/vrex_flow_engine.egg-info/PKG-INFO +1 -1
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/vrex_flow_engine.egg-info/SOURCES.txt +5 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/PYPI_README.md +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/__init__.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/bridge/__init__.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/bridge/flow_client.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/bridge/ws_server.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/__init__.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/app.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/config.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/dashboard.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/health.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/processes.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/cli/supervisor.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/config.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/ingest.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/job_store.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/main.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/media.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/media_store.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/openai/__init__.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/openai/_util.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/openai/images.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/openai/models.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/openai/uploads.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/pool.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/posthog_client.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/session.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/video_context.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/flow_engine/video_context_store.py +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/setup.cfg +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/vrex_flow_engine.egg-info/dependency_links.txt +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/vrex_flow_engine.egg-info/entry_points.txt +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/vrex_flow_engine.egg-info/requires.txt +0 -0
- {vrex_flow_engine-0.3.3 → vrex_flow_engine-0.3.4}/vrex_flow_engine.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# flow-engine
|
|
2
|
+
|
|
3
|
+
Host a **Google Flow browser-extension instance** and expose **OpenAI-compatible
|
|
4
|
+
image/video generation endpoints** backed by Google Flow (flow.google.com).
|
|
5
|
+
|
|
6
|
+
flow-engine drives a Chrome extension that borrows your signed-in Flow session
|
|
7
|
+
(Google cookies + the page's XSRF token + reCAPTCHA solving) and proxies Flow's
|
|
8
|
+
private `batchexecute` RPCs, mapping the results onto familiar OpenAI
|
|
9
|
+
request/response shapes. PyPI package `vrex-flow-engine`, import module
|
|
10
|
+
`flow_engine`, console commands `vrex-flow-engine` / `flow-engine` (subcommands:
|
|
11
|
+
`start`, `serve`, `setup`, `doctor`).
|
|
12
|
+
|
|
13
|
+
> **Transport status (2026-09).** Flow moved from the Next.js app at
|
|
14
|
+
> `labs.google/fx/tools/flow` (Bearer `ya29` tokens against
|
|
15
|
+
> `aisandbox-pa.googleapis.com`) to an Angular app at `flow.google.com` whose
|
|
16
|
+
> only backend is `/_/AiSandboxAngularFrontend/data/batchexecute`. The old
|
|
17
|
+
> transport now fails with *"Request had invalid authentication credentials"*.
|
|
18
|
+
> flow-engine speaks the new protocol for everything the recorded sessions
|
|
19
|
+
> exercised (images, uploads, text/image/reference-to-video); the few modes
|
|
20
|
+
> whose payload has not been captured yet answer **HTTP 501** — see "Porting
|
|
21
|
+
> more Flow features" below.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
OpenAI client ──HTTP :8101──> flow-engine ──WS :9223──> Chrome extension ──fetch──> Google Flow
|
|
25
|
+
(this repo) (extension/, in your browser)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In production the HTTP surface is published at `https://flow.getvrex.com`
|
|
29
|
+
(a `cloudflared` tunnel → `127.0.0.1:8101`); the Next.js app reaches it via
|
|
30
|
+
`FLOW_ENGINE_URL`. See "Expose to production" below.
|
|
31
|
+
|
|
32
|
+
## Endpoints
|
|
33
|
+
|
|
34
|
+
- `POST /v1/videos/generations` + `GET /v1/videos/{id}` — async video job + poll
|
|
35
|
+
(Veo 3.1 lite, low-priority queue): text-to-video, image-to-video
|
|
36
|
+
(`start_images`), reference-to-video (`ref_images`), frames-to-video
|
|
37
|
+
(`end_images` / `loop`) and 1080p upsample of a generated clip
|
|
38
|
+
(`mode: "upsample"` + `source_media_id`). `mode: extend|edit` and r2v
|
|
39
|
+
`reference_audio` answer `501` until captured.
|
|
40
|
+
- `GET /v1/videos/{id}/debug` — raw `{dispatch, last_poll}` Google payloads for a job (troubleshooting).
|
|
41
|
+
- `POST /v1/images/generations` — synchronous image (Nano Banana 2), optional `ref_images`.
|
|
42
|
+
- `POST /v1/flow/uploads` — image **or video** bytes → Flow media id (routes by mime).
|
|
43
|
+
- `GET /v1/models` — model catalog (cached on disk, refresh with `?refresh=1`);
|
|
44
|
+
each mode reports `dispatchable`.
|
|
45
|
+
- `GET /v1/audio/voices` — Flow's preset voices (read from the default project).
|
|
46
|
+
- `GET /media/{id}` — cached generated bytes at a stable URL.
|
|
47
|
+
- `GET /api/health` — liveness + extension-connected status (ungated).
|
|
48
|
+
|
|
49
|
+
`/v1/*` and `/api/pool/status` require `Authorization: Bearer <FLOW_ENGINE_API_KEY>`.
|
|
50
|
+
`/api/health` and `/media/{id}` are ungated.
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- Python 3.10+
|
|
55
|
+
- Google Chrome signed in to **flow.google.com** with a Flow tab open, and the
|
|
56
|
+
extension in `extension/` loaded (chrome://extensions → Load unpacked).
|
|
57
|
+
- A Flow account (Pro or Ultra — the paygate tier is read from your session;
|
|
58
|
+
the pinned `veo_3_1_lite_low_priority` family is only enabled on Ultra).
|
|
59
|
+
- The engine generates into a project titled **`flow_engine`** — found by
|
|
60
|
+
title, created on first use when missing; `FLOW_DEFAULT_PROJECT_ID` pins one.
|
|
61
|
+
- `cloudflared` (only for exposing to production).
|
|
62
|
+
|
|
63
|
+
> The extension's `background.js` hardcodes `ws://127.0.0.1:9223` and
|
|
64
|
+
> `http://127.0.0.1:8101/api/ext/callback`, so flow-engine defaults to those
|
|
65
|
+
> ports. It therefore **cannot run alongside the legacy flowboard agent on
|
|
66
|
+
> defaults** — change both the env vars and the extension URLs to run both.
|
|
67
|
+
|
|
68
|
+
## Quickstart — one command
|
|
69
|
+
|
|
70
|
+
`flow-engine start` is a supervisor: it stores your secrets, launches the engine
|
|
71
|
+
**and** the Cloudflare tunnel as child processes, restarts either on crash, and
|
|
72
|
+
renders a live status dashboard. The package is published to PyPI as
|
|
73
|
+
**`vrex-flow-engine`**. Run it with [uv](https://docs.astral.sh/uv/) (the Python
|
|
74
|
+
equivalent of `npx` — no manual venv):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uvx vrex-flow-engine start
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`uvx` ships with `uv`. If you get `uvx: command not found`, install uv first
|
|
81
|
+
(then re-run the line above):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # then: exec $SHELL (adds ~/.local/bin to PATH)
|
|
85
|
+
# or on macOS: brew install uv
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
No uv? It's a normal PyPI package — `pipx run vrex-flow-engine start`, or
|
|
89
|
+
`pip install --user vrex-flow-engine && flow-engine start`.
|
|
90
|
+
|
|
91
|
+
First run prompts for the **engine API key** (must match the value the Next.js
|
|
92
|
+
app sends — `FLOW_ENGINE_API_KEY` on Vrex Multimodal / `.env.prd`) and the
|
|
93
|
+
**Cloudflare tunnel token** (dashboard → Zero Trust → Networks → Tunnels → your
|
|
94
|
+
tunnel → "run a connector"). Both are saved to `~/.vrex-flow/config.json` (0600)
|
|
95
|
+
so later runs need no input. Then it brings up the engine + tunnel and shows:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
🎬 Vrex Flow Engine ● running uptime 3m02s
|
|
99
|
+
┌ engine ──────────────┐ ┌ tunnel ───────────────┐
|
|
100
|
+
│ status ● UP │ │ status ● connected │
|
|
101
|
+
│ extension ● connected│ │ public flow.getvrex.com│
|
|
102
|
+
│ instances 1 │ │ → 127.0.0.1:8101 │
|
|
103
|
+
└──────────────────────┘ └───────────────────────┘
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The one thing the CLI can't automate: open a `https://flow.google.com` tab in
|
|
107
|
+
Chrome with the `extension/` loaded and signed into a Flow Pro/Ultra account.
|
|
108
|
+
Until you do, the dashboard shows `extension ● OPEN A FLOW TAB`. Ctrl-C stops
|
|
109
|
+
both processes cleanly.
|
|
110
|
+
|
|
111
|
+
### CLI commands
|
|
112
|
+
|
|
113
|
+
| Command | What it does |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `flow-engine start` | Supervise engine + tunnel + live dashboard (the one-liner). `--no-tunnel` runs the engine only; `--tunnel-config <path>` runs a named tunnel from a cloudflared config.yml instead of a token; `--non-interactive` fails instead of prompting. |
|
|
116
|
+
| `flow-engine setup` | (Re)store the engine key + tunnel token in `~/.vrex-flow/config.json`. |
|
|
117
|
+
| `flow-engine doctor` | Check prerequisites (python, cloudflared, config) and probe a running engine. |
|
|
118
|
+
| `flow-engine serve` | Run just the engine in the foreground (for systemd / your own supervisor). |
|
|
119
|
+
|
|
120
|
+
Secrets can also come from env (`FLOW_ENGINE_API_KEY`, `CLOUDFLARE_TUNNEL_TOKEN`)
|
|
121
|
+
or flags (`--engine-key`, `--tunnel-token`) — flags > env > stored config.
|
|
122
|
+
|
|
123
|
+
## Run manually (without the CLI)
|
|
124
|
+
|
|
125
|
+
The engine reads config from the environment directly — **no `.env` auto-load**.
|
|
126
|
+
`FLOW_ENGINE_API_KEY` is **required**; the server refuses to boot without it.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cd flow-engine
|
|
130
|
+
python -m venv .venv && source .venv/bin/activate
|
|
131
|
+
pip install -e .
|
|
132
|
+
export FLOW_ENGINE_API_KEY=<shared-secret> # must match the caller's key
|
|
133
|
+
flow-engine serve # serves :8101 (HTTP) + :9223 (WS)
|
|
134
|
+
# equivalent: python -m flow_engine.main
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Load `extension/` in Chrome, open a `https://flow.google.com` tab, then check:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
curl localhost:8101/api/health
|
|
141
|
+
# {"ok":true,"extension_connected":true,"instances":1}
|
|
142
|
+
curl localhost:8101/api/pool/status -H "authorization: Bearer $FLOW_ENGINE_API_KEY"
|
|
143
|
+
# {"instances":[{"account_id":"acct-001","connected":true,"session_present":true,
|
|
144
|
+
# "email":"…","paygate_tier":3,"credits":25050,"in_flight":0,"capacity":4}]}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`extension_connected:false` means no extension is bridged yet — reload the
|
|
148
|
+
extension. `session_present:false` / `paygate_tier:null` means the extension is
|
|
149
|
+
bridged but Chrome is not signed in to flow.google.com (a generation request
|
|
150
|
+
answers `503 account_unresolved`).
|
|
151
|
+
|
|
152
|
+
### Tests
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cd flow-engine
|
|
156
|
+
uv run --with pytest python -m pytest tests -q # parsers, catalog resolution
|
|
157
|
+
node --test tests/batchexecute-helpers.test.mjs # extension batchexecute helpers
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Expose to production (Cloudflare Tunnel)
|
|
161
|
+
|
|
162
|
+
The public origin `https://flow.getvrex.com` is a `cloudflared` tunnel to the
|
|
163
|
+
local `:8101`. **`flow-engine start` runs this tunnel for you** (via the stored
|
|
164
|
+
token), so normally you don't touch `cloudflared` directly. To run it standalone
|
|
165
|
+
— e.g. the `serve` path — start the tunnel as a separate process on the same
|
|
166
|
+
machine:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
cloudflared tunnel run --token <your-tunnel-token> # flow.getvrex.com → 127.0.0.1:8101
|
|
170
|
+
# or, with a named-tunnel config file:
|
|
171
|
+
cloudflared tunnel run --config cloudflared.yml
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If `https://flow.getvrex.com` returns **HTTP 530 / `error code: 1033`**, the
|
|
175
|
+
tunnel is down (Cloudflare has no active connection to the origin) — restart it
|
|
176
|
+
(or `flow-engine start`). A `502`/`1016` instead means the tunnel is up but the
|
|
177
|
+
engine on `:8101` is not — restart the engine.
|
|
178
|
+
|
|
179
|
+
## Examples
|
|
180
|
+
|
|
181
|
+
Image (`model` must be `NANO_BANANA_2`, or omit to force it; `size` picks the
|
|
182
|
+
aspect — `1024x1024` square, `1280x720` landscape, `720x1280` portrait):
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
curl localhost:8101/v1/images/generations \
|
|
186
|
+
-H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
|
|
187
|
+
-H 'content-type: application/json' \
|
|
188
|
+
-d '{"prompt": "a lighthouse at dusk", "n": 2, "size": "1280x720"}'
|
|
189
|
+
# {"created":…,"data":[{"url":"http://127.0.0.1:8101/media/<id>"}, …]}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Text-to-video (`model` must be `veo_3_1_lite_low_priority`):
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
curl localhost:8101/v1/videos/generations \
|
|
196
|
+
-H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
|
|
197
|
+
-H 'content-type: application/json' \
|
|
198
|
+
-d '{"prompt": "a cat playing", "model": "veo_3_1_lite_low_priority", "size": "1280x720"}'
|
|
199
|
+
# {"id":"video_…","status":"in_progress","mode":"t2v","model_key":"veo_3_1_t2v_lite_low_priority",…}
|
|
200
|
+
curl localhost:8101/v1/videos/video_… \
|
|
201
|
+
-H "authorization: Bearer $FLOW_ENGINE_API_KEY" # poll until status == "completed"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Image-to-video — pass the start frame inline; flow-engine uploads it for you.
|
|
205
|
+
Reference-to-video ("Ingredients", up to 3 images) works the same way with
|
|
206
|
+
`ref_images` / `ref_media_ids`:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
curl localhost:8101/v1/videos/generations \
|
|
210
|
+
-H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
|
|
211
|
+
-H 'content-type: application/json' -d "{
|
|
212
|
+
\"prompt\": \"the subject turns to camera\", \"model\": \"veo_3_1_lite_low_priority\",
|
|
213
|
+
\"start_images\": [\"$(base64 < frame.png)\"], \"size\": \"1280x720\"
|
|
214
|
+
}"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
End frames (`end_images` / `end_media_ids`) turn i2v into frames-to-video —
|
|
218
|
+
motion between two stills. `loop: true` reuses the start frame as the end
|
|
219
|
+
frame, which yields a clip finishing on the frame it began with, so it tiles
|
|
220
|
+
seamlessly behind narration:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
curl localhost:8101/v1/videos/generations \
|
|
224
|
+
-H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
|
|
225
|
+
-H 'content-type: application/json' \
|
|
226
|
+
-d '{"prompt": "subtle calm motion", "model": "veo_3_1_lite_low_priority",
|
|
227
|
+
"start_media_ids": ["<still media id>"], "loop": true, "size": "1280x720"}'
|
|
228
|
+
# {"id":"video_…","status":"in_progress","mode":"interpolation",…}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`start_images` / `ref_images` / `end_images` accept a data URL, an `http(s)`
|
|
232
|
+
URL, or bare base64. Already have a Flow media id (e.g. from `/v1/flow/uploads`)? Pass it in
|
|
233
|
+
`start_media_ids` / `ref_media_ids` instead — the two forms merge. Uploading a
|
|
234
|
+
reference **video** works via `/v1/flow/uploads -F file=@clip.mp4`, which
|
|
235
|
+
returns a `"kind":"video"` media id (Flow registers it as PENDING and flips it
|
|
236
|
+
to SUCCESSFUL a few seconds later).
|
|
237
|
+
|
|
238
|
+
The completed job carries stable `/media/{id}` URLs; the bytes are fetched from
|
|
239
|
+
Flow's short-lived signed CDN link once and cached locally.
|
|
240
|
+
|
|
241
|
+
A finished clip can be upsampled to 1080p. No prompt is needed; `size` only
|
|
242
|
+
tells Flow the clip's orientation (landscape by default). The job's media id
|
|
243
|
+
is the source id with `_upsampled` appended, and Flow charges credits for it
|
|
244
|
+
(25050 on the capturing account, where the 720p generations were free):
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
curl localhost:8101/v1/videos/generations \
|
|
248
|
+
-H "authorization: Bearer $FLOW_ENGINE_API_KEY" \
|
|
249
|
+
-H 'content-type: application/json' \
|
|
250
|
+
-d '{"mode": "upsample", "model": "veo_3_1_lite_low_priority",
|
|
251
|
+
"source_media_id": "<clip media id>", "size": "1280x720"}'
|
|
252
|
+
# {"id":"video_…","status":"in_progress","mode":"upsample","model_key":"veo_3_1_upsampler_1080p",…}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The upsample item names the source clip's workflow. The engine fills it from
|
|
256
|
+
its own registry when it generated the clip in this run, else it reads it from
|
|
257
|
+
Flow's media entry; pass `workflow_id` to skip that lookup.
|
|
258
|
+
|
|
259
|
+
Not yet available on flow.google.com (each answers **`501 Not Implemented`**
|
|
260
|
+
naming the missing capture; the request contract is unchanged):
|
|
261
|
+
`mode: "extend"`, `mode: "edit"`, r2v `reference_audio`.
|
|
262
|
+
|
|
263
|
+
## Porting more Flow features
|
|
264
|
+
|
|
265
|
+
Everything flow-engine knows about the flow.google.com wire format lives in
|
|
266
|
+
`flow_engine/bridge/flow_rpc.py` (RPC ids, payload builders, response parsers)
|
|
267
|
+
and was derived from recorded web sessions. To add a feature:
|
|
268
|
+
|
|
269
|
+
1. In Chrome DevTools (Network, filter `batchexecute`) perform the action once
|
|
270
|
+
in the Flow web app — e.g. generate an image, upload a reference image, run
|
|
271
|
+
image-to-video — and export the HAR.
|
|
272
|
+
2. For each new call note the `rpcids` query param, the decoded `f.req`
|
|
273
|
+
payload (a positional JSON array), and the `wrb.fr` response row.
|
|
274
|
+
3. Add the RPC id + a payload builder + a parser to `flow_rpc.py` with a test
|
|
275
|
+
in `tests/test_flow_rpc.py` against the recorded response, then wire it in
|
|
276
|
+
`flow_sdk.py` and lift the mode out of the `501` guard (`SUPPORTED_MODES`
|
|
277
|
+
in `openai/videos.py`, `_DISPATCHABLE_MODES` in `catalog.py`, the image /
|
|
278
|
+
upload routes).
|
|
279
|
+
|
|
280
|
+
### RPC ids can rotate — the extension re-discovers them
|
|
281
|
+
|
|
282
|
+
The ids are Wiz RPC hashes; Google may change them in a future frontend
|
|
283
|
+
build. Flow's JS declares every RPC with a descriptor that carries the stable
|
|
284
|
+
backend method path next to the id (`new _.kx("nzlxg", …,
|
|
285
|
+
"/VideoFxService.GetCredits")`), so the extension rebuilds a `method → id`
|
|
286
|
+
map from the public bundle (base script + every lazy module, ~45 MB, fetched
|
|
287
|
+
once per build label and cached in `chrome.storage`). Every `rpc_request`
|
|
288
|
+
names its method (`flow_rpc.RPC_METHOD_BY_ID`); the extension calls the
|
|
289
|
+
discovered id when it differs from the one the engine sent, and pushes the
|
|
290
|
+
map to the engine, which logs a warning and lists the drift in
|
|
291
|
+
`/api/pool/status` (`flow_build`, `rpc_map_size`, `rpc_ids_rotated`). A
|
|
292
|
+
rotation therefore keeps working unattended; update the constants in
|
|
293
|
+
`flow_rpc.py` when convenient. Payload *shapes* are not covered — a method
|
|
294
|
+
whose request proto changes still needs a fresh capture.
|
|
295
|
+
|
|
296
|
+
Known RPC ids so far: `YhhmEf` / `eb1hJf` / `MZZa6b` / `nprQif` generate
|
|
297
|
+
video (t2v / i2v / r2v / frames-to-video; reCAPTCHA token at
|
|
298
|
+
`payload[1][10][0]`), `p0UkFb` upsample a clip to 1080p (same context, batch
|
|
299
|
+
id without the trailing `2`; the model key `veo_3_1_upsampler_1080p` is not in
|
|
300
|
+
the catalog RPC), `fZytfe` / `jIps6` extend / edit (ids known, payloads not),
|
|
301
|
+
`ogiZ0b` generate
|
|
302
|
+
image (sync, token in both clientContext copies), `maseQ` upload image (bytes
|
|
303
|
+
inline), `jwpduf` batch media status, `as29s` media detail with signed URLs,
|
|
304
|
+
`UpteDb` list projects (page token at index 2), `jHPbke` create project,
|
|
305
|
+
`Zzl0ze` project contents (+ preset voices), `HTrJv` model catalog, `nzlxg`
|
|
306
|
+
credits + paygate tier. Video upload is not an RPC: a cookie-authed resumable
|
|
307
|
+
upload against `/upload/v1/flow/upload/video/<projectId>` (see
|
|
308
|
+
`extension/background.js` → `handleUploadRequest`).
|
|
309
|
+
|
|
310
|
+
One open question: the reCAPTCHA *action* name the web client uses for image
|
|
311
|
+
uploads is not visible in a capture (reCAPTCHA bodies are opaque). The engine
|
|
312
|
+
sends `IMAGE_UPLOAD`; if Flow rejects uploads with a captcha error, override
|
|
313
|
+
it with `FLOW_UPLOAD_CAPTCHA_ACTION`.
|
|
314
|
+
|
|
315
|
+
## Configuration
|
|
316
|
+
|
|
317
|
+
These configure the **engine** process (read from its environment). The
|
|
318
|
+
`flow-engine start` launcher injects `FLOW_ENGINE_API_KEY` + ports for you from
|
|
319
|
+
`~/.vrex-flow/config.json`; set these directly only for the manual `serve` path.
|
|
320
|
+
All optional except the API key. Port/host/storage vars keep the `FLOWPROXY_`
|
|
321
|
+
prefix for extension compatibility; the API key and pool size use the
|
|
322
|
+
`FLOW_ENGINE_` / `FLOW_POOL_` names.
|
|
323
|
+
|
|
324
|
+
| Env var | Default | Purpose |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| `FLOW_ENGINE_API_KEY` | — (**required**) | Bearer key gating `/v1/*`. Legacy `FLOWPROXY_API_KEY` still accepted. |
|
|
327
|
+
| `FLOWPROXY_HTTP_PORT` | `8101` | HTTP surface (OpenAI endpoints + `/media` + callback). |
|
|
328
|
+
| `FLOWPROXY_WS_HOST` | `127.0.0.1` | Extension WS bind — **must be loopback** (unauthenticated by design). |
|
|
329
|
+
| `FLOWPROXY_EXT_WS_PORT` | `9223` | Extension WebSocket port. |
|
|
330
|
+
| `FLOWPROXY_PUBLIC_BASE_URL` | `http://127.0.0.1:8101` | Base used to build absolute `/media` URLs returned to clients. |
|
|
331
|
+
| `FLOW_POOL_CONCURRENCY` | `4` | Max concurrent in-flight Flow calls per instance (back-pressure, no 503). |
|
|
332
|
+
| `FLOWPROXY_STORAGE` | `./storage` | Local byte cache dir. |
|
|
333
|
+
| `FLOWPROXY_CATALOG_TTL` | `86400` | Model-catalog cache TTL (seconds). |
|
|
334
|
+
| `FLOWPROXY_PROJECT_TITLE` | `flow_engine` | Title of the Flow project to generate into (found or created). |
|
|
335
|
+
| `FLOW_DEFAULT_PROJECT_ID` | — | Pin the Flow project id instead of looking it up by title. |
|
|
336
|
+
| `FLOW_UPLOAD_CAPTCHA_ACTION` | `IMAGE_UPLOAD` | reCAPTCHA action solved before an image upload (see "Porting more Flow features"). |
|
|
337
|
+
| `VIDEO_POLL_MAX_CYCLES` / `VIDEO_POLL_INTERVAL_S` | `72` / `10` | Video poll budget (default 12 min). |
|
|
338
|
+
|
|
339
|
+
## Layout
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
extension/ Chrome MV3 bridge (open a flow.google.com tab);
|
|
343
|
+
batchexecute.js = shared request/response helpers
|
|
344
|
+
flow_engine/
|
|
345
|
+
bridge/ WS server + flow_client (transport) + flow_rpc (wire
|
|
346
|
+
format: ids, builders, parsers) + flow_sdk (high level)
|
|
347
|
+
media.py DB-free media cache (registry + on-disk bytes)
|
|
348
|
+
jobs.py in-memory job store + async video poller
|
|
349
|
+
session.py readiness gate + default project + pool seam
|
|
350
|
+
catalog.py model catalog (Flow catalog RPC), disk-cached
|
|
351
|
+
openai/ /v1/images, /v1/videos, /v1/flow/uploads, /v1/models
|
|
352
|
+
main.py FastAPI app, /api/ext/callback, /media/{id}, lifespan
|
|
353
|
+
cli/ launcher: app (argparse), supervisor, dashboard,
|
|
354
|
+
processes, health, config (~/.vrex-flow)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Notes & caveats
|
|
358
|
+
|
|
359
|
+
- **Single account.** One extension instance = one Google account = one tier.
|
|
360
|
+
Multi-tenant needs a browser-instance pool routed by API key — the
|
|
361
|
+
`session.get_bridge(api_key)` indirection is where that goes.
|
|
362
|
+
- **Live Flow tab required.** Every dispatch solves an enterprise reCAPTCHA in
|
|
363
|
+
an open Flow tab; hosting needs a headful/virtual-display Chrome kept warm.
|
|
364
|
+
Polling and metadata calls only need the cookies + XSRF token, which the
|
|
365
|
+
extension reads from the flow.google.com page shell and refreshes every
|
|
366
|
+
30 min (and on the first rejected call).
|
|
367
|
+
- **Signed URLs expire.** flow-engine caches bytes locally and serves stable
|
|
368
|
+
`/media/{id}` URLs.
|
|
369
|
+
- **Localhost WS is unauthenticated** by design — never bind it to a network
|
|
370
|
+
interface. Gate the HTTP `/v1/*` surface with `FLOW_ENGINE_API_KEY` when exposed.
|
|
371
|
+
- **No `.env` auto-load.** The process reads `os.environ` directly — export vars
|
|
372
|
+
in the shell (or a wrapper/launchd unit), don't rely on a `.env` file.
|
|
373
|
+
```
|
|
@@ -30,6 +30,7 @@ RPC_GENERATE_VIDEO_T2V = "YhhmEf"
|
|
|
30
30
|
RPC_GENERATE_VIDEO_I2V = "eb1hJf"
|
|
31
31
|
RPC_GENERATE_VIDEO_R2V = "MZZa6b"
|
|
32
32
|
RPC_GENERATE_VIDEO_INTERPOLATION = "nprQif"
|
|
33
|
+
RPC_GENERATE_VIDEO_UPSAMPLE = "p0UkFb"
|
|
33
34
|
RPC_GENERATE_VIDEO = RPC_GENERATE_VIDEO_T2V # historical alias
|
|
34
35
|
RPC_CHECK_MEDIA = "jwpduf"
|
|
35
36
|
RPC_GET_MEDIA = "as29s"
|
|
@@ -38,7 +39,6 @@ RPC_GET_MEDIA = "as29s"
|
|
|
38
39
|
# rotation check covers them and porting them later starts from a known id).
|
|
39
40
|
RPC_GENERATE_VIDEO_EXTEND = "fZytfe"
|
|
40
41
|
RPC_GENERATE_VIDEO_EDIT = "jIps6"
|
|
41
|
-
RPC_GENERATE_VIDEO_UPSAMPLE = "p0UkFb"
|
|
42
42
|
|
|
43
43
|
# Backend method path behind each id, as declared in Flow's JS bundle
|
|
44
44
|
# (``new _.kx("nzlxg", …, "/VideoFxService.GetCredits")``). The ids are Wiz
|
|
@@ -77,6 +77,13 @@ TOOL_PINHOLE = 22
|
|
|
77
77
|
CAPTCHA_APPLICATION_WEB = 1
|
|
78
78
|
# Trailing int in the mediaGenerationContext ``[batchId, 2]`` pair.
|
|
79
79
|
GENERATION_CONTEXT_FLAG = 2
|
|
80
|
+
# The 1080p upsampler's model key. Flow's catalog RPC does not list the video
|
|
81
|
+
# upsamplers (they are not a family the account picks a tier variant of), so
|
|
82
|
+
# the key comes from the capture, not from ``catalog.resolve_video``.
|
|
83
|
+
VIDEO_UPSAMPLER_1080P_KEY = "veo_3_1_upsampler_1080p"
|
|
84
|
+
# Second int in an upsample item (index 6). Observed as 2 alongside a landscape
|
|
85
|
+
# source; its meaning is not visible in the capture.
|
|
86
|
+
UPSAMPLE_ITEM_FLAG = 2
|
|
80
87
|
# Where the extension writes the solved reCAPTCHA token. A clientContext is
|
|
81
88
|
# ``[null, 22, null×3, projectId, null×4, [token, 1]]`` so the token sits at
|
|
82
89
|
# ``[10][0]`` of wherever the context lives in a payload.
|
|
@@ -238,12 +245,38 @@ def video_request_item_r2v(
|
|
|
238
245
|
]
|
|
239
246
|
|
|
240
247
|
|
|
248
|
+
def video_request_item_upsample(
|
|
249
|
+
source_media_id: str,
|
|
250
|
+
workflow_id: str,
|
|
251
|
+
aspect_enum: int,
|
|
252
|
+
scene_id: Optional[str] = None,
|
|
253
|
+
model_key: str = VIDEO_UPSAMPLER_1080P_KEY,
|
|
254
|
+
) -> list[Any]:
|
|
255
|
+
"""Upsample item (``p0UkFb``): ``[[null, sourceMediaId], null, aspect,
|
|
256
|
+
null, [null, workflowId, null×2, sceneId], null, 2, null×24, modelKey]``.
|
|
257
|
+
The web client sends the source clip's own workflow id — the one the
|
|
258
|
+
generate / get-media responses carry — and a fresh scene id. The result is
|
|
259
|
+
a new media whose id is the source id with ``_upsampled`` appended."""
|
|
260
|
+
return [
|
|
261
|
+
[None, source_media_id], None, aspect_enum, None,
|
|
262
|
+
[None, workflow_id, None, None, scene_id or uuid_upper()],
|
|
263
|
+
None, UPSAMPLE_ITEM_FLAG, *([None] * 24), model_key,
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
|
|
241
267
|
def generate_video_payload(
|
|
242
|
-
items: list[list[Any]],
|
|
268
|
+
items: list[list[Any]],
|
|
269
|
+
project_id: str,
|
|
270
|
+
batch_id: Optional[str] = None,
|
|
271
|
+
with_context_flag: bool = True,
|
|
243
272
|
) -> list[Any]:
|
|
244
|
-
"""Body shared by the
|
|
245
|
-
|
|
246
|
-
|
|
273
|
+
"""Body shared by the video RPCs: ``[items, clientContext, [batchId, 2]]``.
|
|
274
|
+
Token goes at ``GENERATE_VIDEO_CAPTCHA_PATH``. The upsample RPC sends the
|
|
275
|
+
batch id alone (``[batchId]``) — pass ``with_context_flag=False``."""
|
|
276
|
+
context = [batch_id or uuid_upper()]
|
|
277
|
+
if with_context_flag:
|
|
278
|
+
context.append(GENERATION_CONTEXT_FLAG)
|
|
279
|
+
return [items, client_context(project_id), context]
|
|
247
280
|
|
|
248
281
|
|
|
249
282
|
def image_request_item(
|
|
@@ -383,17 +416,27 @@ def parse_media_entry(entry: Any) -> dict[str, Any]:
|
|
|
383
416
|
def parse_generate_video(data: Any) -> dict[str, Any]:
|
|
384
417
|
"""``YhhmEf`` → ``{credits, workflows:[{name, primary_media_id}], media}``.
|
|
385
418
|
|
|
386
|
-
``data[2]`` lists the
|
|
419
|
+
``data[2]`` lists the workflows as ``[workflowId, null, null,
|
|
387
420
|
[title, createTime, null, null, mediaId, batchId, updateTime], projectId]``;
|
|
388
421
|
``data[3]`` lists the media rows (see ``parse_media_entry``).
|
|
422
|
+
|
|
423
|
+
The media to poll is taken from the media row that names the workflow,
|
|
424
|
+
not from the workflow's own ``mediaId`` slot: for a fresh generation the
|
|
425
|
+
two agree, but a derived generation (upsample) reuses the source clip's
|
|
426
|
+
workflow, whose slot still points at the source. Polling that would
|
|
427
|
+
report the 720p clip as the finished 1080p one.
|
|
389
428
|
"""
|
|
429
|
+
media = [parse_media_entry(m) for m in _list(_at(data, 3))]
|
|
430
|
+
new_media_by_workflow = {
|
|
431
|
+
m["workflow_id"]: m["media_id"]
|
|
432
|
+
for m in media if m.get("workflow_id") and m.get("media_id")
|
|
433
|
+
}
|
|
390
434
|
workflows: list[dict[str, str]] = []
|
|
391
435
|
for wf in _list(_at(data, 2)):
|
|
392
436
|
wf_id = _str(_at(wf, 0))
|
|
393
|
-
media_id = _str(_at(wf, 3, 4))
|
|
437
|
+
media_id = new_media_by_workflow.get(wf_id) or _str(_at(wf, 3, 4))
|
|
394
438
|
if wf_id and media_id:
|
|
395
439
|
workflows.append({"name": wf_id, "primary_media_id": media_id})
|
|
396
|
-
media = [parse_media_entry(m) for m in _list(_at(data, 3))]
|
|
397
440
|
if not workflows:
|
|
398
441
|
# Fall back to the media rows, which also carry their workflow id.
|
|
399
442
|
for m in media:
|
|
@@ -5,10 +5,11 @@ and the job debug endpoint can inspect Flow's payload when a shape drifts.
|
|
|
5
5
|
|
|
6
6
|
Wired from flow.google.com captures (2026-09): project list/create, model
|
|
7
7
|
catalog, credits/tier, preset voices, image upload (inline) and resumable
|
|
8
|
-
video upload, image generation (sync), text-/image-/reference-to-video
|
|
9
|
-
frames-to-video (interpolation),
|
|
10
|
-
resolution. Extend and edit still need their
|
|
11
|
-
the videos route answers 501 for them instead
|
|
8
|
+
video upload, image generation (sync), text-/image-/reference-to-video,
|
|
9
|
+
frames-to-video (interpolation), 1080p upsampling of a generated clip, media
|
|
10
|
+
status polling and signed URL resolution. Extend and edit still need their
|
|
11
|
+
batchexecute payload captured — the videos route answers 501 for them instead
|
|
12
|
+
of guessing array positions.
|
|
12
13
|
"""
|
|
13
14
|
from __future__ import annotations
|
|
14
15
|
|
|
@@ -264,13 +265,14 @@ class FlowSDK:
|
|
|
264
265
|
items: list[list[Any]],
|
|
265
266
|
project_id: str,
|
|
266
267
|
scene_id: Optional[str] = None,
|
|
268
|
+
with_context_flag: bool = True,
|
|
267
269
|
) -> dict[str, Any]:
|
|
268
270
|
"""Submit video items and shape the reply as ``{raw, operation_names,
|
|
269
271
|
workflows, credits}`` (``operation_names`` are the workflow ids, each
|
|
270
272
|
paired in ``workflows`` with the media id to poll) or ``{raw, error}``."""
|
|
271
273
|
resp = await self._client.rpc(
|
|
272
274
|
rpc_id,
|
|
273
|
-
flow_rpc.generate_video_payload(items, project_id),
|
|
275
|
+
flow_rpc.generate_video_payload(items, project_id, with_context_flag=with_context_flag),
|
|
274
276
|
captcha_action=CAPTCHA_VIDEO,
|
|
275
277
|
captcha_path=flow_rpc.GENERATE_VIDEO_CAPTCHA_PATH,
|
|
276
278
|
source_path=flow_rpc.project_source_path(project_id),
|
|
@@ -421,6 +423,43 @@ class FlowSDK:
|
|
|
421
423
|
items = [flow_rpc.video_request_item_r2v(prompt, model_key, aspect_enum, refs, seed=seed)]
|
|
422
424
|
return await self._dispatch_video(flow_rpc.RPC_GENERATE_VIDEO_R2V, items, project_id, scene_id)
|
|
423
425
|
|
|
426
|
+
async def gen_video_upsample(
|
|
427
|
+
self,
|
|
428
|
+
source_media_id: str,
|
|
429
|
+
project_id: str,
|
|
430
|
+
aspect_ratio: str = "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
|
431
|
+
paygate_tier: Optional[int] = None,
|
|
432
|
+
workflow_id: Optional[str] = None,
|
|
433
|
+
scene_id: Optional[str] = None,
|
|
434
|
+
model_key: Optional[str] = None,
|
|
435
|
+
) -> dict[str, Any]:
|
|
436
|
+
"""Upsample a generated clip to 1080p. The item names the source
|
|
437
|
+
clip's workflow; when the caller does not know it, it is read from
|
|
438
|
+
the clip's media entry (one get-media RPC). No prompt is sent — the
|
|
439
|
+
source clip is the whole input."""
|
|
440
|
+
if paygate_tier is None:
|
|
441
|
+
raise ValueError("paygate_tier is required — resolve the account first")
|
|
442
|
+
aspect_enum = flow_rpc.VIDEO_ASPECT_ENUM.get(aspect_ratio)
|
|
443
|
+
if aspect_enum is None:
|
|
444
|
+
return {"raw": None, "error": f"upsample_aspect_unsupported_{aspect_ratio}"}
|
|
445
|
+
if not isinstance(source_media_id, str) or not source_media_id:
|
|
446
|
+
return {"raw": None, "error": "missing_source_media_id"}
|
|
447
|
+
if not workflow_id:
|
|
448
|
+
detail = await self.get_media(source_media_id, project_id)
|
|
449
|
+
if detail.get("error"):
|
|
450
|
+
return {"raw": detail.get("raw"), "error": detail["error"]}
|
|
451
|
+
workflow_id = (detail.get("media") or {}).get("workflow_id")
|
|
452
|
+
if not workflow_id:
|
|
453
|
+
return {"raw": detail.get("raw"), "error": "upsample_source_has_no_workflow"}
|
|
454
|
+
items = [flow_rpc.video_request_item_upsample(
|
|
455
|
+
source_media_id, workflow_id, aspect_enum, scene_id=scene_id,
|
|
456
|
+
model_key=model_key or flow_rpc.VIDEO_UPSAMPLER_1080P_KEY,
|
|
457
|
+
)]
|
|
458
|
+
return await self._dispatch_video(
|
|
459
|
+
flow_rpc.RPC_GENERATE_VIDEO_UPSAMPLE, items, project_id, scene_id,
|
|
460
|
+
with_context_flag=False,
|
|
461
|
+
)
|
|
462
|
+
|
|
424
463
|
# ── polling ────────────────────────────────────────────────────────────
|
|
425
464
|
async def check_async(
|
|
426
465
|
self,
|
|
@@ -33,9 +33,12 @@ _CACHE_FILE = STORAGE_DIR / "catalog.json"
|
|
|
33
33
|
_CACHE_VERSION = 2
|
|
34
34
|
|
|
35
35
|
# Modes whose batchexecute payload has been captured and verified. Other modes
|
|
36
|
-
# (extension, edit
|
|
37
|
-
#
|
|
38
|
-
|
|
36
|
+
# (extension, edit) still resolve a key so the error can name it, but
|
|
37
|
+
# resolve() reports them as not dispatchable and the route answers 501.
|
|
38
|
+
# Upsample is dispatchable but the catalog RPC does not list the upsampler
|
|
39
|
+
# keys, so the videos route uses ``flow_rpc.VIDEO_UPSAMPLER_1080P_KEY``
|
|
40
|
+
# directly instead of resolving it here.
|
|
41
|
+
_DISPATCHABLE_MODES: frozenset[str] = frozenset({"t2v", "i2v", "r2v", "interpolation", "upsample"})
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class _Catalog:
|