H3-CONSOLE.md 8.33 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) presumably work for fl2va/ref2va-style submissions — not yet tested from our side, only inferred from console UI tabs (文生视频/首尾帧生视频/多参考图生视频).

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.