H3-CONSOLE.md 11.2 KB

H3 Console — internal video generation API

Internal server running MiniMax H3 on our own GPU cluster (4x GPU, single backend node). No per-call cost, unlike fal.ai/MiniMax cloud. Used as the seed video library + free generation queue for the live channel project.

Two exposed instances, same token:

  • LAN: http://<host>:8080
  • Public: http://qwen.wudaima.com/console (= :8081/console)

Credentials live in .env (H3_CONSOLE_TOKEN, H3_CONSOLE_BASE) — not duplicated here. server.js loads .env at startup via process.loadEnvFile.

Auth

All /v1/* endpoints require:

Authorization: Bearer <H3_CONSOLE_TOKEN>

<video>/download links can't send custom headers, so the file endpoint also accepts ?token=<H3_CONSOLE_TOKEN> as a query param (same check). No token / wrong token → 401. The console pages themselves (/console, /console/newtask.html) load without a token; the page's own JS prompt()s for it on first API call and caches it in localStorage.

Endpoints (verified against live server)

GET {base}/v1/backends

Cluster/queue status.

{
  "status": "ok", "ready": 1, "total": 1, "queued": 0, "running": 0,
  "backends": [{"status":"ok","model":"ready","gpu":"0,1,2,3","partition":"raylight-fsdp","queued":0,"running":0,"backend":"http://127.0.0.1:8100"}],
  "gpus": [{"index":0,"memory_used_mb":5970,"memory_total_mb":22528,"utilization_pct":0}, ...]
}

GET {base}/v1/videos?limit=N&sort=desc

Library listing. limit=200&sort=desc currently returns all 200 most recent jobs (184 done, 13 cancelled, 3 failed). partition breakdown seen: ref2va (reference image/video → video, 164), fl2va (first/last-frame → video, 21), text (pure text-to-video, 15).

{ "jobs": [ { ...same shape as GET /v1/videos/{id}, see below... } ] }

GET {base}/v1/videos/{id}

Single job status/detail (also the poll URL returned by submit).

{
  "id": "44bf5b969e94",
  "status": "done",          // queued | running | done | error/failed | cancelled
  "req": {
    "prompt": "...",          // full structured H3 prompt (can be 100s of words)
    "width": 1024, "height": 576, "frames": 362, "steps": 20,
    "turbo": false, "seed": 42, "cfg_scale": null, "negative_prompt": null,
    "first_frame_url": null, "last_frame_url": null,
    "reference_image_urls": ["https://...png"], "reference_video_urls": [], "reference_audio_urls": []
  },
  "created_at": 1788162094.0, "started_at": 1788162094.2, "finished_at": 1788163221.6,
  "step": 20, "total_steps": 20,
  "file": "/data/models/raylight/ComfyUI/output/video/MiniMax_H3_00272_.mp4",  // server-local path, not fetchable directly
  "size_bytes": 1020185, "error": null,
  "partition": "ref2va", "comfy_prompt_id": "bf8bde0e-...",
  "backend": "http://127.0.0.1:8100",
  "download": "/console/v1/videos/44bf5b969e94/file",   // relative path, use this
  "deleted_at": null
}

frames/fps ≈ 24 (e.g. 362 frames ≈ 15.08s, matches console UI's "15s" default for 1024x768).

POST {base}/v1/videos

Submit a new job. Confirmed live (test job b20846897627, text-to-video, 832x480, 96 frames/4s, 20 steps): took ~201s wall clock (started_at → finished_at) on this cluster — much slower than fal.ai's cloud (~4s for a comparable clip), so treat H3 Console generation as background/best-effort, never something the live player blocks on.

// request
{
  "prompt": "test: a red ball slowly rolling across a plain white floor, static camera, simple lighting",
  "width": 832, "height": 480, "frames": 96, "steps": 20, "turbo": false, "seed": 0
}
// response (immediate)
{
  "id": "b20846897627", "status": "queued", "queue_position": 1, "eta_seconds": null,
  "poll": "/console/v1/videos/b20846897627", "backend": "http://127.0.0.1:8100", "partition": "text",
  "req": { ...echoed... }, "created_at": 1788168842.96, "deleted_at": null
}

Same optional fields as req above (first/last frame urls, reference urls) work for fl2va/ref2va-style submissions — confirmed 2026-09-02 (dance room project spike S1/S2, see spike/s1-s2-s4-2026-09-02/RESULTS.md): reference_image_urls (ref2va, attribute transfer via prompt language) and reference_video_urls (motion/choreography reference) both work as documented on the console UI, reference_video_urls has a real, strong effect on output motion (not a no-op) — A/B tested with identical prompt text and reference images, only toggling this field, produced visibly different movement energy/vocabulary.

POST {base}/v1/assets — undocumented until 2026-09-02, found by reading the console's own front-end JS

Upload endpoint the console's own file pickers use to turn a local file into a URL usable in reference_image_urls/reference_video_urls/first/last frame fields. Tested with an Authorization: Bearer <token> header (same as every other endpoint) — whether it's actually required wasn't isolated. Multipart form, field name file:

POST {base}/v1/assets
Content-Type: multipart/form-data; boundary=...
  file: <binary>
// response
{ "id": "237d91a054cf4113", "url": "http://192.168.110.67:8080/assets/237d91a054cf4113__person.png", "kind": "image" }

url is a direct LAN-IP URL (not behind the qwen.wudaima.com reverse proxy), but it's reachable from outside that LAN too (tested from this dev machine, curl → 200) — treat it as a normal public URL, not LAN-only. This is the answer to "how does H3 reach our uploaded images/videos" (dev-plan.md S4): upload through this endpoint first, use the returned URL in the job submission, instead of standing up our own public file server. Pasting an external URL directly (skipping /v1/assets) also works — the backend does a plain urllib.request.urlopen() on whatever URL it's given, confirmed by the console's own JS source comment.

POST {base}/v1/videos/{id}/prioritize

Queue-jump — moves a job to the front of the queue. Only works on a job whose status is queued; calling it on anything else (running/done/failed) 409s. Confirmed live 2026-09-07: used to bump 15 correction-regen jobs to the front after finding the shared cluster had 113+ jobs queued ahead of them.

{ "id": "a6601db83700", "status": "queued", "queue_position": 1 }

Calling it on several jobs in a row puts the most-recently-prioritized one at position 1 and pushes the others back slightly — still clusters them all near the front, just not in a guaranteed relative order between themselves.

POST {base}/v1/videos/{id}/cancel

Genuinely cancels a queued/running job — unlike DELETE (below), which only hides it from listings but lets it keep consuming compute. Confirmed live 2026-09-09: cancelling a queued job flips its status to cancelled, sets error to "用户手动取消" and finished_at, and the cluster's queued count in GET /v1/backends actually decrements. Use this (not DELETE) to actually stop a duplicate/unwanted queued job.

{ "id": "6361be157e48", "status": "cancelled", "error": "用户手动取消", ... }

GET {base}/v1/videos/{id}/file

Downloads the mp4. Content-Type: video/mp4, supports range requests (Accept-Ranges: bytes), Content-Disposition: attachment. Auth via Authorization header or ?token=.

DELETE {base}/v1/videos/{id}

Soft-delete — moves the job to the console's own 回收站 (recycle bin), sets deleted_at, and it stops appearing in GET /v1/videos list results.

{ "id": "b20846897627", "deleted": true }

Gotcha: this does NOT cancel an in-flight job. A queued/running job you delete keeps running to completion on the GPU — you've only hidden it from listings, not freed the compute. GET /v1/videos/{id} (single-job lookup) still returns it in full, deleted_at set, once it finishes. Our generateForPrompt() in server.js checks deleted_at and treats it as a failure so a deleted job can't silently get pulled into library.json — learned this the hard way after deleting jobs mid-generation during testing.

Prompt format

Jobs generated through the console's own UI use a long structured prompt (sections: subject_definitions:, summary:, retention_analysis:, detailed_description:, overall_soundscape:, non_diegetic_music:) — these are drama-production character/expression reference clips (project looks like a "画皮"-style period-horror short drama), not one-line prompts. Our library ingest needs to rule-extract a short display prompt from this (see ingest.js — ponytail: regex extraction from the summary:/ detailed_description: section, not an LLM call, per explicit decision to keep this free).

Our server (server.js) — what it builds on top of the console API

Runs under pm2 as minimax-live (pm2 start server.js --name minimax-live, pm2 save). Auto-restarts on crash; does not yet survive a machine reboot — that needs pm2 startup run once with sudo (prints the exact command; not run automatically since it's a system-level change).

  • Library sync: ingest.js's syncLibrary() runs on server startup and every 5 minutes, rewriting library.json from the console's current job list (rule-extracts a short display prompt, filters out infra smoke-test jobs, preserves likes across re-syncs). So content added directly through the console UI shows up here within 5 minutes, no manual step needed. Admin overrides survive this: any entry with edited: true (set by admin.html's edit/add) is carried forward as-is instead of being rebuilt from the H3 job — otherwise every admin edit would get silently reverted on the next sync, and manually-added entries (not backed by any H3 job at all) would just vanish.
  • Live schedule: schedule/promptQueue (in server.js) are the shared virtual playhead + prompt queue described in the main project — persisted to live-state.json every tick so a restart resumes instead of going cold. A generating entry also persists its h3JobId once known, so a restart mid-generation resumes polling that same job instead of losing it.
  • Endpoints: GET /api/library, GET /api/live/state, POST /api/prompt, POST /api/like/:id, GET /lib/video/:id (proxies GET {base}/v1/videos/{id}/file server-side so H3_CONSOLE_TOKEN never reaches the browser; if the clip has its own videoUrl — set via admin.html, or any source: "manual" entry — this 302-redirects there instead), and the admin.html CRUD trio: POST /api/admin/library, PATCH /api/admin/library/:id, DELETE /api/admin/library/:id (deleting an h3console-sourced entry also calls DELETE {base}/v1/videos/:id so it doesn't reappear on the next sync — see the DELETE gotcha above, it only hides the job, doesn't cancel it if still running).
  • No auth on any of this — same as the rest of the project, these endpoints (admin CRUD included) trust the network they're reachable on.

ComfyUI direct access (not currently used by our integration)

comfyui.wudaima.com — raw ComfyUI behind HTTP Basic Auth (frpc http_user/http_pwd, no changes to ComfyUI itself). Creds in .env (COMFYUI_BASE, COMFYUI_USER, COMFYUI_PASS). We go through H3 Console's /v1/* wrapper API instead, which already gives us a queue/status/library interface — no reason to hit ComfyUI directly unless that wrapper is insufficient later.