From fcab3e2f1debe4ef1b999b3e7ab193947d978f85 Mon Sep 17 00:00:00 2001 From: timtoole02 Date: Tue, 11 Aug 2026 08:10:39 -0700 Subject: [PATCH 1/3] Add MiniMax-H3 video generation --- COMPATIBILITY.md | 4 + Cargo.toml | 8 +- DOCS.md | 1 + README.md | 22 + THIRD_PARTY_NOTICES.md | 19 +- camelid-desktop/Info.plist | 8 + camelid-desktop/src/main.rs | 20 + camelid-desktop/tauri.conf.json | 4 +- docs/runtime/minimax-h3-video.md | 176 ++++ frontend/src/App.jsx | 23 +- frontend/src/components/CommandPalette.jsx | 1 + frontend/src/components/TopBar.jsx | 1 + .../src/components/layout/SidebarRail.jsx | 3 +- frontend/src/components/ui/icons.jsx | 1 + frontend/src/lib/desktopShell.js | 22 +- frontend/src/styles.css | 1 + frontend/src/styles/shell.css | 17 - frontend/src/styles/video.css | 126 +++ frontend/src/views/VideoView.jsx | 335 +++++++ scripts/build-macos-desktop.sh | 13 + src/api/mod.rs | 37 +- src/api/video.rs | 865 ++++++++++++++++ src/lib.rs | 1 + src/main.rs | 267 +++++ src/minimax_h3.rs | 933 ++++++++++++++++++ 25 files changed, 2843 insertions(+), 65 deletions(-) create mode 100644 camelid-desktop/Info.plist create mode 100644 docs/runtime/minimax-h3-video.md create mode 100644 frontend/src/styles/video.css create mode 100644 frontend/src/views/VideoView.jsx create mode 100644 src/api/video.rs create mode 100644 src/minimax_h3.rs diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index e19d69144..8eaae799e 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -30,6 +30,7 @@ Camelid's public support language is intentionally narrow, evidence-bound, and e - **Full-support normalization rule:** those Llama rows are not broader/full support until they meet the same normalized standard as TinyLlama: repeated current-head parity, API evidence, WebUI evidence, 50-token output, longer-context coverage, memory/performance bounds, and durable current-head evidence bundles. - **Embedding exact-row rule:** `nomic-embed-text-v1.5.Q8_0.gguf` (146,146,432 bytes, sha256 `3e24342164b3d94991ba9692fdc0dd08e3fd7362e0aacc396a9a5c54a544c3b7`) is supported only for the Nomic-BERT CPU embedding lane: exact WordPiece tokenization, 768-dimensional unit-normalized embeddings, Matryoshka truncation, embedding-similarity reranking, and bounded in-memory Workspace semantic retrieval. Three oracle vectors match llama.cpp b10173 at cosine `>0.9997` and max absolute delta `<0.003`; relevant/irrelevant semantic scores preserve the reference ordering. This does not imply generic BERT, other Nomic files/quants, classifier-head cross-encoder ranking, GPU encoder execution, or generative support. - **BitNet groundwork-only rule:** the official `BitNet-b1.58-2B-4T` causal I2_S GGUF and the `BitNet-embedding-0.6B` / `BitNet-embedding-270M` I2_S GGUFs are cataloged experimental bring-up rows. Tensor sizing/dequantization, NEOX RoPE, causal SubLN, the embedding projection-input norms, GGUF-declared pooling, embedding/rerank API routing, generation refusal for embedding-only files, and cleanroom CPU/Metal/CUDA projections are implemented. The `i2_s`, `tl1`, and `tl2` modes all consume canonical I2_S bytes and do not claim support for BitNet.cpp's separately permuted TL files. No row is supported until exact-artifact tokenizer/output or embedding-vector parity is captured against the pinned Microsoft BitNet reference; the embedding cards' last-token-pooling text also conflicts with the current GGUFs' mean-pooling metadata and must be adjudicated before promotion. +- **MiniMax H3 video groundwork rule:** `camelid video` downloads and SHA-256 verifies the exact Unsloth `fl2va` UD-Q2_K_XL or `ref2va` Q2_K diffusion GGUF, the paired Q2_K_M H3 Qwen3-VL encoder, and the exact Comfy-Org video/audio VAEs after explicit license acknowledgement. It validates H3 conditioning and invokes a capability-checked `stable-diffusion.cpp` backend with H3-safe settings. Video Studio and its loopback-only asynchronous job API orchestrate the same bridge. This is not a native Camelid runtime or a supported video row; output parity/quality/performance, other quants, `/v1/videos` compatibility, and portable fit remain unclaimed. - **Mistral exact-row rule:** Mistral-7B-Instruct-v0.3.Q8_0.gguf is supported exact-row smoke (promoted post-v0.1.0 at head d7b1699). Source/SHA, tokenizer/template, 1-token generation, broader five-prompt/50-token parity, bounded 512/1024/2048, checked 4096/8192 context evidence, GPU-vs-CPU greedy parity, and the support-promotion API/WebUI smoke bundle `qa/evidence-bundles/mistral-7b-v0.3-q8-support-promotion-20260605T090914Z-head-d7b1699/manifest.json` back the claim; the contract records `supported_exact_row_smoke` and WebUI chat unlocks via the standard green-when gate. Other Mistral variants, sizes, quants, or adjacent families remain unsupported. - **Mixtral active-validation rule:** `Mixtral-8x7B-Instruct-v0.1.Q8_0.gguf` has bounded one-token backend MoE runtime evidence only. Later-generation evidence diverges and a longer-continuation backend HTTP hang remains unresolved, so no Mixtral API/WebUI/frontend readiness, neighboring-row, broad-family, long-context, production, portability, or broader/full support claim may be published. - **Mixtral continuation rule:** continuation/long-generation hardening is blocker work, not promotion evidence; it may not widen Mixtral wording until later-generation parity plus API/WebUI/RSS and frontend readiness artifacts are green and synchronized. @@ -67,6 +68,7 @@ Use this table when writing README copy, frontend readiness text, API summaries, | Qwen3-8B-Q8_0.gguf | Supported exact-row smoke (ChatML, thinking DISABLED): square head_dim, UNTIED embeddings (separate output.weight) — first Qwen3 row exercising the untied output projection. Token-AND-text-identical greedy parity vs pinned llama.cpp at 1/5/50 tokens for three fixed prompts (`all_pass=true`). Tested on mini2 (clean 16GB). `qa/evidence-bundles/qwen3-8b-q8-chatml-parity-20260614T072602Z-head-368ed9b/`. Also runs on the **GPU-resident decode + single-shot prefill** path (per-head QK-norm + untied output.weight in-kernel): token-AND-text-identical to llama.cpp on the same 3-prompt 1/5/50 support bar (`qa/evidence-bundles/qwen3-8b-q8-gpu-resident-parity-20260614T213932Z-head-a0ee3d6/`, `all_pass=true`). | Resident single-shot prefill engages and produces coherent output at a 7,982-token context (single-shot prefill ceiling 16,384; KV ceiling 40,960); 8B large-context token-parity is not separately claimed (the resident prefill kernel is bit-exact-proven vs llama.cpp at 15,373 tokens on 1.7B). **Thinking mode** is available opt-in (`camelid_enable_thinking:true`; also `--enable-thinking` / WebUI toggle) on the `thinking_opt_in_leading_trace_only` lane: template-shape byte parity for the `enable_thinking=true` rendering is fully locked (`qa/prompt-packs/qwen3-chatml-thinking-template-pack-v1.json`), and thinking engages with leading-trace parity vs llama.cpp (b9430) token-identical across a **host-bounded 16-token window** for all four probes (no divergence within it). The full leading-trace envelope is **host-bounded, not yet captured**: 8B Q8 (8.1 GB) thrashes the page cache on this 16 GB host (~53 s/token, GPU absent), so the 256-token trace is deferred to a resident-capable host — bundle `qa/evidence-bundles/qwen3-8b-q8-thinking-leadingtrace-*/` (full llama.cpp 256-token reference captured; camelid side host-bounded). | Other Qwen3 sizes, base variants, other quants, MoE (A3B), context above the 16,384/40,960 ceilings, token-parity at 8B large-context, full-trace thinking-mode token-parity, a thinking leading-trace envelope beyond the host-bounded 16-token window (thinking-DISABLED stays the parity-locked mode), or broad Qwen support. | | gemma-3-1b-it-Q8_0.gguf | Supported exact-row smoke on the **Metal GPU-resident serve lane**, which is the **default** lane for this row on a Metal host (`selected_backend=metal_resident_q8_runtime`, `decode_path=q8_0_metal_resident_decode`; opt-out `CAMELID_METAL_RESIDENT_DECODE=0`) — **promoted 2026-07-16 (MUSTER M-A1) on the runnable CPU bridge, re-anchored onto the resident lane 2026-07-30**, recognized in `/api/capabilities` as row `gemma_3_1b_it_q8_0`. This is the first gemma3 forward in the tree that applies the 512-token sliding-window mask and the 5:1 local/global schedule. The gemma3 marker renderer is byte-locked against the pinned oracle's `/apply-template` output (`qa/prompt-packs/gemma3-chat-template-shapes-v1.json` + in-src pack-lock test). Greedy chat parity vs pinned llama.cpp `acd79d603` (build 9632, CPU `-ngl 0 -ctk f32 -ctv f32 -fa off --no-repack`): **15/15 legs token-AND-text identical** on the committed 5-prompt gate pack at depths 1/5/50, prompt tokenization identical 5/5, zero flips. **On a CUDA host the row is served by default on the CUDA GPU-resident windowed lane** (`selected_backend=cuda_resident_windowed_runtime`, `decode_path=q8_0_cuda_resident_windowed_decode`; opt-out `CAMELID_GEMMA3_CUDA_RESIDENT=0`), which carries the same window mask and dual-θ schedule. That lane is **9/9 identical above the window** against the same pinned captures but only **10/15 below it** — five divergences across three prompts, attributed to lane numerics by a per-layer hidden-state trace (no step change at any layer, worst relative L2 1.89%) and **disclosed rather than adjudicated**, since confirming them from the oracle side needs a live llama.cpp run that was not performed. **Where neither GPU-resident lane can run — no Metal and no CUDA device, resident decode opted out, deterministic mode — and for any non-Q8_0 gemma3 file, serve falls back to the runnable CPU bridge**, which has no window mask; the windowed claim below does NOT travel with it. Raw `/v1/completions` is a SEPARATE harness on its own 4-prompt set (not the 5-prompt chat pack above) and is disclosed, not claimed: clean 4/4 at depths 1 and 5, with three near-tie flips at depth 50 (one at **0.4471 nat** against a rank-1-stable oracle) — committed as-is with `all_pass=false`. No throughput or speed claim is made for this lane. Bundle `qa/evidence-bundles/gemma3-1b-q8-gpu-resident-parity-20260730-head-6eaf9053/` (README + manifest + SHA256SUMS); the frozen runnable-lane bundle `qa/evidence-bundles/gemma3-1b-q8-runnable-serve-chat-parity-20260716-head-6d0d57eb/` stands as history for that other lane and is not re-adjudicated. | **Above the 512-token sliding window, proven externally**: committed pack `qa/prompt-packs/gemma3-windowed-context-pack-v1.json` renders to **606 / 1205 / 2403 prompt tokens** (1.18x / 2.35x / 4.69x the file's own `gemma3.attention.sliding_window = 512`), each answerable only from its first sentence — **9/9 generation legs at depths 1/5/50 token-AND-text identical** to the pinned oracle, prompt tokenization 3/3, zero flips. Checked boundary is therefore **2,403 prompt + 50 generated tokens on the resident lane**; the file's native 32,768 and everything between ~2.4k and 32k is UNMEASURED. Determinism: byte-identical decode across two fresh serve processes, including past the window. No bounded-context ladder bucket was run for this row. On the **runnable CPU bridge** the pre-existing boundary stands unchanged and is now measured: that lane implements no window mask, and on the same 606-token prompt it diverges from the oracle at generated index 2 and never resynchronises, its token **1.667 nats** behind the oracle's — not a near-tie. One prompt at one depth: a demonstration that the window is real, not a runnable-lane receipt. | Any throughput or speed number on this lane, or any speed comparison with another engine. Token-exact raw `/v1/completions` beyond depth 5 (three disclosed near-ties at depth 50, one at 0.4471 nat — stated, not eliminated). Any context above 2,403 prompt tokens, the bounded-context ladder packs, or model-native 32,768 context. Any >512-token claim on the runnable CPU fallback lane. Multi-turn, streaming, tools (fail closed with a typed 422; no gemma3 tool branch or certified grammar exists on any lane), tool-capability, speculative decode or the prompt-prefix cache (both fail closed for windowed archs). Neighbouring gemma3 sizes/quants, the gemma4 family, frontend load-path promotion, portability, or broader/full support. | | diffusiongemma-26B-A4B-it-Q4_K_M.gguf | **Supported (experimental) via the dedicated DiffusionGemma lane** (`camelid diffusion-gemma-chat`; CPU-pure build, GGML_BLAS=OFF). DiffusionGemma is a discrete block-diffusion encoder-decoder, not an autoregressive model, so it runs through its own diffusion-lane runtime (`DgEncoderRuntime`), NOT the AR engine (which fail-closes the architecture and points to the lane). End-to-end bit-exact parity vs the pinned llama.cpp diffusion reference at zero tolerance across the whole stack: tokenizer (12/12 raw + chat-template cases), wire dequant (the file's five quant formats), encoder checkpoints (242/242), single denoise step (256×262144 canvas logits + the mt19937/libc++ RNG streams), the full Entropy-Bound denoise loop (S=48, 268,435,456 logits, adaptive stop matched), and the multi-canvas block-autoregressive loop (Phase 5: 2 blocks, 22 + 26 EB steps, 512-token response byte-identical to the reference) — sealed bundle `target/dg-mc-loop-parity-20260616T011145Z/`; recon at [`docs/recon/DIFFUSIONGEMMA_RECON.md`](docs/recon/DIFFUSIONGEMMA_RECON.md). | A ≤256-token canvas per block; the validated multi-canvas gate ran 2 blocks (512-token response) within one ubatch on the CPU-pure pinned build. The serve chat path is on by default (opt-out `CAMELID_DG_SERVE=0`) and experimental. | Production throughput (CPU-only, ~minutes per denoise step; no GPU/Metal decode), GPU-resident or distributed DiffusionGemma, other DiffusionGemma quants/sizes, multimodal (image/video) input, the autoregressive engine running this architecture (it redirects to the lane by design), or any inheritance from Gemma 4 rows. | +| MiniMax H3 `fl2va` UD-Q2_K_XL / `ref2va` Q2_K bundle | Groundwork only: experimental `stable-diffusion.cpp` backend bridge through Video Studio, the loopback job API, and `camelid video`; exact download sizes and SHA-256 digests are pinned. | CLI plan/doctor tests plus UI/API orchestration tests only; no generated-output evidence bundle has been captured. | Supported/native Camelid H3, output parity or quality, performance/memory fit, `/v1/videos` compatibility, other H3 components or quants, backend-revision portability, or production use. | | gemma-4-E2B-it-Q8_0.gguf | Supported exact-row text-token-generation smoke: five-prompt greedy parity (prompt ids, generated ids, text) vs pinned llama.cpp 5d56eff on CPU AND the Metal GPU-resident runtime, reference-exact template shapes, API/streaming chat smoke, distributed-sharding parity with fail-closed guards, and the distributed serve lane (loopback-validated). | **Bounded-context bundle committed** (512/1024/2048/4096/8192): on current head the exact row is greedy token-AND-text-identical (prompt ids, generated ids, generated text) to the pinned llama.cpp 5d56eff oracle at every bucket, via the in-tree `gemma4_generation_parity` harness — bundle `qa/evidence-bundles/gemma4-e2b-e4b-context-512-8192-20260706T190805Z-head-0b0e4709188f/` (`passed=true`, both E2B-it and E4B-it Q8_0). Model-native/larger context beyond the checked 8192 bucket is not promoted. | Gemma-family support, multimodal input, model-native/larger context beyond the checked 8192 bucket, production throughput, neighboring rows, or full support. | | gemma-4-E4B-it-Q8_0.gguf | Supported exact-row text-token-generation smoke: five-prompt greedy parity vs the pinned reference on CPU AND the Metal GPU-resident runtime, reference-exact template shapes, API/streaming chat smoke on the gemma4 serve lane (on by default; opt-out CAMELID_GEMMA4_SERVE=0), and the distributed serve lane. Windows NVIDIA CUDA decode is experimental (first-token argmax matches the CPU oracle via an `#[ignore]` in-tree gate; not token-for-token, no committed bundle). | **Bounded-context bundle committed** (512/1024/2048/4096/8192): on current head the exact row is greedy token-AND-text-identical (prompt ids, generated ids, generated text) to the pinned llama.cpp 5d56eff oracle at every bucket, via the in-tree `gemma4_generation_parity` harness — bundle `qa/evidence-bundles/gemma4-e2b-e4b-context-512-8192-20260706T190805Z-head-0b0e4709188f/` (`passed=true`, both E2B-it and E4B-it Q8_0). Model-native/larger context beyond the checked 8192 bucket is not promoted. | Gemma-family support, multimodal input, model-native/larger context beyond the checked 8192 bucket, Windows-CUDA token-parity, production throughput, neighboring rows, or full support. | | gemma-4-12b-it-Q8_0.gguf | Active validation, two-Mac distributed lane ONLY (not promoted to support): two-Mac distributed sharding runs all five pack prompts token-identical to single-node camelid (decode 6.2-6.75 tok/s across the pair, both 16 GB nodes within budget); 3/5 match the pinned llama.cpp comparator full-budget with two recorded reference-comparator frontiers (the reference has no sound bit-exact mode for this row). The distributed SERVE lane now answers /v1/chat/completions (non-streaming + SSE) and /v1/completions end-to-end across the two Macs (master shard 0..24 + worker 24..48, wire v1; bundle qa/evidence-bundles/gemma4-12b-it-q8-0-distributed-serve-20260610T235155Z-head-80e3dddfbdb4). V-less GPU kernels are implemented and validated at layer scope; full-row GPU residency is untested and memory-infeasible on 16 GB hosts. WebUI promotion smoke (model load, completions, chat, capabilities expectations, frontend closure) passes through the distributed lane. | basic_v1 prompt pack via the two-Mac bundle qa/evidence-bundles/gemma4-12b-it-q8-0-two-mac-20260610T103711Z-head-96a75007b156, the distributed-serve API smoke bundle, and the WebUI promotion smoke bundle; no context bucket. | Single-node support, GPU full-row claims, bounded/model-native context, or performance claims beyond the recorded pair cadence; nothing beyond the two-Mac distributed lane. | @@ -291,6 +293,7 @@ For **Llama 3 8B** specifically, the durable citation anchors are the current-he | Gemma 3 exact row | Supported exact-row smoke | `gemma-3-1b-it-Q8_0.gguf` on the Metal GPU-resident serve lane, which is the DEFAULT lane for this row on a Metal host (`selected_backend=metal_resident_q8_runtime`). No other Gemma 3 size, quant, or platform is implied. | | Gemma 4 exact rows | Mixed: supported smoke, distributed active validation, and fail-closed | `gemma-4-E2B-it-Q8_0.gguf` and `gemma-4-E4B-it-Q8_0.gguf` are supported exact-row smoke with five-prompt greedy parity vs pinned llama.cpp `5d56eff` on CPU AND the Metal GPU-resident runtime. `gemma-4-12b-it-Q8_0.gguf` and `gemma-4-26B_q4_0-it.gguf` are active validation on the two-Mac distributed lane ONLY and are NOT promoted to support. 26B-A4B `Q8_0`, 31B, and the MTP rows remain fail-closed with typed errors. | | DiffusionGemma | Supported (experimental) via a dedicated lane | `diffusiongemma-26B-A4B-it-Q4_K_M.gguf` runs only through `camelid diffusion-gemma-chat` on a CPU-pure build. It is a discrete block-diffusion encoder-decoder, so it is not runnable on the autoregressive serve path and promotes nothing on it. | +| MiniMax H3 video bundle | Groundwork / experimental backend bridge | Video Studio, its loopback-only job API, and `camelid video` recognize the pinned `fl2va` UD-Q2_K_XL or `ref2va` Q2_K diffusion GGUF plus the Q2_K_M H3 Qwen3-VL text encoder and the exact video/audio VAEs. Pull is license-acknowledged, resumable, size-checked, and SHA-256-pinned; generation validates conditioning modes and invokes a capability-checked `stable-diffusion.cpp` `sd-cli` with H3-safe defaults. No native Camelid diffusion runtime, output-parity/quality/performance receipt, `/v1/videos` compatibility, other quant, or portable hardware-fit claim is made. | | Gemma 2-family GGUF | Planned exact-row candidate, unsupported today | First candidate: `gemma-2-9b-it-Q8_0.gguf`. Confirm Gemma2 architecture/control-token/template behavior, bounded load, and prompt-token parity before any runtime-support wording. The supported Gemma 3 and Gemma 4 rows above lend this row nothing. | | Phi-family GGUF | Groundwork only — formal HOLD, downloadable but not advertised | `Phi-3-mini-4k-instruct-Q8_0.gguf` is in the curated `camelid pull` catalog and loads on the experimental lane, but it is NOT advertised as supported and holds no `/api/capabilities` row. Sealed verdict: `qa/muster/HOLD-phi3-mini-4k-instruct-q8_0.json`. Prompt-token parity passes (8/8 raw, 3/3 chat), but two blockers stand: an SPM rstrip seam on the chat special tokens whose fix would touch the shared SPM tokenizer under parity-locked rows, and non-determinism at temperature 0 on this row specifically (identical requests return different tokens). A probe-proven NEOX RoPE flip and a byte-locked `render_phi3_prompt` shipped as engine facts with no support claim. `Phi-4-mini-instruct-Q4_K_M` is likewise HELD. | | LFM2 / LFM2.5 | Supported exact-row smoke for one hash-pinned row on the Windows runnable CPU lane and one tested Apple M4 resident-Metal lane | `LFM2.5-2.6B-Q8_0.gguf` is supported only for the hash-pinned `LiquidAI/LFM2.5-2.6B-GGUF@b421ad1d549afeda6a0fb2ad3a697cb5a7879adc` artifact (2,874,779,456 bytes, SHA-256 `36587fdf27bdfc69caf2637273679a0870ec155162161bde6fd16e8c70bdb757`) on the Windows x86_64 runnable CPU chat lane (`qa/evidence-bundles/lfm2-2.6b-q8-phase1-promotion-20260810/`) and the resident-Metal lane on Apple M4 / macOS 26.5 / arm64 (`qa/evidence-bundles/lfm2-2.6b-q8-macos-metal-20260810-head-d31e5cb0/`). Raw completions remain refused by design; nothing is inherited by neighboring artifacts or broader LFM, and CUDA, other Apple hardware, broad portability, context above 512, sampling, tools, and throughput remain unclaimed. | @@ -329,6 +332,7 @@ For **Llama 3 8B** specifically, the durable citation anchors are the current-he | `/v1/models`, `/api/models/load`, `/api/models/current` | Supported current gate; row-scoped Llama support | Local GGUF load/list/readiness path used by the frontend; `/v1/models` includes llama-server-style public `meta` for loaded dense GGUF rows without exposing local paths, and returns `meta: null` when dense runtime metadata is not available. Exact Llama 3.2 1B and Llama 3 8B rows are verified within bounded envelopes, while the exact 3B row remains smoke-supported and all broader/full-support language still waits on normalized current-head reruns. | | `/models`, `/models/load`, `/models/unload` | Partial llama-server model-discovery/load compatibility | `GET /models` may expose only a privacy-safe list of currently loaded Camelid models with redacted paths and text-only architecture metadata when focused API tests cover that shape. `POST /models/load` is a narrow local-path alias over Camelid's stable `/api/models/load` path and returns a redacted compatibility response. Router-mode cache listing, reload/autoload, native `POST /models/unload`, multimodal architecture metadata, and full llama-server model-management parity remain unsupported; this route does not unlock WebUI readiness or broaden exact-row support. | | `/api/capabilities` | Supported contract surface | Exposes explicit support contract, supported/planned quants, model families, and API features; row statuses must distinguish TinyLlama current-gate support, Llama 3.2 1B and Llama 3 8B bounded support, and Llama 3.2 3B exact-row smoke support. | +| `/api/video/capabilities`, `/api/video/jobs*` | Experimental loopback-only local orchestration | Video Studio uses a serialized asynchronous MiniMax H3 job queue, cancellation, backend logs, and range-capable WebM delivery through the external `sd-cli` bridge. This is not `/v1/videos` compatibility or model-support evidence. | | `/tokenize`, `/detokenize` | Partial llama-server utility compatibility | Bounded loaded-model tokenizer aliases are available for token-id encode/decode. `/tokenize` also supports `with_pieces=true` for loaded supported tokenizer lanes by returning id/piece objects; arbitrary tokenizer kwargs and broader tokenizer parity remain unsupported, and tokenizer availability still depends on a loaded exact-row/tokenizer-supported GGUF. | | `/props` | Partial llama-server control-plane compatibility | Read-only public properties are available for WebUI/client discovery: default generation settings, slot count, chat-template metadata when a model is loaded, and fail-closed Camelid readiness notes. Local model paths are intentionally redacted, `POST /props` is unsupported, and this does not imply slot lifecycle, streaming native `/completion`, generic embedding-model compatibility, multimodal, or full llama-server WebUI parity. | | `/slots` | Partial llama-server control-plane compatibility | `GET /slots` returns one read-only, privacy-safe entry per admissible cooperative streaming slot — the same count `/props` reports as `total_slots` — with generation readiness and `fail_on_no_slot=1` handling that refuses only when every slot is busy. Per-slot task identity and per-slot progress are engine-wide values repeated on the busy entries (declared as `per_slot_task_identity` / `per_slot_progress`). `POST /slots`, slot save/restore/erase actions, prompt-cache metadata, cancellation metadata, continuous batching metrics, and full llama-server slot lifecycle parity remain unsupported. | diff --git a/Cargo.toml b/Cargo.toml index cc4310cdb..9c0d7682e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,10 +49,10 @@ thiserror = "1" # contract and use the pure-Rust regex backend: no Oniguruma/C build, C++ esaxx, # progress UI, or hf-hub/network feature enters Camelid's cross-platform build. tokenizers = { version = "=0.23.1", default-features = false, features = ["fancy-regex"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] } -# CancellationToken for the generation paths: guard/compute lifetime equivalence -# (see docs/recon/ENGINE_INVERSION_CONDUCTOR.md). sync-only, no extra features. -tokio-util = "0.7" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "fs", "io-util"] } +# CancellationToken for generation guard/compute lifetime equivalence plus +# ReaderStream for range-capable local MiniMax-H3 WebM delivery. +tokio-util = { version = "0.7", features = ["io"] } tower-http = { version = "0.5", features = ["trace", "cors"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/DOCS.md b/DOCS.md index 1c81db883..1775d7c98 100644 --- a/DOCS.md +++ b/DOCS.md @@ -48,6 +48,7 @@ These documents are working notes, not support ledgers. When a note and a public - [`ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) - [`STRUCTURED_OUTPUTS.md`](docs/architecture/STRUCTURED_OUTPUTS.md) — LLGuidance JSON Schema and CFG constrained decoding contract - [`EMBEDDINGS.md`](docs/architecture/EMBEDDINGS.md) — exact-row encoder, embedding/reranking APIs, and Workspace semantic retrieval contract +- [`minimax-h3-video.md`](docs/runtime/minimax-h3-video.md) — experimental MiniMax H3 bundle download, backend setup, Video Studio, local job API, and CLI - [`WORKSPACE_CLI.md`](docs/architecture/WORKSPACE_CLI.md) — durable read-only Workspace commands and the same-user loopback authentication contract - [`SPECULATIVE_DECODE.md`](docs/architecture/SPECULATIVE_DECODE.md) — default-off lossless greedy speculation: proven byte-exact, faster than the default stack on repetitive output (measured envelope inside), CPU-vanilla floor elsewhere - [`FORGELOCAL_INTEGRATION.md`](docs/architecture/FORGELOCAL_INTEGRATION.md) diff --git a/README.md b/README.md index e5bedea92..be8c52b9b 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,26 @@ The desktop **Models** page downloads the projector automatically with either 27 The exact Nomic Embed Text v1.5 Q8_0 row supports OpenAI-compatible `/v1/embeddings`, Matryoshka dimensions, cosine-similarity reranking through `/v1/rerank`, and optional in-memory semantic retrieval for Workspace. The encoder currently runs on CPU; other embedding families and quantizations fail closed. See the [embedding API guide](docs/architecture/EMBEDDINGS.md) for loading and request examples. +### Experimental MiniMax H3 video + +Camelid has an experimental **Video Studio** UI and CLI lane for local MiniMax H3 +video generation through an H3-capable `stable-diffusion.cpp` backend. It supports +text, initial/final-frame, and reference-conditioned WebM/AVI generation with +optional native stereo audio. The UI queues long jobs asynchronously, reports +bundle download progress, and plays completed WebM clips. +The default four-part bundle is about 25.13 GiB and is downloaded only after an +explicit MiniMax-H3 Community License acknowledgement. + +```bash +camelid video pull --variant fl2va --accept-license +camelid video doctor --variant fl2va --verify-sha256 +camelid video generate --prompt "a red panda in a misty forest, cinematic" --output panda.webm +``` + +This is a capability-checked external backend bridge, not yet a Camelid-native or +parity-certified video runtime. See the [MiniMax H3 video guide](docs/runtime/minimax-h3-video.md) +for backend setup, hardware caveats, reference-media commands, and the exact support boundary. + ## Ways to use Camelid | Interface | Start it with | Best for | @@ -203,6 +223,8 @@ The exact Nomic Embed Text v1.5 Q8_0 row supports OpenAI-compatible `/v1/embeddi | **HTTP API** | Start `camelid serve` | Chat, image input, embeddings, and reranking | | **Agent mode** | `camelid chat --agent --model ` | Approval-gated tools in a repository | | **Workspace** (preview) | Open **Workspace** in the web UI | Read-only analysis of a local folder | +| **Video Studio** (experimental) | Open **Video Studio** in the web UI | Queue and preview local MiniMax H3 videos | +| **Video CLI** (experimental) | `camelid video generate` | Local MiniMax H3 video/audio generation | Agent mode confines file tools to a workspace root and keeps network access off unless enabled. Workspace is read-only and resumable. Both require a model marked `tool_capable` in the compatibility ledger. Review the [agent documentation](DOCS.md) and every requested action before enabling additional tools or network access. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index a860ae759..2d88e8d49 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,6 +1,6 @@ # Third-Party Notices -Last updated: 2026-06-18 +Last updated: 2026-08-11 ## Scope note @@ -77,6 +77,23 @@ ecosystem); this records its promotion to a direct, load-bearing dependency. - Project: - License: MIT +### stable-diffusion.cpp (optional MiniMax H3 video backend) + +The experimental `camelid video` lane can invoke a separately installed +`stable-diffusion.cpp` `sd-cli` binary for MiniMax H3 inference. Camelid does not +redistribute that binary in the source tree and does not represent its C/C++ H3 +implementation as Camelid-native code. If a release later bundles the binary, its +license and copyright notice must travel with that distribution. + +- Project: +- License: MIT +- First Camelid-checked H3 revision: `c6beeef35526c6dc94b74a7fb69f9d2e6a2a7a12` + +MiniMax H3 model weights are separately governed by the +[MiniMax-H3 Community License Agreement](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/LICENSE), +not by stable-diffusion.cpp's or Camelid's MIT license. Camelid requires an explicit +acknowledgement before its video pull command downloads those artifacts. + ## Maintenance note Keep this file in sync with any third-party source, binary, fixture, or reference tooling Camelid redistributes or materially depends on for public evidence. Documentation polish, branding cleanup, or repository renaming work must not remove these credits while the underlying technical reliance still exists. diff --git a/camelid-desktop/Info.plist b/camelid-desktop/Info.plist new file mode 100644 index 000000000..9b00ae53c --- /dev/null +++ b/camelid-desktop/Info.plist @@ -0,0 +1,8 @@ + + + + + NSRemovableVolumesUsageDescription + Camelid reads MiniMax-H3 model bundles and writes generated videos on your selected external drive. + + diff --git a/camelid-desktop/src/main.rs b/camelid-desktop/src/main.rs index 1932a5916..ccf295540 100644 --- a/camelid-desktop/src/main.rs +++ b/camelid-desktop/src/main.rs @@ -230,6 +230,25 @@ async fn choose_models_directory( })) } +/// Pick an H3 bundle without changing Camelid's text-model storage preference. +/// On macOS, selecting the external folder also gives the running app the +/// user-mediated removable-volume access required by the video sidecar. +#[tauri::command] +async fn choose_video_models_directory(app: tauri::AppHandle) -> Result, String> { + let Some(selected) = app + .dialog() + .file() + .set_title("Choose the MiniMax-H3 model bundle") + .blocking_pick_folder() + else { + return Ok(None); + }; + let selected = selected + .into_path() + .map_err(|err| format!("the selected folder is not a local filesystem path: {err}"))?; + Ok(Some(selected.to_string_lossy().into_owned())) +} + // Async for the same main-thread reason as choose_models_directory: this only // does small preference-file IO, but it has no business on the UI thread. #[tauri::command] @@ -274,6 +293,7 @@ fn main() { startup_snapshot, retry_startup, choose_models_directory, + choose_video_models_directory, reset_models_directory ]) .setup(|app| { diff --git a/camelid-desktop/tauri.conf.json b/camelid-desktop/tauri.conf.json index f60f6b494..e1681508b 100644 --- a/camelid-desktop/tauri.conf.json +++ b/camelid-desktop/tauri.conf.json @@ -18,9 +18,7 @@ "minHeight": 560, "center": true, "backgroundColor": "#0e1216", - "url": "index.html", - "titleBarStyle": "Overlay", - "hiddenTitle": true + "url": "index.html" } ], "security": { diff --git a/docs/runtime/minimax-h3-video.md b/docs/runtime/minimax-h3-video.md new file mode 100644 index 000000000..133e1a0e9 --- /dev/null +++ b/docs/runtime/minimax-h3-video.md @@ -0,0 +1,176 @@ +# MiniMax H3 local video (experimental) + +Camelid can generate MiniMax H3 video through a capability-checked +[`stable-diffusion.cpp`](https://github.com/leejet/stable-diffusion.cpp) `sd-cli` +backend. This is an experimental backend bridge, not a claim that Camelid's +autoregressive Rust engine natively executes the H3 diffusion graph. + +## What the lane supports + +- `fl2va`: text-to-video, first-frame image-to-video, and first/last-frame video. +- `ref2va`: image, video-frame-directory, and WAV reference conditioning. +- Native 24 fps WebM or AVI output; WebM is recommended for H3's generated stereo audio. +- A pinned, SHA-256-verified starter bundle for either checkpoint. +- A dry-run plan and doctor command that do not load the model. + +The **Video Studio** page uses a loopback-only asynchronous HTTP job queue so a +long render never blocks Camelid's text decode worker. Chat model loading stays +separate: H3's GGUFs are diffusion/text-encoder components, not chat-completion +models. + +## Requirements + +The default `fl2va` bundle is 26,978,253,672 bytes (about 25.13 GiB): + +| Component | File | Bytes | +| --- | --- | ---: | +| Diffusion transformer | `minimax_h3_fl2va_pruned-UD-Q2_K_XL.gguf` | 8,063,029,344 | +| H3 text encoder | `qwen3vl_32b_minimax_h3-Q2_K_M.gguf` | 13,102,161,024 | +| Video VAE | `minimax_h3_video_vae_fp16.safetensors` | 5,207,808,496 | +| Audio VAE | `minimax_h3_audio_vae_fp32.safetensors` | 605,254,808 | + +The `ref2va` starter bundle is about 23.84 GiB because its Q2_K diffusion +transformer is smaller. Runtime memory depends heavily on resolution, frame count, +backend, and offload. Camelid defaults the text encoder to CPU and enables CPU +offload; this lowers the GPU-memory requirement but needs substantial system RAM +and fast local storage. + +MiniMax H3 is governed by the +[MiniMax-H3 Community License Agreement](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/LICENSE), +including its applicable-territory terms. Camelid requires an explicit license +acknowledgement before downloading and does not decide whether the license applies +to a particular user or location. + +## Install an H3-capable backend + +Use a current `sd-cli` build from `stable-diffusion.cpp`. The bridge was checked +against source revision `c6beeef35526c6dc94b74a7fb69f9d2e6a2a7a12` and performs a +runtime capability check for the required CLI flags. Put `sd-cli` beside the +Camelid executable, on `PATH`, in `CAMELID_SD_CLI`, in +`/Volumes//Camelid/bin/sd-cli` on macOS, or pass `--sd-cli `. + +Follow the upstream +[build instructions](https://github.com/leejet/stable-diffusion.cpp/blob/master/docs/build.md) for +the desired Metal, CUDA, Vulkan, or CPU backend. Camelid does not download or +execute build scripts for this third-party binary. + +## Download and verify the model bundle + +Read the H3 license first, then download the text/first/last-frame bundle: + +```bash +camelid video pull --variant fl2va --accept-license +camelid video doctor --variant fl2va --verify-sha256 +``` + +Use `--no-audio` with both commands for a silent-video bundle. To use reference +media instead: + +```bash +camelid video pull --variant ref2va --accept-license +camelid video doctor --variant ref2va --verify-sha256 +``` + +Downloads are resumable. Camelid writes `.part` files, checks exact byte counts +and pinned SHA-256 digests, and only then promotes each artifact to its final name. + +On macOS, Video Studio automatically discovers a bundle at +`/Volumes//Camelid/models/minimax-h3`. Its readiness card reports partial +download bytes while `.part` files are still being filled and writes completed +clips to the sibling `Camelid/outputs` folder. In the desktop app, use **Choose** +beside the bundle path the first time macOS asks Camelid to access a removable +volume; selecting the `minimax-h3` folder grants that access without changing the +separate text-model storage preference. + +Desktop release builders can bundle the small backend executable inside the +signed app while leaving the model files external: + +```bash +CAMELID_DESKTOP_SD_CLI=/path/to/sd-cli ./scripts/build-macos-desktop.sh +``` + +## Use Video Studio + +Start the local server and open **Video Studio** from the Workspace section of +the sidebar. The page provides: + +- FL2VA text-to-video and optional first/last-frame paths. +- REF2VA image, frame-directory, and audio reference paths when its separate + diffusion model is installed. +- Resolution, frame, step, seed, and generated-audio controls. +- A serialized asynchronous job list with cancellation, backend logs, and + seekable in-browser WebM playback. + +The page polls bundle/backend readiness, so it can remain open while a resumable +`camelid video pull` runs in another terminal. + +## Local video API + +Video Studio uses these local endpoints: + +| Endpoint | Purpose | +| --- | --- | +| `GET /api/video/capabilities` | Bundle bytes, artifact/backend readiness, and discovered paths | +| `GET/POST /api/video/jobs` | List or queue jobs | +| `GET /api/video/jobs/:id` | Read job state | +| `POST /api/video/jobs/:id/cancel` | Cancel a queued/running job | +| `GET /api/video/jobs/:id/content` | Range-capable WebM playback | +| `GET /api/video/jobs/:id/log` | Backend log | + +These filesystem-bearing routes fail closed when Camelid is not listening on a +loopback address. They are a Camelid-local control surface, not an OpenAI +`/v1/videos` compatibility claim. + +## Generate a video + +Text-to-video with native stereo audio: + +```bash +camelid video generate \ + --prompt "a red panda stepping along a mossy log in a misty forest, cinematic" \ + --output red-panda.webm \ + --width 640 --height 384 --frames 25 --steps 4 --seed 11 +``` + +Image-to-video adds an initial image; add `--end-image` for first/last-frame +conditioning: + +```bash +camelid video generate \ + --prompt "the camera slowly circles the subject as snow begins to fall" \ + --init-image start.png \ + --end-image finish.png \ + --output snow.webm +``` + +Reference-conditioned generation uses the other checkpoint: + +```bash +camelid video generate \ + --variant ref2va \ + --prompt "Use as the main character in a cinematic tracking shot" \ + --reference-image subject.png \ + --output subject.webm +``` + +`--reference-image`, `--reference-video`, and `--reference-audio` may be repeated. +The upstream backend currently represents a reference video as a directory of +lexicographically sorted frames; reference audio must be a supported WAV file. + +Inspect the exact process invocation without running it: + +```bash +camelid video generate --prompt "test" --dry-run +``` + +H3 fixes fps at 24 and uses a `17k + 5` temporal frame grid. Camelid reports both +the requested and effective frame counts in its JSON plan. It pins `cfg-scale=1.0` +because H3 is distilled and classifier-free-guidance values above 1.0 are invalid. + +## Current boundary + +This lane is groundwork/experimental until a reproducible video-and-audio receipt +is captured on named hardware against the pinned artifacts and backend revision. +The UI and local job API are orchestration surfaces, not model-support evidence. +There is no claim yet for output parity, quality, throughput, portable memory fit, +other H3 quants, `/v1/videos` compatibility, or a Camelid-native H3 runtime. diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index cde0103c6..68ac05f9d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,7 +7,6 @@ import { Notice } from './components/ui/Notice' import { ConfirmDialog } from './components/ui/ConfirmDialog' import { isFirstRunHost } from './lib/firstRunActivation' import { formatPreview, formatSidebarDate } from './lib/formatters' -import { hasOverlayTitleBar } from './lib/desktopShell' import { useDashboardData } from './hooks/useDashboardData' import { useBackendLauncher } from './hooks/useBackendLauncher' import { useNotice } from './hooks/useNotice' @@ -32,9 +31,10 @@ const CompatibilityView = lazy(() => import('./views/CompatibilityView')) const TelemetryView = lazy(() => import('./views/TelemetryView')) const InferenceObservatoryView = lazy(() => import('./views/InferenceObservatoryView')) const WorkspaceView = lazy(() => import('./views/WorkspaceView')) +const VideoView = lazy(() => import('./views/VideoView')) const DEMO_UI = import.meta.env?.VITE_CAMELID_DEMO_UI === 'true' -const HASH_TABS = new Set(['chat', 'workspace', 'library', 'downloads', 'api', 'analytics', 'history', 'memory', 'system', 'settings', 'cluster', 'observatory', 'compatibility', 'telemetry']) +const HASH_TABS = new Set(['chat', 'workspace', 'video', 'library', 'downloads', 'api', 'analytics', 'history', 'memory', 'system', 'settings', 'cluster', 'observatory', 'compatibility', 'telemetry']) function App() { const { notice, noticeTone, showNotice, clearNotice } = useNotice() @@ -246,14 +246,10 @@ function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [loadDashboard]) - /* Above the loading early-return: every hook must run on every render, and - the shell this window lives in cannot change while it is open. */ - const [overlayTitleBar] = useState(hasOverlayTitleBar) - if (!dashboard) { return ( -
-
+
+
Loading Camelid…
@@ -266,17 +262,10 @@ function App() { sidebarCollapsed ? 'is-collapsed' : '', mobileNavOpen ? 'is-mobile-open' : '', DEMO_UI ? 'is-demo' : '', - overlayTitleBar ? 'has-overlay-chrome' : '', ].filter(Boolean).join(' ') return (
- {/* macOS desktop only: the window draws no title bar of its own, so the - traffic lights float over the top-left of our content. This strip is - the room they sit in and the surface the window is dragged by — - without it the app showed the OS bar stacked above our own header, - which is what makes an app look like a web page in a frame. */} - {overlayTitleBar &&
} {!DEMO_UI && ( )} + {tab === 'video' && ( + + )} + {tab === 'analytics' && ( )} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx index b975bd672..f277e89ca 100644 --- a/frontend/src/components/CommandPalette.jsx +++ b/frontend/src/components/CommandPalette.jsx @@ -9,6 +9,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' const VIEW_LABELS = [ ['chat', 'Chat'], ['workspace', 'Workspace'], + ['video', 'Video Studio'], ['library', 'Models'], ['history', 'Chat history'], ['analytics', 'Analytics'], diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 46da8401c..563cc2281 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -9,6 +9,7 @@ import { CamelidMark } from './ui/CamelidMark' const TITLES = { chat: 'Chat', workspace: 'Workspace', + video: 'Video Studio', library: 'Models', downloads: 'Downloaded models', api: 'API', diff --git a/frontend/src/components/layout/SidebarRail.jsx b/frontend/src/components/layout/SidebarRail.jsx index d7eeefb99..99d86cadb 100644 --- a/frontend/src/components/layout/SidebarRail.jsx +++ b/frontend/src/components/layout/SidebarRail.jsx @@ -6,7 +6,7 @@ import { Tooltip } from '../ui/Tooltip' import { ConversationListItem } from './ConversationListItem' import { IconAnalytics, IconApi, IconBolt, IconChart, IconChat, IconClose, IconHistory, IconMemory, IconModels, - IconDownload, IconNetwork, IconNewChat, IconObservatory, IconReceipt, IconSearch, IconSettings, IconSidebar, IconSystem, + IconDownload, IconNetwork, IconNewChat, IconObservatory, IconReceipt, IconSearch, IconSettings, IconSidebar, IconSystem, IconVideo, } from '../ui/icons' const NAV_SECTIONS = [ @@ -15,6 +15,7 @@ const NAV_SECTIONS = [ items: [ { tab: 'chat', label: 'Chat', Icon: IconChat }, { tab: 'workspace', label: 'Workspace', Icon: IconBolt }, + { tab: 'video', label: 'Video Studio', Icon: IconVideo }, { tab: 'history', label: 'Chat history', Icon: IconHistory }, { tab: 'memory', label: 'Memory', Icon: IconMemory }, ], diff --git a/frontend/src/components/ui/icons.jsx b/frontend/src/components/ui/icons.jsx index 9ff4cce94..c2149a89c 100644 --- a/frontend/src/components/ui/icons.jsx +++ b/frontend/src/components/ui/icons.jsx @@ -39,6 +39,7 @@ export const IconApi = (p) => export const IconMemory = (p) => export const IconImage = (p) => +export const IconVideo = (p) => export const IconSend = (p) => export const IconStop = (p) => export const IconCopy = (p) => diff --git a/frontend/src/lib/desktopShell.js b/frontend/src/lib/desktopShell.js index 483d87c85..a12ced3d3 100644 --- a/frontend/src/lib/desktopShell.js +++ b/frontend/src/lib/desktopShell.js @@ -1,25 +1,5 @@ -/* Desktop-shell detection, shared so the browser build never pays for it. - - Two different questions get asked about the desktop app and they are not the - same: whether we are inside Tauri at all (BackendBanner asks this, to know a - page cannot restart its own engine), and whether this window draws its own - title bar. Only macOS uses the overlay style, where the traffic lights float - over our content instead of sitting in a strip the OS owns — so only macOS - needs the window to reserve room for them and provide a drag region. */ +/* Desktop-shell detection, shared so the browser build never pays for it. */ export function isDesktopShell() { if (typeof window === 'undefined') return false return Boolean(window.__TAURI__?.core?.invoke) } - -export function isMacPlatform() { - if (typeof navigator === 'undefined') return false - const platform = navigator.userAgentData?.platform || navigator.platform || '' - return /mac/i.test(platform) -} - -/* True only where the window chrome is ours to draw. Keep this the single - source: styling that assumes the traffic lights are overlapping content must - never switch on in the browser build, where there are none. */ -export function hasOverlayTitleBar() { - return isDesktopShell() && isMacPlatform() -} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 118b4ce2d..f16db7d95 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -12,3 +12,4 @@ @import './styles/views.css'; @import './styles/cluster.css'; @import './styles/observatory.css'; +@import './styles/video.css'; diff --git a/frontend/src/styles/shell.css b/frontend/src/styles/shell.css index 8d269f539..5f01b9265 100644 --- a/frontend/src/styles/shell.css +++ b/frontend/src/styles/shell.css @@ -13,23 +13,6 @@ .camelid-app.is-collapsed { grid-template-columns: var(--rail-collapsed-width) minmax(0, 1fr); } .camelid-app.is-demo { grid-template-columns: minmax(0, 1fr); } -/* ---- macOS desktop window chrome ---- - With the overlay title bar the OS draws no bar of its own, so the traffic - lights land on top of whatever occupies the window's top-left — the sidebar - brand row. The shell gains one title-bar's worth of room at the top and a - transparent strip across it that the window can be dragged by. Only children - carrying data-tauri-drag-region drag, so every control below stays clickable. - Applied nowhere but the macOS desktop shell; the browser build never sees it. */ -.camelid-app.has-overlay-chrome { padding-top: var(--overlay-titlebar-height); } -.app-drag-strip { - position: fixed; - inset: 0 0 auto 0; - height: var(--overlay-titlebar-height); - z-index: 60; - /* Same ground as the shell so the strip reads as window chrome, not a gap. */ - background: var(--color-canvas); -} - /* ---- Main column ---- */ .camelid-main { display: flex; diff --git a/frontend/src/styles/video.css b/frontend/src/styles/video.css new file mode 100644 index 000000000..49b3b731f --- /dev/null +++ b/frontend/src/styles/video.css @@ -0,0 +1,126 @@ +.video-view { gap: var(--space-6); } + +.video-head { align-items: flex-start; } +.video-head .cxv-head__actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; justify-content: flex-end; } + +.video-alert { + padding: var(--space-3) var(--space-4); + border: 1px solid color-mix(in srgb, var(--color-danger) 45%, var(--color-border-soft)); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--color-danger) 8%, var(--color-bg-elevated)); + color: var(--color-danger); + font-size: var(--text-sm); +} + +.video-readiness, +.video-compose, +.video-preview, +.video-jobs { padding: var(--space-6); } + +.video-readiness { + display: flex; + flex-direction: column; + gap: var(--space-4); + background: + radial-gradient(90% 140% at 0% 0%, var(--color-hero-glow), transparent 58%), + var(--color-bg-elevated); +} + +.video-readiness__summary { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-5); } +.video-readiness__summary > div:first-child { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; } +.video-readiness__summary strong { font-size: var(--text-xl); letter-spacing: -0.02em; } +.video-readiness__summary small { color: var(--color-text-muted); } +.video-readiness__flags { display: flex; gap: var(--space-4); flex-wrap: wrap; } + +.video-eyebrow { + color: var(--color-accent-text); + font-size: var(--text-2xs); + font-weight: 750; + letter-spacing: var(--tracking-label); + text-transform: uppercase; +} + +.video-progress { height: 7px; overflow: hidden; border-radius: 999px; background: var(--color-surface); border: 1px solid var(--color-border-soft); } +.video-progress span { display: block; height: 100%; min-width: 0; border-radius: inherit; background: linear-gradient(90deg, var(--color-accent), var(--color-accent-hover)); transition: width 500ms var(--ease-standard); } + +.video-paths { display: grid; grid-template-columns: minmax(280px, 1.4fr) repeat(2, minmax(220px, 1fr)); gap: var(--space-3); align-items: end; } +.video-paths > div { display: flex; min-width: 0; flex-direction: column; gap: var(--space-2); } +.video-paths > div > span { color: var(--color-text-muted); font-size: var(--text-sm); font-weight: 600; } +.video-paths code { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 11px var(--space-3); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md); background: var(--color-surface); color: var(--color-text-muted); } +.video-path-picker { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); } +.video-readiness__note { margin: 0; color: var(--color-text-muted); font-size: var(--text-xs); overflow-wrap: anywhere; } + +.video-workbench { display: grid; grid-template-columns: minmax(0, 1.12fr) minmax(340px, .88fr); gap: var(--space-5); align-items: start; } +.video-compose { display: flex; flex-direction: column; gap: var(--space-5); } +.video-compose .cxv-section__head h2, +.video-preview .cxv-section__head h2, +.video-jobs .cxv-section__head h2 { margin-top: var(--space-1); } +.video-prompt textarea { min-height: 128px; } + +.video-mode-tabs { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); padding: 4px; border: 1px solid var(--color-border-soft); border-radius: var(--radius-lg); background: var(--color-surface); } +.video-mode-tabs button { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: var(--space-3) var(--space-4); border: 1px solid transparent; border-radius: calc(var(--radius-lg) - 4px); background: transparent; color: var(--color-text-muted); text-align: left; cursor: pointer; } +.video-mode-tabs button strong { color: inherit; font-size: var(--text-sm); } +.video-mode-tabs button span { color: var(--color-text-faint); font-size: var(--text-xs); } +.video-mode-tabs button:hover { color: var(--color-text); background: var(--color-bg-hover); } +.video-mode-tabs button.is-active { border-color: color-mix(in srgb, var(--color-accent) 34%, var(--color-border-soft)); background: var(--color-accent-soft); color: var(--color-accent-text); box-shadow: var(--shadow-1); } + +.video-grid { display: grid; gap: var(--space-3); } +.video-grid--two { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.video-grid--settings { grid-template-columns: repeat(3, minmax(120px, 1fr)); padding: var(--space-4); border: 1px solid var(--color-border-soft); border-radius: var(--radius-lg); background: var(--color-surface); } +.video-grid em { font-weight: 400; color: var(--color-text-faint); } +.video-reference-fields { display: grid; gap: var(--space-3); } +.video-reference-fields textarea { min-height: 64px; } + +.video-advanced-toggle { align-self: flex-start; padding: 0; border: 0; background: none; color: var(--color-accent-text); font-size: var(--text-sm); font-weight: 650; cursor: pointer; } +.video-advanced-toggle:hover { text-decoration: underline; } +.video-check { display: flex; align-items: center; gap: var(--space-3); min-height: 64px; padding: var(--space-3); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md); background: var(--color-bg-elevated); cursor: pointer; } +.video-check input { width: 17px; height: 17px; accent-color: var(--color-accent); } +.video-check span { display: flex; flex-direction: column; gap: 2px; } +.video-check strong { font-size: var(--text-sm); } +.video-check small { color: var(--color-text-faint); font-size: var(--text-xs); } + +.video-compose__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding-top: var(--space-2); border-top: 1px solid var(--color-border-soft); } +.video-compose__foot > div { display: flex; flex-direction: column; gap: 2px; color: var(--color-text-faint); } + +.video-preview { position: sticky; top: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); } +.video-preview video { display: block; width: 100%; aspect-ratio: 5 / 3; border-radius: var(--radius-lg); background: #080a0e; box-shadow: inset 0 0 0 1px rgb(255 255 255 / 7%); } +.video-preview__empty { display: flex; min-height: 330px; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3); padding: var(--space-7); border: 1px dashed var(--color-border-strong); border-radius: var(--radius-lg); background: linear-gradient(145deg, var(--color-surface), var(--color-bg-elevated)); color: var(--color-text-faint); text-align: center; } +.video-preview__empty strong { color: var(--color-text); } +.video-preview__empty p { max-width: 38ch; margin: 0; color: var(--color-text-muted); font-size: var(--text-sm); line-height: var(--leading-normal); overflow-wrap: anywhere; } +.video-preview__meta { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; color: var(--color-text-faint); font-size: var(--text-xs); } +.video-preview__meta > span { padding: 3px var(--space-2); border: 1px solid var(--color-border-soft); border-radius: 999px; background: var(--color-surface); } +.video-preview__meta a { display: inline-flex; align-items: center; gap: 3px; margin-left: auto; color: var(--color-accent-text); text-decoration: none; } + +.video-jobs { display: flex; flex-direction: column; gap: var(--space-4); } +.video-job-list { display: flex; flex-direction: column; gap: var(--space-2); } +.video-job { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: var(--space-3); padding: var(--space-2); border: 1px solid var(--color-border-soft); border-radius: var(--radius-md); background: var(--color-surface); } +.video-job.is-active { border-color: color-mix(in srgb, var(--color-accent) 42%, var(--color-border-soft)); background: var(--color-accent-soft); } +.video-job__select { display: grid; min-width: 0; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-1); border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; } +.video-job__copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; } +.video-job__copy strong { overflow: hidden; color: var(--color-text); font-size: var(--text-sm); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } +.video-job__copy small { overflow: hidden; color: var(--color-text-faint); font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; } +.video-job__status { color: var(--color-text-muted); font-size: var(--text-xs); text-transform: capitalize; } + +.video-license { margin: 0; color: var(--color-text-faint); font-size: var(--text-xs); text-align: center; } +.video-license a { color: var(--color-accent-text); } + +@media (max-width: 1080px) { + .video-workbench { grid-template-columns: 1fr; } + .video-preview { position: static; } + .video-paths { grid-template-columns: 1fr; } +} + +@media (max-width: 720px) { + .video-readiness, + .video-compose, + .video-preview, + .video-jobs { padding: var(--space-4); } + .video-readiness__summary, + .video-compose__foot { align-items: stretch; flex-direction: column; } + .video-mode-tabs, + .video-grid--two, + .video-grid--settings { grid-template-columns: 1fr; } + .video-job { grid-template-columns: minmax(0, 1fr); } + .video-job__status { padding-left: calc(20px + var(--space-3)); } + .video-job > .cx-btn { justify-self: start; margin-left: calc(20px + var(--space-3)); } +} diff --git a/frontend/src/views/VideoView.jsx b/frontend/src/views/VideoView.jsx new file mode 100644 index 000000000..cb3d25282 --- /dev/null +++ b/frontend/src/views/VideoView.jsx @@ -0,0 +1,335 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { Button } from '../components/ui/Button' +import { StatusDot } from '../components/ui/StatusDot' +import { + IconExternal, + IconPlay, + IconRefresh, + IconStop, + IconVideo, +} from '../components/ui/icons' +import { formatBytes } from '../lib/formatters' + +function endpoint(apiBase, path) { + return `${String(apiBase || '').replace(/\/$/, '')}${path}` +} + +async function readJson(response, fallback) { + const payload = await response.json().catch(() => null) + if (!response.ok) throw new Error(payload?.error?.message || fallback || `Request failed (${response.status})`) + return payload +} + +function paths(value) { + return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean) +} + +function timestamp(value) { + if (!value) return 'just now' + return new Date(value * 1000).toLocaleString() +} + +function statusTone(status) { + if (status === 'succeeded') return 'ready' + if (status === 'failed') return 'error' + if (status === 'running' || status === 'queued') return 'warn' + return 'neutral' +} + +export default function VideoView({ apiBase = '', showNotice }) { + const [capabilities, setCapabilities] = useState(null) + const [jobs, setJobs] = useState([]) + const [selectedJobId, setSelectedJobId] = useState('') + const [modelsDir, setModelsDir] = useState('') + const [loading, setLoading] = useState(true) + const [choosingBundle, setChoosingBundle] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState('') + const [advanced, setAdvanced] = useState(false) + const [form, setForm] = useState({ + prompt: '', + variant: 'fl2va', + width: 640, + height: 384, + frames: 25, + steps: 4, + seed: 11, + includeAudio: true, + initImage: '', + endImage: '', + referenceImages: '', + referenceVideos: '', + referenceAudios: '', + }) + + const refresh = useCallback(async ({ quiet = false } = {}) => { + if (!quiet) setLoading(true) + try { + const query = new URLSearchParams({ variant: form.variant, include_audio: String(form.includeAudio) }) + if (modelsDir.trim()) query.set('models_dir', modelsDir.trim()) + const [caps, list] = await Promise.all([ + fetch(endpoint(apiBase, `/api/video/capabilities?${query}`)).then((response) => readJson(response, 'Video readiness is unavailable.')), + fetch(endpoint(apiBase, '/api/video/jobs')).then((response) => readJson(response, 'Video jobs are unavailable.')), + ]) + const nextJobs = list?.data || [] + setCapabilities(caps) + setJobs(nextJobs) + setSelectedJobId((current) => { + if (current && nextJobs.some((job) => job.id === current)) return current + return nextJobs.find((job) => job.status === 'succeeded')?.id || nextJobs[0]?.id || '' + }) + setError('') + } catch (err) { + setError(String(err?.message || err)) + } finally { + if (!quiet) setLoading(false) + } + }, [apiBase, form.includeAudio, form.variant, modelsDir]) + + useEffect(() => { + refresh() + }, [refresh]) + + useEffect(() => { + const timer = window.setInterval(() => refresh({ quiet: true }), 4000) + return () => window.clearInterval(timer) + }, [refresh]) + + const selectedJob = jobs.find((job) => job.id === selectedJobId) || null + const percent = capabilities?.expected_bytes + ? Math.min(100, (Number(capabilities.downloaded_bytes || 0) / Number(capabilities.expected_bytes)) * 100) + : 0 + const verifyingArtifact = capabilities?.artifacts?.find((artifact) => artifact.stage === 'verifying') + const effectiveFrames = form.frames <= 5 ? 5 : Math.ceil((form.frames - 5) / 17) * 17 + 5 + const ref2va = form.variant === 'ref2va' + const refsReady = !ref2va || Boolean(paths(form.referenceImages).length || paths(form.referenceVideos).length || paths(form.referenceAudios).length) + const ready = Boolean(capabilities?.artifacts_ready && capabilities?.backend_ready) + const canSubmit = ready && form.prompt.trim() && refsReady && !submitting + const activeJobs = jobs.filter((job) => job.status === 'queued' || job.status === 'running') + + const update = (field, value) => setForm((current) => ({ ...current, [field]: value })) + + const chooseBundle = async () => { + const invoke = window.__TAURI__?.core?.invoke + if (!invoke) return + setChoosingBundle(true) + setError('') + try { + const selected = await invoke('choose_video_models_directory') + if (selected) setModelsDir(selected) + } catch (err) { + setError(String(err?.message || err)) + } finally { + setChoosingBundle(false) + } + } + + const generate = async (event) => { + event.preventDefault() + if (!canSubmit) return + setSubmitting(true) + setError('') + try { + const payload = { + prompt: form.prompt.trim(), + variant: form.variant, + models_dir: modelsDir.trim() || capabilities?.models_dir, + width: Number(form.width), + height: Number(form.height), + frames: Number(form.frames), + steps: Number(form.steps), + seed: Number(form.seed), + include_audio: form.includeAudio, + init_image: form.initImage.trim() || null, + end_image: form.endImage.trim() || null, + reference_images: paths(form.referenceImages), + reference_videos: paths(form.referenceVideos), + reference_audios: paths(form.referenceAudios), + offload_to_cpu: true, + } + const response = await fetch(endpoint(apiBase, '/api/video/jobs'), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload), + }) + const job = await readJson(response, 'Video generation could not start.') + setJobs((current) => [job, ...current.filter((item) => item.id !== job.id)]) + setSelectedJobId(job.id) + showNotice?.('MiniMax-H3 video job queued.', 'ready') + } catch (err) { + const message = String(err?.message || err) + setError(message) + showNotice?.(message, 'error') + } finally { + setSubmitting(false) + } + } + + const cancel = async (id) => { + try { + const response = await fetch(endpoint(apiBase, `/api/video/jobs/${id}/cancel`), { method: 'POST' }) + const job = await readJson(response, 'Video job could not be canceled.') + setJobs((current) => current.map((item) => item.id === id ? job : item)) + } catch (err) { + setError(String(err?.message || err)) + } + } + + const readinessLabel = ready + ? 'Ready to generate' + : verifyingArtifact + ? `Verifying ${String(verifyingArtifact.role || 'model').replaceAll('_', ' ')}` + : capabilities?.downloaded_bytes > 0 && !capabilities?.artifacts_ready + ? `Downloading bundle · ${percent.toFixed(1)}%` + : 'Setup incomplete' + + return ( +
+
+
+

Experimental creation lane

+

Video Studio

+

Create local MiniMax-H3 video with the pinned 25 GiB FL2VA bundle and a capability-checked local backend.

+
+
+ 0 ? 'warn' : 'offline'} pulse={ready || activeJobs.length > 0} label={readinessLabel} /> + +
+
+ + {error &&
{error}
} + +
+
+
+ MiniMax-H3 bundle + {readinessLabel} + {formatBytes(capabilities?.downloaded_bytes || 0)} of {formatBytes(capabilities?.expected_bytes || 0)} on external storage +
+
+ + +
+
+
+ +
+
+ +
Backend{capabilities?.backend || 'Checking sd-cli…'}
+
Finished videos{capabilities?.output_dir || 'Checking output folder…'}
+
+ {!capabilities?.backend_ready && capabilities?.backend_error &&

{capabilities.backend_error}

} +
+ +
+
+
+
New generation

Direct the clip

+ 24 FPS · WebM +
+ +