Architecture¶
This document is the steady-state reference for how gflow-cli is organised. The intent and sequencing live in PLAN.md — this file describes the shape that PLAN converges on.
Layers¶
┌──────────────────────────────────────────────────────────────┐
│ interfaces/ ← CLI (Click) │
│ one entry point per user-facing command │
└──────────────────────┬───────────────────────────────────────┘
│ dispatches Command/Query objects
┌──────────────────────▼───────────────────────────────────────┐
│ application/ ← Use cases │
│ Commands (writes) + Queries (reads), │
│ one Handler per Command/Query. │
│ Handlers depend on PORTS only. │
└──────────────────────┬───────────────────────────────────────┘
│ depends on Protocol (port)
┌──────────────────────▼───────────────────────────────────────┐
│ domain/ ← Pure business logic │
│ Entities, value objects, domain events, │
│ domain errors. No I/O, no frameworks. │
└──────────────────────▲───────────────────────────────────────┘
│ implements
┌──────────────────────┴───────────────────────────────────────┐
│ infrastructure/ ← Adapters (driven side) │
│ FlowProvider (REST), AuthSession │
│ (Playwright), AssetStorage (local/cloud).│
└──────────────────────────────────────────────────────────────┘
Dependency rule (hexagonal): interfaces → application → domain ← infrastructure. Domain depends on nothing. Application depends on domain + ports (Protocols). Infrastructure implements the ports. Compile this rule into the import graph: domain/* must not import from application/, infrastructure/, or interfaces/.
Modular monolith (current shape)¶
The hexagonal target above is the steady state. The current package — and the shape Phase 4+ work converges to before any DDD restructure — is a flat-namespace modular monolith: one deployable artifact, organized as a flat namespace of clearly-bounded modules.
Per-module rules:
- Each top-level package or file under
src/gflow_cli/is a module with one clear domain (auth,api,cli,errors,observability,manifest,paths,storage,config,profile_store,data). - Each module exposes a public interface via
__init__.pyand (where applicable) explicit__all__. - Internals are prefixed with
_(single leading underscore) and never imported across modules. - Cross-module communication goes through public interfaces, never private internals.
- Modules don't share global mutable state. Configuration is read-only at the boundary (
Settings). - When a module file grows past 400 lines, prefer extraction to a sub-package over inline growth.
Phase 4 module additions (in v0.4.0a1):
gflow_cli.errors— exception taxonomy aligned with RFC 9457 Problem Details. EachGFlowErrorsubclass carriestype(URI),title,status,detail,instance,remediation_hint. Theto_problem_details()method serializes to the RFC 9457 JSON shape and is the stable contract for telemetry consumers.gflow_cli.observability— structlog configuration + theerror_raisedevent emitter. This is the future home for metrics + tracing (Phase 5+) too.
Data layer module (shipped in v0.9.0):
gflow_cli/data/— local SQLite persistence layer (DataStore+DataRepository+OperationRecorder+ redaction + read-onlyqueriesforgflow data list). Records all new image/video operations, asset provenance, and final asset locations (local paths or cloud URIs). Default DB path is resolved from$GFLOW_CLI_DB_PATH(default:~/.local/share/gflow-cli/data.dbon POSIX, the equivalent platformdirs user-data dir on Windows). Migrations versioned via SHA-256 checksums; safe forward-only semantics with newer-schema detection. T2V now flows throughFlowApiClient.generate_video, sharing the client boundary with image commands.
External storage module (shipped after v0.9.1):
gflow_cli/storage.py— target resolver and writer for generated assets. WithGFLOW_CLI_STORAGE_URIunset it returns normal localPathtargets underGFLOW_CLI_OUTPUT_DIR; withgs://ors3://it returns a cloud-backedUPathand writes through the matching fsspec backend. The module owns key sanitisation, cloud URI metadata extraction, and event-loop-safe writes. User-facing setup lives in EXTERNAL_STORAGE.md.
Why RFC 9457 for errors: Problem Details is the IETF-standard shape for machine-readable HTTP error responses. Even though gflow-cli is a CLI (not an HTTP server), adopting the same vocabulary means: (a) the error log shape is greppable by stable type URI, (b) future cloud-edge integrations (e.g., a gflow serve HTTP front-end) can return our errors directly without translation, (c) downstream telemetry tools recognize the shape immediately.
Phase 4 does NOT:
- Restructure existing modules beyond minimal dedup (shared CLI helpers were promoted to
gflow_cli._cli_helpersat the package top level — kept flat to avoid acli.pyfile /cli/package collision). - Introduce dependency-injection containers, command/query buses, or any DDD/CQRS scaffolding (deferred per PLAN ADR #2).
v0.6.0a2 module additions — auth strategy package:
auth.py was promoted to a sub-package gflow_cli.auth/ with the strategy pattern to support multiple browser backends for the gflow auth login command:
src/gflow_cli/auth/
├── __init__.py # public: login(), AuthStatus, get_status(), list_profiles()
├── base.py # AuthStrategy Protocol — the shared contract
├── factory.py # AuthStrategyFactory — routes auto/chrome/internal modes
├── internal_chromium.py # InternalChromiumStrategy — Playwright bundled Chromium (legacy)
├── real_chrome.py # RealChromeStrategy — system Google Chrome with stealth flags
└── strategies.py # (internal) shared Playwright helpers
Why: Google's bot-detection ("G12 block") rejects Playwright's bundled Chromium during gflow auth login. Launching the user's installed Google Chrome with --disable-blink-features=AutomationControlled plus a JS add_init_script that overrides navigator.webdriver bypasses detection. Two strategies are needed because the setup (persistent-context flags, Chrome binary path, stealth init) differs fundamentally between them.
AuthStrategy Protocol (base.py):
class AuthStrategy(Protocol):
name: str
async def login(self, profile_dir: Path, headless: bool) -> None: ...
AuthStrategyFactory (factory.py):
- mode="auto" — probes is_chrome_available() → RealChromeStrategy if found, else InternalChromiumStrategy with a warning.
- mode="chrome" — explicit RealChromeStrategy; raises ConfigurationError if Chrome binary missing.
- mode="internal" — explicit InternalChromiumStrategy.
RealChromeStrategy stealth design (real_chrome.py):
- Uses a Passive Capture pattern: launches system Chrome via subprocess.Popen without any automation flags or remote-debugging ports.
- Provides a 100% clean browser process that Google's G12 block cannot detect.
- The CLI blocks on proc.wait(), prompting the user to complete the sign-in and close the browser completely.
- Post-close: performs a fast, headless launch_persistent_context probe to verify the SAPISID cookie was successfully captured.
- Privacy guard: raises SecurityError if the resolved profile_dir is outside GFLOW_CLI_HOME — protects the user's primary system Chrome profile from being used as a session store.
UiAutomationTransport (UI Mimicry):
- Drives the Flow editor via real DOM interactions (clicks, keyboard events).
- Project Setup Evolution: Due to REST 401 blockers, this transport is moving toward "UI-First" setup, where it handles its own project creation by clicking the "+ New Project" CTA in the browser instead of relying on the FlowApiClient.create_project REST call.
CLI surface: gflow auth login [--browser auto|chrome|internal] (env: GFLOW_CLI_AUTH_BROWSER).
When the project converges on the hexagonal target above, modules graduate to layers: e.g., today's gflow_cli.api becomes gflow_cli.infrastructure.flow_api, gflow_cli.cli becomes gflow_cli.interfaces.cli, and so on. The modular-monolith shape is the staging area, not the destination.
Folder layout¶
Note: this document describes the TARGET architecture, not the current package layout. The current shape (per PLAN.md § 2 and ADR #2) is the simpler
src/gflow_cli/{api/, auth/, browser_manager.py, cli.py, _cli_helpers.py, cli_image.py, cli_run.py, cli_video.py, config.py, errors.py, exceptions.py, image_batch.py, manifest.py, observability.py, paths.py, profile_store.py}. The DDD layout below was deferred indefinitely; converge toward it incrementally if/when a secondProvideror agflow serveHTTP front-end justifies the split.
src/gflow_cli/
├── domain/
│ ├── models.py # Asset, GenerationJob, GenerationProject
│ ├── value_objects.py # AspectRatio, Prompt, OutputCount, etc.
│ ├── events.py # AssetUploaded, JobStarted, …
│ └── errors.py # AuthExpiredError, RateLimitExceededError, …
├── application/
│ ├── ports/
│ │ ├── image_provider.py # Protocol
│ │ ├── video_provider.py # Protocol
│ │ ├── asset_storage.py # Protocol
│ │ └── auth_session.py # Protocol
│ ├── commands/
│ │ ├── upload_image.py
│ │ ├── generate_image.py
│ │ ├── generate_image_batch.py
│ │ ├── generate_video.py
│ │ ├── generate_video_batch.py
│ │ └── download_asset.py
│ ├── queries/
│ │ ├── get_job_status.py
│ │ └── list_generations.py
│ ├── handlers/ # one file per command/query handler
│ └── bus.py # CommandBus + QueryBus
├── infrastructure/
│ ├── flow/ # adapter for aisandbox-pa
│ │ ├── client.py
│ │ ├── image_provider.py
│ │ ├── video_provider.py
│ │ └── routes.py # captured route shapes
│ ├── auth/
│ │ └── playwright_session.py
│ ├── storage/
│ │ └── asset_storage.py
│ └── observability/
│ └── logging.py
├── interfaces/
│ └── cli/
│ ├── main.py # Click entry
│ ├── image.py # `gflow image …`
│ ├── video.py # `gflow video …`
│ └── auth.py # `gflow auth …`
└── shared/
├── config.py # pydantic-settings, .env loader
└── paths.py # platformdirs default dirs
DDD pieces¶
Aggregates
GenerationProject— owns a Flow project lifecycle, the assets uploaded into it, and the jobs spawned from it.GenerationJob— async work (Veo/Imagen) with status, progress, and outputs.
Entities
Asset— uploaded image OR generated image OR generated video. Has stable UUID, media URL, kind, source-job (if generated).
Value objects (frozen dataclasses)
AspectRatio(9:16|16:9|1:1|4:3|3:4) — validated at construction.Prompt,MotionPrompt— non-empty, length-bounded strings.OutputCount— integer 1–4.JobId,AssetId,ProjectId—NewTypeoverstr(UUID4 format).OutputPath—Pathwith directory-vs-file role tagged.
Domain events (in-process for now, eventable later)
AssetUploaded,JobStarted,JobProgressed,JobCompleted,JobFailed,AssetDownloaded.
Domain errors (typed exceptions; no stringly-typed HTTP codes leaking out)
- Target DDD names (not yet implemented):
RateLimitExceededError,QuotaExhaustedError,InvalidPromptError,ProjectNotFoundError,JobNotFoundError,ProviderUnavailableError. - Current Phase 4 classes (shipped in v0.4.0a2, see
gflow_cli.errors):AuthExpiredError,RateLimitError,ContentPolicyError,NetworkError,WireFormatError. All inherit fromFlowApiError → GFlowError;EXIT_CODE_MAPwalks them in subclass-first order soexcept FlowApiErrorstill catches every typed leaf. Per-class exit codes: 3 (auth), 4 (rate-limit), 5 (content-policy), 6 (network), 7 (wire-format). - Module alias:
gflow_cli.exceptionsis a complete re-export ofgflow_cli.errors. Both module paths resolve to identical class objects. The alias exists so downstream integrators can use the conventionalexceptionsname without a special import path.
CQRS¶
Every state-changing intent is a Command. Every read is a Query. Both are immutable dataclasses; their handlers are async.
# application/commands/generate_image.py
@dataclass(frozen=True)
class GenerateImageCommand:
prompt: Prompt
aspect: AspectRatio
count: OutputCount
output_path: Optional[OutputPath] = None
profile: ProfileName = ProfileName("default")
# application/handlers/generate_image_handler.py
class GenerateImageHandler:
def __init__(self, image_provider: ImageProvider, storage: AssetStorage): ...
async def handle(self, cmd: GenerateImageCommand) -> list[Asset]: ...
A tiny in-process CommandBus dispatches cmd → handler.handle(cmd). No middleware, no event sourcing — that would be theatre for a CLI.
# application/bus.py
class CommandBus:
def __init__(self): self._handlers = {}
def register(self, cmd_type, handler): self._handlers[cmd_type] = handler
async def dispatch(self, cmd): return await self._handlers[type(cmd)].handle(cmd)
Why CQRS in a CLI? Two real wins:
- Testability — handlers are easy to test in isolation; CLI is just thin Click glue.
- Future-proof — when a
gflow serveHTTP front-end ships, the command/query layer is reused as-is.
Ports & adapters¶
A port is a Protocol (PEP 544) in application/ports/. An adapter is a concrete class in infrastructure/ that implements one. Handlers depend only on ports.
# application/ports/image_provider.py
class ImageProvider(Protocol):
async def start_generation(self, prompt: Prompt, aspect: AspectRatio, count: OutputCount) -> GenerationJob: ...
async def poll(self, job_id: JobId) -> GenerationJob: ...
async def list_outputs(self, job_id: JobId) -> list[Asset]: ...
# infrastructure/flow/image_provider.py
class FlowImageProvider: # implements ImageProvider
async def start_generation(self, prompt, aspect, count): ... # POST /v1/imagen:generate
async def poll(self, job_id): ... # POST /v1/imagen:checkStatus
async def list_outputs(self, job_id): ...
Tests substitute fakes/mocks for the same Protocol. The handler doesn't change.
Composition root¶
A single function in interfaces/cli/main.py wires the dependency graph. No global registry, no DI framework — explicit construction is short and readable.
def build_bus(settings: Settings) -> CommandBus:
auth_session = PlaywrightSession(profile_dir=settings.profile_subdir(profile_name))
image_provider = FlowImageProvider(auth_session)
video_provider = FlowVideoProvider(auth_session)
storage = AssetStorage.from_settings(settings)
bus = CommandBus()
bus.register(GenerateImageCommand, GenerateImageHandler(image_provider, storage))
bus.register(GenerateVideoCommand, GenerateVideoHandler(video_provider, storage))
# ... etc
return bus
Click commands then become tiny:
@click.command()
@click.argument("prompt")
def generate(prompt: str) -> None:
bus = build_bus(load_settings())
cmd = GenerateImageCommand(prompt=Prompt(prompt), aspect=AspectRatio("1:1"), count=OutputCount(1))
asyncio.run(bus.dispatch(cmd))
Concurrency model (shipped, Phase 4 / v0.4.0a2)¶
- Single-process, single-event-loop (
asyncio). No multiprocessing, no application-level threads (only what Playwright uses internally). - Page pool:
FlowApiClient.__aenter__opensSettings.concurrencyPlaywright Pages inside one shared persistentBrowserContext. Operations check out a Page via anasyncio.Queue(FIFO, bounded bymaxsize=N); a double-checkin raisesQueueFullloudly rather than corrupting the pool. Seegflow_cli.api.client.FlowApiClient._checkout_page/_checkin_page. No current CLI command drives more than one generation concurrently through this pool — the one caller that used to fan out manifest entries viaasyncio.gather(a manifest-driven video batch runner) never worked end-to-end and was removed as a nonfunctional stub;gflow image batchprocesses its manifest sequentially. - Across profiles: parallelism by spawning one shell per profile. Chromium refuses two persistent contexts on the same
user-data-dir, so cross-profile parallelism is a process-level concern, not a coroutine one. See KNOWN_ISSUES § Same profile can't be used in parallel. - Why a Page pool and not a
Semaphore? A semaphore over a single shared Page would serialize atpage.request.postanyway (Playwright doesn't make a Page thread-safe). N Pages inside one Context let Chromium pipeline the requests while still sharing the auth cookies. T0 of the Phase 4 spike measured ~45 ms per Page creation at N=16 — well under the 200 ms/Page threshold. - What's backlog (post-v0.5): a concurrent caller that actually exercises the Page pool above
N=1, account-pool aware scheduling across multiple signed-in profiles (round-robin), and a separate-process driver so two concurrent invocations against the same profile can serialize properly via OS file lock.
Observability (shipped, Phase 4 / v0.4.0a2)¶
structlog is configured at startup via gflow_cli.observability.configure(...), called by _cli_helpers.run_with_handlers(...) at the CLI boundary.
Stable event names emitted at the boundary:
error_raised— caughtGFlowError(and subclasses). Carrieserror_class,problem(the full RFC 9457 Problem Details dict includingtype,title,status,detail,instance,remediation_hint,route), andcli_command.error_unhandled— any non-GFlowErrorreaching the boundary. Privacy-safe: hashes the exception message + stack trace with SHA-256; never logs raw payload. Carrieserror_class,message_hash,stack_hash,cli_command.
Process-boundary contextvars bind once: correlation_id (UUID4 per CLI invocation) and cli_version. Both ride along on every event line.
Format defaults to human-readable on TTY, JSON when piped or GFLOW_CLI_LOG_FORMAT=json. The exception renderer is constructed as structlog.processors.ExceptionRenderer(structlog.tracebacks.ExceptionDictTransformer(show_locals=False)) — the verbose form makes the privacy guarantee visible at the call site (frame locals may contain auth cookies and signed URLs, never log them).
Pipes cleanly into jq / Loki / Datadog without configuration. See docs/USER_GUIDE.md § Journey 6 for jq recipes.
Incident diagnostics (shipped, v0.43.0)¶
FlowApiClient owns one session-scoped IncidentRecorder
(gflow_cli.diagnostics) because it already owns the persistent
BrowserContext, the Page pool, teardown ordering, settings, and the
profile lease. Lifecycle, in order:
- Construct before lease acquisition (plus best-effort retention
pruning), so
ProfileLockedErrorcontention can produce a metadata-only bundle without ever launching Chrome. - Attach listeners after context launch, before the bootstrap
navigation: context-level
request/response/requestfailedplus apagehook, andconsole/pageerroron every pooled page. Callbacks do synchronous primitive extraction into bounded ring journals only — they never retain Playwright objects, read bodies, touch the DOM, or perform I/O. - Capture at the generation boundaries (
generate_image/generate_images_batch/generate_video) while the page is still alive: structural DOM + sensitive screenshot + journal snapshots are staged; the manifest is not yet written. Capture is observation-only and serialized by a recorder-local lock; repeats of one failure fingerprint only bump a suppression counter (max 3 bundles/command). - Teardown order (load-bearing): detach + freeze journals → bounded
context close → resolve HAR state honestly from a pre-launch file
snapshot → atomically finalize staged manifests (
manifest.jsonlast) → driver stop → lease release. Finalization runs through the same bounded/shielded teardown-step helper as everything else and can never mask a close/driver/lease failure or swallow a cancellation.
The legacy capture_ui_diagnostics debug dump consumes the same structural
DOM engine (STRUCTURAL_DOM_JS + validate_structural_dom) — there is one
DOM/screenshot engine, not two artifact formats. See
DEBUGGING § Automatic incident bundles
and SECURITY § Automatic incident bundles.
Testing topology¶
| Layer | Test type | Where | Network? |
|---|---|---|---|
domain/* |
Unit | tests/domain/ |
no |
application/* (handlers) |
Unit (mock ports) | tests/handlers/ |
no |
application/* ↔ infrastructure/* |
Integration (mocked HTTP) | tests/integration/ |
no |
infrastructure/flow/* |
Contract (replayed HTTP fixtures) | tests/providers/ |
no (replay) |
infrastructure/flow/* |
Live opt-in (@pytest.mark.live + GFLOW_LIVE=1) |
tests/providers/test_flow_live.py |
yes |
interfaces/cli/* |
BDD (pytest-bdd Gherkin) |
tests/features/ |
no (mocked provider) |
CI runs everything except live. Live tests run on the maintainer's machine before each release.
When to break the rules¶
The dependency direction is inviolate. Everything else is preference, not religion:
- A 5-line helper that doesn't quite belong in
domain/is fine in ashared/module. - A pragmatic synchronous shortcut in
interfaces/that callsasyncio.run(...)once is fine — handlers stay async, CLI stays simple. - If a Provider implementation needs to call into another Provider for some compound operation, do it from a handler (orchestration belongs in
application/), not from the adapter itself.
See also¶
- PLAN.md — phasing, exit criteria, ADRs
- CONTRIBUTING.md — TDD discipline, test categories
- AUTHENTICATION.md — full auth flow lifecycle
System overview¶
┌─────────────────┐
│ gflow CLI │ ← Click + Rich
└────────┬────────┘
│
┌────────▼────────┐
│ Provider │ ← protocol (planned abstraction — see ROADMAP; not yet a `providers/` package)
│ abstraction │
└────────┬────────┘
│
┌─────┴─────┬───────────────┐
│ │ │
┌──▼──┐ ┌────────┐ ┌───────┐
│Flow │ │Official│ │ Mock │
│(now)│ │ Veo │ │(tests)│
│ │ │(planned│ │ │
│ │ │ later) │ │ │
└──┬──┘ └────────┘ └───────┘
│
│ POST /v1/flow/uploadImage
│ POST /v1/video:batchAsyncGenerateVideoText
│ POST /v1/video:batchCheckAsyncVideoGenerationStatus
│ PATCH /v1/flowWorkflows/{id}
▼
aisandbox-pa.googleapis.com (Google's private Flow API)
The Provider interface keeps backends interchangeable. v0.1 shipped FlowProvider. A future release may add OfficialVeoProvider (via googleapis/python-genai against generativelanguage.googleapis.com) — same code path, swap with GFLOW_CLI_PROVIDER=official.
Auth strategy¶
gflow-cli does not reverse-engineer Google's OAuth flow. Instead it piggybacks on Playwright's persistent context: gflow auth login --browser chrome opens a real Chrome window, the user signs in normally, and the resulting cookie jar is saved to a per-OS user-data dir via platformdirs:
- Windows:
%LOCALAPPDATA%\gflow-cli\profile_default\ - macOS:
~/Library/Application Support/gflow-cli/profile_default/ - Linux:
~/.local/share/gflow-cli/profile_default/
Subsequent commands launch a Playwright context using that profile and call REST endpoints via Playwright's HTTP client — which auto-attaches the cookies. No tokens to refresh manually, no SSO scraping. Auth is the only browser interaction users see, and it is a one-time event.
See docs/AUTHENTICATION.md for the full lifecycle, multi-account workflows, and recovery paths.
Headed-browser dependency — current limitation¶
This is the project's defining trade-off and the most valuable place an external contributor could move the needle.
Why headed¶
Google's auth + reCAPTCHA stack on aisandbox-pa.googleapis.com rejects:
- Playwright's bundled Chromium — flagged by Google's bot detection on first request. The CLI fails fast with
AuthBrowserRejectedError(exit code 14) when this happens. - Headless browsers — same fingerprinting trips during the OAuth-consent flow.
- Bare HTTP clients without the cookies + tokens minted by a real Chrome session — most endpoints return HTTP 401 or a reCAPTCHA challenge that can only be solved interactively.
We attempted three pure-HTTP transport strategies before settling on ui_automation:
evaluate_fetch— callfetch()from inside the Page context to inherit cookies. Worked for some endpoints, failed on video upload (HTTP 401 with no actionable message).bearer— extract bearer tokens from the live page and replay them viahttpx. Tokens rotate too aggressively; rate-limited within minutes.sapisidhash— compute Google's SAPISIDHASH header from session cookies and replay. Works for read endpoints; rejected for any mutation endpoint (including all generation calls).
All three now live as standalone modules under src/gflow_cli/api/transports/experimental/ (evaluate_fetch.py / bearer.py / sapisidhash.py), preserved for reference and future iteration. None survives Google's anti-bot stack for mutation/generation endpoints, so the production path is ui_automation.
Standalone-only transports. bearer and sapisidhash discard any caller-supplied Playwright page and launch their own browser under a fresh ProfileLease, so they are standalone-only — they cannot run inside a FlowApiClient that already holds the profile lease (the second acquire would self-lock with ProfileLockedError). Selecting either via GFLOW_CLI_TRANSPORT (or the Python API) while the client owns the profile now fails fast with a clear ConfigurationError naming the transport, rather than the opaque lock error. evaluate_fetch is exempt: it reuses the client's shared page and takes no second lease. The standalone-only set lives in STANDALONE_ONLY_TRANSPORTS (api/transports/__init__.py); to drive bearer/sapisidhash, run them outside an owning client.
What this costs users¶
- A persistent Chrome profile on disk (~150 MB after
playwright install chromium). - A display server for the one-time
gflow auth login --browser chrome(Linux/WSL need X or Wayland; profile transplants freely between machines after). - Per-account horizontal concurrency is capped by what one warm Page pool can drive —
GFLOW_CLI_CONCURRENCY=Nfans out N Playwright Pages on the same profile, but cannot scale infinitely. - No native serverless deployment. Running on Lambda / Cloud Run / Fly requires a transplanted profile + a desktop-like environment.
How contributors can help¶
The biggest single improvement to gflow-cli's scalability would be a working pure-HTTP transport for video generation that survives Google's anti-bot stack. Specific contributions we welcome:
- Network traffic captures from a successful headed video generation (with personally-identifying data scrubbed) — the Keysight HAR analysis is the public reference; we want our own.
- A working pure-HTTP transport (extend the existing
api/transports/experimental/subpackage) that produces a video against the live API without a browser. - Insight into Google's reCAPTCHA-mint flow for
aisandbox-pa.googleapis.com(especially howAuthorization: SAPISIDHASH ...interacts withX-Goog-Visitor-Id). - An adapter to the official Veo API (
googleapis/python-genai) as a parallel provider — that bypasses the headed-browser dependency entirely for users who have direct API access.
If you ship any of the above, open an issue or a PR — this is the most strategic contribution any new collaborator can make to the project.