zen-video — headless text-to-video queue (GB10)
Fronts Hanzo Studio (ComfyUI, :8188) on the spark GB10 box with a small,
serialized, persisted queue that turns a prompt into a real MP4. Public model
name is the Zen video family (zen3-video); the diffusion backend is the
Zen video model (Wan2.2 TI2V-5B). The upstream/private model filenames live
only in zen_wan_workflow.py and are NEVER returned to callers.
Why a queue
The GB10 is one GPU — one generation at a time. A single async worker drains an
ordered queue and submits to Studio, which itself serializes on the GPU. Jobs
persist in SQLite (jobs.db) so a service restart never loses in-flight or
finished state; interrupted jobs are re-queued on boot.
Two API surfaces (composable, one backend)
-
OpenAI-style — for direct/frontend use, gated by gateway-injected identity:
POST /v1/videos/generations{model, prompt, size?, seconds?, seed?, negative_prompt?}→202 {id, status:"queued", ...}. RequiresX-Org-Id(the gateway injects it from the IAM JWT); anonymous is refused unlessZEN_ALLOW_ANON=1.GET /v1/videos/generations/{id}→ status; on completion{status:"completed", url}.GET /v1/videos/files/{name}→ the MP4 (served over the Cloudflare tunnel).- Bills per generated second into the
usagetable (and posts toZEN_METER_URLif set).
-
do-ai-compatible async — driven by the cloud LLM gateway (
hanzoai/aiGenerateVideoDOAI), gated by a Bearer provider key (ZEN_PROVIDER_KEY):POST /v1/videos{model, prompt, size?, seconds?}→202 {id:"video_…", object:"video", status:"queued"}GET /v1/videos/{id}→{status:"queued|in_progress|completed|failed"}GET /v1/videos/{id}/content→ rawvideo/mp4(only once completed)
This is the exact Sora-style create→poll→download contract the ai service's video client expects, so the ai registry adopts spark by pointing the
zen3-videoprovider athttps://spark-video.hanzo.ai/v1— no ai-side client change.
GET /healthz reports comfy up/down + queue depth. GET /v1/videos/models
lists the Zen video SKUs.
Models
| id | default | max s | steps | $/s |
|---|---|---|---|---|
zen3-video |
1280×704 | 5 | 20 | 0.20 |
zen3-video-fast |
704×704 | 3 | 16 | 0.08 |
zen3-video-pro |
1280×704 | 5 | 30 | 0.40 |
wan2-2-t2v-a14b |
704×704 | 3 | 20 | 0.20 |
Deploy (spark, systemd)
Runs under the Studio venv (aiohttp, boto3). Unit: zen-video.service
(Requires=studio.service). Secrets in /etc/zen-video.env (chmod 600):
ZEN_PROVIDER_KEY (KMS hanzo/video/SPARK_VIDEO_PROVIDER_KEY).
sudo cp zen-video.service /etc/systemd/system/ && sudo systemctl daemon-reload
sudo systemctl enable --now zen-video.service
Reachability: spark is LAN-only/NAT'd. The lux-dchain cloudflared tunnel routes
spark-video.hanzo.ai → localhost:8189 (TLS terminated by Cloudflare).
Delivery
S3-first (SeaweedFS) when ZEN_S3_* is set, else the MP4 is served from this
service over the tunnel at /v1/videos/files/{name} (a scoped, unguessable URL).
NOTE: the cluster SeaweedFS admin identity is currently a placeholder
(PLACEHOLDER/PLACEHOLDER) — provision real creds in KMS to enable S3.
Ops
studio_clean_restart.sh breaks a Studio EADDRINUSE/JIT crash-loop: it stops the
service, frees :8188 of orphans, waits for memory to settle, and starts one
clean instance (studio's comfy_kitchen backend JIT-compiles CUDA kernels once
on first clean boot; a crash-loop never lets that finish → runaway cicc
compilers exhaust unified memory).