diff --git a/.agents/STATUS.md b/.agents/STATUS.md deleted file mode 100644 index 1583abba56..0000000000 --- a/.agents/STATUS.md +++ /dev/null @@ -1,94 +0,0 @@ -# Agent Infrastructure — Status Dashboard - -Developer-maintained overview of all agent components and their maturity. -Use this to understand what exists, how complete it is, and how much to trust it. - -_Last synced: 2026-03-02_ - -> To resync this dashboard, use the workflow: `.agents/workflows/sync-dashboard.md` - ---- - -## Summary - -| Category | Total | ✅ Ready | 🟡 Draft | 🔴 Stub | Trust | -|----------|-------|---------|---------|---------|-------| -| Skills | 8 | 0 | 8 | 0 | Low — newly created, untested | -| Workflows (SOPs) | 4 | 0 | 4 | 0 | Low — newly created, untested | -| Memory files | 4 | 1 | 3 | 0 | Medium — codebase_map is solid | -| Lessons | 0 | — | — | — | N/A — empty | -| Exploration logs | 0 | — | — | — | N/A — empty | - ---- - -## Skills (`.agents/skills/`) - -| Skill | File | Status | Trust | Tested | Notes | -|-------|------|--------|-------|--------|-------| -| Launch Experiment | `launch-experiment.md` | 🟡 Draft | Low | ❌ | Needs dry-run validation | -| Monitor Experiment | `monitor-experiment.md` | 🟡 Draft | Low | ❌ | Requires W&B API access to test | -| Summarize Run | `summarize-run.md` | 🟡 Draft | Low | ❌ | Pattern from existing test infra | -| Log Experiment | `log-experiment.md` | 🟡 Draft | Low | ❌ | Journal formatting only | -| Evaluate Video Quality | `evaluate-video-quality.md` | 🟡 Draft | Low | ❌ | SSIM section most mature | -| Index Related Work | `index-related-work.md` | 🟡 Draft | Low | ❌ | Schema defined, no entries yet | -| Search Related Work | `search-related-work.md` | 🟡 Draft | Low | ❌ | Depends on indexed entries | -| Skill Template | `SKILL_TEMPLATE.md` | ✅ Ready | High | ✅ | Meta-template, stable | - -### Trust Level Definitions -- **High**: Tested in production, validated against real experiments -- **Medium**: Logic is sound, partially tested or based on existing patterns -- **Low**: Newly written, not yet validated -- **None**: Placeholder only - ---- - -## Workflows / SOPs (`.agents/workflows/`) - -| Workflow | File | Status | Trust | Tested | Notes | -|----------|------|--------|-------|--------|-------| -| Experiment Lifecycle | `experiment-lifecycle.md` | 🟡 Draft | Low | ❌ | End-to-end flow, untested | -| Evaluation Development | `evaluation-development.md` | 🟡 Draft | Low | ❌ | Metric dev process | -| Experiment Journaling | `experiment-journaling.md` | 🟡 Draft | Low | ❌ | Journaling cadence | -| Lesson Capture | `lesson-capture.md` | 🟡 Draft | Low | ❌ | Post-experiment reflection | -| Sync Dashboard | `sync-dashboard.md` | 🟡 Draft | Low | ❌ | This dashboard's updater | - ---- - -## Memory (`.agents/memory/`) - -| File | Status | Trust | Notes | -|------|--------|-------|-------| -| `codebase_map.md` | ✅ Ready | High | Synthesized from full repo research | -| `experiment_journal.md` | 🟡 Draft | Medium | Schema defined, no entries yet | -| `evaluation_registry.md` | 🟡 Draft | Medium | SSIM/loss metrics documented | -| `related_work/README.md` | 🟡 Draft | Medium | Schema defined, no entries yet | - ---- - -## Lessons (`.agents/lessons/`) - -| File | Category | Severity | Notes | -|------|----------|----------|-------| - -_No lessons captured yet._ - ---- - -## Exploration Logs (`.agents/exploration/`) - -| File | Status | Topic | Notes | -|------|--------|-------|-------| - -_No exploration logs yet._ - ---- - -## What to Do Next - -1. **Validate skills**: Run a minimal training experiment using the - `experiment-lifecycle` SOP to test `launch-experiment` → `monitor-experiment` - → `summarize-run` end-to-end. -2. **Index first related work**: Use `index-related-work` to add at least one - paper (e.g., the Self-Forcing paper used in the codebase). -3. **Capture first lesson**: After the validation run, capture any findings. -4. **Promote to Ready**: As each skill/SOP is tested, update its status here. diff --git a/.agents/memory/dreamverse-integration/README.md b/.agents/memory/dreamverse-integration/README.md new file mode 100644 index 0000000000..489230c156 --- /dev/null +++ b/.agents/memory/dreamverse-integration/README.md @@ -0,0 +1,201 @@ +# Dreamverse Integration — Memory Index + +Living knowledge base for the FastVideo ↔ Dreamverse ↔ Dynamo integration. +Tracks the public API refactor (PRs 0-17), the LTX-2 streaming server +upstream, the Dreamverse switch from `FastVideo-internal` to public +`FastVideo`, and the NVFP4 quantization landing. + +**Last reconciled:** 2026-05-06 (**D-26** EXECUTED — rebased +`will/dreamverse-monorepo` directly onto `origin/main` (`c17d33bf`) +via `git rebase origin/main`; 66 commits cleanly replayed; force- +pushed via `--force-with-lease`. New tip `83829c5e`. Local backup +branch `will/dreamverse-monorepo-pre-main-rebase-backup-20260506` +preserved at the pre-rebase tip `2ee839a3`. PR #1288 on +`will/ltx2_sr_port` is untouched. The branch is now ready to open +as a single PR against main. + +**Earlier — D-21** EXECUTED — chunk-stutter root cause +analysis + NVENC build path + opt-in `--nvenc` flag + benchmark regression +test. 3 parallel explore agents confirmed apps/dreamverse matches +FastVideo-internal byte-for-byte on NVFP4 + torch.compile coverage; stutter +is NOT a regression. Software libx264 encoding consumes ~22% of segment +wall-time. Built ffmpeg with NVENC support; B200 silicon doesn't have NVENC +encoder hardware so the path is currently moot on this dev host but works +on RTX 50-series / T4 / A10 deploys. Benchmark captured libx264 ultrafast +at 611ms median (8.25x realtime in isolation). Open follow-ups D-22/D-23/D-24. + +**Earlier — D-20** EXECUTED — segment-2 BrokenPipe +root cause was a TWO-direction silent drop of LTX-2 audio kwargs in +public `VideoGenerator` (inbound `SamplingParam.update()` rejected +`audio_num_frames`/`ltx2_audio_clean_latent`/etc. as unknown fields and +`logger.error`'d, outbound result dict didn't surface +`ltx2_audio_latents` from `output_batch.extra`). Ported the +FastVideo-internal extra-overrides routing block + made `update()` +strict + added regression test (7 tests, all pass) + landed 4 commits +on `will/dreamverse-monorepo` @ `5eaf0a13` (11 commits ahead of +`fbd823df`). End-to-end verified on GPU4: `Cached audio latents shape +=(1, 8, 126, 16) for segment 2`, `Segment 2: relayed av chunks=22, +bytes=3.8MB`, no BrokenPipeError. Public-API fix needs cherry-pick to +`will/ltx2_sr_port` for PR #1288 — see open-threads.md item D-20-CP. +**D-19** EXECUTED previously: Dreamverse migration landed on +`will/dreamverse-monorepo` @ `c1fe5d4c` (5 commits ahead of +`will/ltx2_sr_port` HEAD `fbd823df`). 164 files, 53,294 LOC, 31,725 +files under `apps/dreamverse/`. e2e PASSES against migrated code (8/8 +Playwright in 5.1s, `/proc/$PID/cwd` verified). Significant deviation +from [integration-plan.md](integration-plan.md): the plan's "DELETE +generic-merged from Dreamverse, import public substitutes" assumption +was invalid (public APIs aren't drop-ins) — generic-merged files now +carried PRODUCT-LOCAL inside `apps/dreamverse/server/`. Public +`fastvideo.entrypoints.streaming.*` reverts to `fbd823df` state. See +[decisions-log.md D-19](decisions-log.md#d-19) + +[D-20](decisions-log.md#d-20) for full context.). +FastVideo `will/ltx2_sr_port` @ HEAD (post-D-17 STACK.md removal + +integration-review.md addition + integration-plan.md addition + D-18 +reconciliation). Dreamverse `will/integrate-public-fastvideo` @ `ec8ef92`. +PRs #1257 / #1258 / #1284 / #1286 MERGED to main. **PR #1287 CLOSED +(in favor of consolidation); PR #1288 OPEN as the single mega-PR +landing the entire `will/ltx2_sr_port` chain at once** (LTX-2 SR +runtime + NVFP4 + `generate_async`/Dynamo contract + agents memory dir). +Split branches kept as historical bookmarks; STACK.md model **abandoned** — +see [decisions-log.md D-17](decisions-log.md#d-17). Local backup +`will/ltx2_sr_port-pre-1286-rebase` @ `1baa60bb` preserves the +pre-rebase chain. + +## Fresh-context onboarding (read in order) + +If you're an agent picking up this work for the first time, do these +**5 things in this order**. Once done, you have full context to continue +any open thread, commit correctly, push, and propagate to the open PR. + +1. **Confirm worktree state** — run the "First 60 seconds" block in + [runbook.md](runbook.md). Tells you the branch is right, services + are up, and PR #1286's head matches what this dir claims. + +2. **Read [state.md](state.md)** — single-page snapshot of branch tips, + live services, test status, pre-existing failures, "do not pop" + stashes. + +3. **Read [pr-roadmap.md](pr-roadmap.md)** — what PRs landed, what's in + flight, what's planned. Identifies the active open PR (currently + #1286) and where it sits in the dependency chain. + +4. **Read [open-threads.md](open-threads.md)** — prioritized work items + with effort estimates and dependencies. The "Recommended pull order" + section is a ready-made TODO list if you need one. + +5. **Skim [runbook.md](runbook.md) end-to-end** — operational how-to: + verify, commit (with co-author trailers), push, propagate to PR + #1286, maintain the memory dir, and the "Common pitfalls" section + that catches the recurring traps. + +Skip the deep-context docs (design / streaming-server / cross-repo / +quantization / decisions-log) until you need them — they're indexed in +the "Deep-dive reading guide" below. + +Final check: run the "Self-test" block at the bottom of +[runbook.md](runbook.md). If you can answer all 8 questions from this +dir alone, you're ready. If you can't, the gap is a memory-dir bug — +file it in [open-threads.md](open-threads.md) before continuing. + +## Deep-dive reading guide + +| Question / task | File | +|---|---| +| "What's running right now? What just landed?" | [state.md](state.md) | +| "How do I commit / push / propagate to PR #1286?" | [runbook.md](runbook.md) | +| "Why is the schema typed this way? What's the philosophy?" | [design.md](design.md) | +| "What PRs landed? In flight? Planned?" | [pr-roadmap.md](pr-roadmap.md) | +| "Streaming server, `generate_async`, `build_app` routes?" | [streaming-server.md](streaming-server.md) | +| "How does Dreamverse use FastVideo? What about Dynamo?" | [cross-repo-surfaces.md](cross-repo-surfaces.md) | +| "NVFP4? Layer profiles? `LinearBase` fallback? AbsMaxFP8?" | [quantization.md](quantization.md) | +| "Why was decision X made? What's resolved vs. open?" | [decisions-log.md](decisions-log.md) | +| "What should I work on next? Priority order?" | [open-threads.md](open-threads.md) | +| "Who should be co-authored on commits in this scope?" | [authors.md](authors.md) | +| "How do we execute the Dreamverse → FastVideo monorepo merge?" | [integration-plan.md](integration-plan.md) ← **CURRENT** | +| "Historical drift audit + Option-D evaluation (deprecated by D-18)" | [integration-review.md](integration-review.md) (DEPRECATED) | + +## Repo + worktree paths + +| Repo | Path | Active branch | +|---|---|---| +| FastVideo (public) | `/home/william5lin/FastVideo` | `will/ltx2_sr_port` | +| Dreamverse | `/home/william5lin/Dreamverse` | `will/integrate-public-fastvideo` | +| FastVideo-internal (read-only ref) | `/home/william5lin/FastVideo-internal` | their `main` | +| Dynamo (read-only ref) | `/home/william5lin/dynamo` | upstream | + +## Glossary + +- **NVFP4**: NVIDIA's specific block-scaled FP4 (e2m1 mantissa, fp32 alpha, + `layout_128x4` scale layout, group size 16). Distinct from MX-FP4 / OCP-FP4. +- **`GeneratorConfig`**: typed init-time public config (model_path, engine, + pipeline). Replaces flat `from_pretrained(**kwargs)`. +- **`GenerationRequest`**: typed per-call request (prompt, inputs, sampling, + runtime, output, stage_overrides, state, plan, extensions). Replaces flat + `generate_video(**kwargs)`. +- **`ServeConfig`** / **`RunConfig`**: top-level YAML envelopes. ServeConfig + for `fastvideo serve`; RunConfig for offline `fastvideo generate`. +- **`InferencePreset`**: model-owned named preset (e.g. `ltx2_two_stage`) + defining stage topology + per-stage defaults + valid override types. +- **`ContinuationState`**: opaque round-trip state envelope `{kind, payload}`. + Hybrid: server-held for streaming WS, client-round-trip for stateless HTTP. +- **`generate_async`**: future canonical async exec API (PR 7.10) yielding + `VideoProgressEvent` / `VideoPartialEvent` / `VideoFinalEvent`. Substrate + for streaming server, OpenAI server, AND Dynamo backend. +- **`build_app`**: FastAPI app factory in + `fastvideo.entrypoints.streaming.server`. Currently exposes only + `/health` + `/v1/stream`. FE-required `/healthz`+`/readyz`+`/status` + migration is open follow-up #1. +- **`LLMProvider`**: protocol abstraction for prompt enhancer providers + (cerebras, cerebras_ifm, groq). Public schema currently restricts to + `Literal["cerebras", "groq"]`; `cerebras_ifm` is internal-only. +- **`compat.py`**: legacy kwargs translation layer (~370 lines). Scheduled + for death across PRs 14-17. +- **`prepare_for_compile`**: duck-type protocol method called via + `getattr(module, "prepare_for_compile", None)` before `torch.compile`. + Currently only Gemma3 implements it. +- **`SubprocessGpuPool`**: PR 7.6 public replacement for the internal + `realtime/local_runtime.GPUPool`. Per-GPU subprocess workers, typed + `GeneratorConfig` boundary. +- **PR 5.5**: streaming server subpackage skeleton — adds + `fastvideo/entrypoints/streaming/` parallel to `openai/`. +- **PR 7.10**: the unlock PR. Closes Q-5 (audio re-encode), Q-9 (Dynamo + progress), and PR 7.5's mid-segment cancellation TODO simultaneously. + +## Live process map (as of 2026-05-03) + +| Port | Service | Source | +|---|---|---| +| 8009 | `dreamverse-server` | running, `/readyz` 200, 1 warmed GPU worker | +| 5274 | `next-server` (dev) | running | +| 8000 | unknown FastAPI | not in handoff — verify before launching new BE | + +## How this directory is maintained + +- Source of truth for the integration story. Update when state changes. +- Each file has a "Last updated" header; bump when you edit. +- Cross-reference siblings via relative links; do NOT duplicate content. +- New entries: register in `../index.jsonl`. +- These files supersede the untracked source docs in the repo root and + `.agents/exploration/` — see [state.md](state.md) "Untracked but + present" section for disposition. + +## Source documents (archived 2026-05-03) + +The 7 source docs that this directory consolidates have been moved into +[`source-archive/`](source-archive/). They remain available for agents +who want the full unsynthesized rationale, but the synthesized memory +files in this dir are the canonical source of truth. + +| Source doc | Lines | Synthesized into | +|---|---|---| +| [`source-archive/apirefactor.md`](source-archive/apirefactor.md) | 838 | [design.md](design.md) | +| [`source-archive/PR-plan.md`](source-archive/PR-plan.md) | 1145 | [pr-roadmap.md](pr-roadmap.md) | +| [`source-archive/dreamverse_review.md`](source-archive/dreamverse_review.md) | 390 | [state.md](state.md) + [decisions-log.md](decisions-log.md) | +| [`source-archive/handoff-nvfp4-launch-demo.md`](source-archive/handoff-nvfp4-launch-demo.md) | 518 | [state.md](state.md) + [quantization.md](quantization.md) + [open-threads.md](open-threads.md) | +| [`source-archive/streaming-server-upstream-plan.md`](source-archive/streaming-server-upstream-plan.md) | 539 | [streaming-server.md](streaming-server.md) + [decisions-log.md](decisions-log.md) | +| [`source-archive/dreamverse_integration.md`](source-archive/dreamverse_integration.md) | 285 | [cross-repo-surfaces.md](cross-repo-surfaces.md) | +| [`source-archive/video-generator-config-api-design.md`](source-archive/video-generator-config-api-design.md) | 93 | [design.md](design.md) (early-draft material) | +| `.agents/exploration/pr-link-review.md` | 29 | already promoted to `.agents/skills/review-pr-link/` (kept in exploration dir) | + +See [`source-archive/README.md`](source-archive/README.md) for the +archive policy. diff --git a/.agents/memory/dreamverse-integration/authors.md b/.agents/memory/dreamverse-integration/authors.md new file mode 100644 index 0000000000..4c15d24fa1 --- /dev/null +++ b/.agents/memory/dreamverse-integration/authors.md @@ -0,0 +1,150 @@ +# Authors — Dreamverse Integration + +**Status:** PERMANENT — keep around as the source of truth for who collaborated +on the dreamverse-integration work, even after every PR in the integration +scope has merged. +**Last updated:** 2026-05-05 (strategy reversal — single mega-PR #1288 on `will/ltx2_sr_port` replaces planned 6-PR split; #1287 closed; per [decisions-log.md D-17](decisions-log.md#d-17)) + +This file documents the human co-authors credited on every commit in the +dreamverse-integration scope (FastVideo public-API refactor, streaming server +upstream, GPU pool, prompt enhancer, NVFP4 wire-up, LTX-2 SR port). The +4 collaborators below worked on the FastVideo-internal precursor of this code +and are credited as co-authors on every public-side upstream commit via Git's +standard +[`Co-authored-by`](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors) +trailer convention. + +Scope-wise this is the dreamverse-integration-flavored mirror of the +top-level [`CO-AUTHORS.md`](../../../CO-AUTHORS.md), which is scoped to the +broader `will/ltx2_sr_port` 10-PR stack. The roster is identical; this file +exists so the dreamverse-integration memory dir is self-contained and +discoverable without traversing to the repo root. + +## Co-author roster + +| GitHub user | Real name | GitHub ID | Trailer email | +|---|---|---|---| +| [`@Davids048`](https://github.com/Davids048) | Junda (David) Su | 90978028 | `90978028+Davids048@users.noreply.github.com` | +| [`@RandNMR73`](https://github.com/RandNMR73) | Matthew Noto | 99706358 | `99706358+RandNMR73@users.noreply.github.com` | +| [`@XOR-op`](https://github.com/XOR-op) | (unset) | 17672363 | `17672363+XOR-op@users.noreply.github.com` | +| [`@jzhang38`](https://github.com/jzhang38) | Zhang Peiyuan | 42993249 | `42993249+jzhang38@users.noreply.github.com` | + +## Verification — where these trailers appear + +Verified via `gh pr view --json commits --jq '.commits[].messageBody'` +across every PR in the integration scope: + +| PR | Branch | Status | Trailers present on every commit | +|---|---|---|---| +| #1257 | `will/api_7.6` (GPU pool upstream) | ✅ merged 2026-05-04 | yes (4/4) | +| #1258 | `will/api_7.7` (prompt enhancer + LLMProvider) | ✅ merged 2026-05-04 | yes (3/3) | +| #1284 | `will/api_7.8` (streaming auxiliaries) | ✅ merged 2026-05-04 | yes (2/2) | +| #1286 | `will/api_7.9` (streaming router) | ✅ merged 2026-05-05 at `2aaeee2a` (squash) | yes on commits 1-3; commit `a152cb77` (`[fix] streaming: router polish`) was missing trailers but got squashed into the merge commit, so the merge commit on main inherits the trailers from the other 3. The trailerless cherry-pick partner (`40e265b8` on `will/ltx2_sr_port`) was dropped by the post-#1286 rebase — gap permanently resolved. | +| #1287 | `will/api_7.10` (`generate_async` + `VideoEvent`) | ❌ CLOSED 2026-05-05 — superseded by #1288 per [D-17](decisions-log.md#d-17) | yes on all 3 commits (now part of #1288's chain) | +| **#1288** | **`will/ltx2_sr_port`** (mega-PR — full stack: SR runtime + NVFP4 + generate_async + Dynamo contract + agents memory + integration-review) | 🟢 OPEN, MERGEABLE at `b36bdbc9`, 36 commits / 70 files / ~+13.0k LOC (post STACK.md removal) | yes on all 36 commits | + +Aggregate count across `will/ltx2_sr_port` (top of stack) at the time of +writing: 32-33 commits per co-author, matching the 32 commits in the stack +on top of base `cfccd292`. Numbers stay consistent because the rebase +command (see "How the trailers were applied" below) walks every commit. + +## Trailer block (copy-paste ready) + +The trailers added to every commit on `will/ltx2_sr_port` and every +dreamverse-integration PR: + +``` +Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com> +Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com> +Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com> +Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com> +``` + +For one-off `git commit -m` invocations, use `--trailer` flags: + +```bash +git commit -m "..." \ + --trailer "Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com>" \ + --trailer "Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>" \ + --trailer "Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>" \ + --trailer "Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>" +``` + +`--trailer` is idempotent (dedupes by full `key: value`) so re-running is safe. + +## Why no-reply emails + +GitHub's `+@users.noreply.github.com` form is the most reliable +way to link a `Co-authored-by` trailer to a GitHub account. It: + +- Always works regardless of whether the user has a public verified email +- Survives the user changing their primary email +- Doesn't expose anyone's personal email to git history +- Is the format GitHub itself produces when you click "Add co-author" in the + web UI + +(All 4 collaborators have this email already used in `FastVideo-internal` +git history, verified via `git log --all` on that repo.) + +## How the trailers were applied (bulk rebase) + +```bash +git rebase --exec ' + git commit --amend --no-edit \ + --trailer "Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com>" \ + --trailer "Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>" \ + --trailer "Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>" \ + --trailer "Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>" +' origin/main will/ltx2_sr_port +``` + +After running, re-slice all 10 split branches per [`STACK.md`](../../../STACK.md) +and force-push the published branches (`will/api_7.9`, `will/ltx2_sr_port`). + +## How to add a new co-author later + +1. Add the user to the roster table above (and the top-level + [`CO-AUTHORS.md`](../../../CO-AUTHORS.md) — keep them in sync). +2. Append their `Co-authored-by` line to the trailer block above. +3. Re-run the bulk rebase command on `will/ltx2_sr_port` — git's trailer + dedupe handles the existing 4; the new one gets appended. +4. Re-slice all split branches per [`STACK.md`](../../../STACK.md). +5. Force-push the published branches. + +## What we do NOT add + +Per the repo's top-level [`AGENTS.md`](../../../AGENTS.md): + +> Never add any coding agent or models such as Claude (or Claude Code), GPT, +> Codex or others as a co-author in commits or PRs. Do not include +> `Co-Authored-By: Claude ...` trailers or "Generated with Claude Code" and +> other such lines. + +So no `Co-authored-by: Claude `, no +`Generated with Claude Code` footer, no `Cursor ` +trailer (one such commit exists on `will/ltx2_sr_port` from a pre-policy +external contribution and stays grandfathered; new commits MUST NOT introduce +the pattern). Only human collaborators. + +## Known gaps + +**Resolved 2026-05-05 by the post-#1286 rebase.** The two trailerless +commits (`a152cb77` on `will/api_7.9` and `40e265b8` on +`will/ltx2_sr_port`) are no longer reachable from any active branch: + +- `a152cb77` was absorbed into squash merge `2aaeee2a` on main, which + inherits the trailers from the other 3 commits in the squash. +- `40e265b8` was dropped by the post-#1286 rebase of + `will/ltx2_sr_port`. + +Both still exist on the local backup `will/ltx2_sr_port-pre-1286-rebase` +for archeological reference. No further action needed. + +## See also + +- [`../../../CO-AUTHORS.md`](../../../CO-AUTHORS.md) — top-level stack-scoped + co-authors file (same roster, broader scope) +- [`../../../STACK.md`](../../../STACK.md) — 10-PR split layout for + `will/ltx2_sr_port` (re-slice commands live here) +- [`pr-roadmap.md`](pr-roadmap.md) — per-PR status within the + dreamverse-integration scope diff --git a/.agents/memory/dreamverse-integration/cross-repo-surfaces.md b/.agents/memory/dreamverse-integration/cross-repo-surfaces.md new file mode 100644 index 0000000000..155b9a2654 --- /dev/null +++ b/.agents/memory/dreamverse-integration/cross-repo-surfaces.md @@ -0,0 +1,277 @@ +# Cross-Repo Surfaces — Dreamverse + Dynamo + +How Dreamverse consumes FastVideo today, what's already shared, what's +ad hoc, and what migrations land alongside each PR. Plus the Dynamo +backend contract. + +For the streaming-server side see [streaming-server.md](streaming-server.md). +For the API design see [design.md](design.md). For PR sequence see +[pr-roadmap.md](pr-roadmap.md). + +**Last updated:** 2026-05-03. + +## The three surfaces + +Dreamverse depends on FastVideo across three surfaces (in order of +stability): + +1. **Pipeline construction** (stable) +2. **Realtime runtime** (in flight: PRs 7.5/7.6) +3. **Continuation state** (PR 7 typed; PR 7.6 wires server-held) + +## Surface 1: Pipeline construction (stable) + +`Dreamverse/server/video_generation.py:VideoGenerationWorker` calls +`VideoGenerator.from_pretrained(...)`. + +After PR 6 the typed `GeneratorConfig` path exists; **as of `d80c2a8` +(May 2)** Dreamverse migrated to the typed path: + +| Dreamverse usage | FastVideo public surface (post-PR 6) | +|---|---| +| `VideoGenerator.from_pretrained(model_path, ltx2_refine_enabled=…, …)` | `VideoGenerator.from_pretrained(config=GeneratorConfig(...))` | +| Flat `torch_compile_kwargs={…}` dict | `engine.compile.{backend,fullgraph,mode,dynamic,extras}` | +| `ltx2_vae_tiling=True` | `pipeline.vae_tiling=True` | +| `ltx2_refine_*` family | `pipeline.preset_overrides.refine.*` + `pipeline.components.upsampler_weights` | +| `enable_torch_compile_text_encoder` | `engine.compile.text_encoder_enabled` | + +Refine knobs moved from `ltx2_refine_*` flat kwargs into +`preset_overrides["refine"]`. **The in-memory `pipeline_config` pin** +(`dit_config.quant_config = NVFP4Config()`) keeps using the legacy +`experimental["pipeline_config"]` carrier because typed +`transformer_quant: "NVFP4"` doesn't yet support setting +`layer_profile` (see [open-threads.md](open-threads.md) follow-up #4 + +[quantization.md](quantization.md)). + +Legacy flat-kwarg path stays supported via `compat.py`; migration is +opt-in. PR 13's deprecation warnings are the eventual nudge. + +## Surface 2: Realtime runtime (in flight: PRs 7.5–7.6) + +`Dreamverse/server/runtime/factory.py` selects a runtime backend at +process start: + +```python +def create_runtime_pool() -> RuntimePool: + if os.getenv("FASTVIDEO_REALTIME_BASE_URL"): + return FastVideoRealtimePool(base_url=..., ws_url=..., default_model_id=...) + return GPUPool(get_available_gpus()) # in-process, wraps + # fastvideo.entrypoints.realtime.local_runtime +``` + +Both backends speak the same `RuntimePool` / `RuntimeSlot` Protocol +(`server/runtime/interfaces.py`): + +- `acquire(client_id, websocket=None) -> (gpu_id, RuntimeSlot)` +- `release(client_id)` +- `RuntimeSlot.{join_user, user_step, leave_user, register_stream_queue, ...}` + +Today both impls reach into FastVideo-internal's +`fastvideo.entrypoints.realtime.local_runtime` (which exposes +`RealtimeRuntimeConfig`, `GPUPool`, `GPUSlot`). The remote backend talks +HTTP+WS to a separately-deployed runtime of the same shape. + +**Contract that PR 7.5/7.6 must preserve:** + +- `RealtimeRuntimeConfig` accepts `model_registry`, `default_model_id`, + `default_height/width/num_frames/fps/num_inference_steps/guidance_scale/seed/negative_prompt`, + `default_ltx2_image_crf`, `startup_warmup_{enabled,prompt,timeout_seconds}`. +- `GPUPool(gpu_ids: list[int], config: RealtimeRuntimeConfig)` constructor. +- `pool.initialize() / shutdown() / acquire() / release() / get_status()`. +- HTTP endpoints on the remote variant: `GET /healthz`, `GET /readyz`, + `GET /status`, `WS /ws`. (Already match what + `Dreamverse/server/routes/health.py` consumes.) + +**These three health routes still need to migrate into FastVideo's +`build_app` to make `BE_FLAVOR=fastvideo` FE-compatible** — see +[streaming-server.md](streaming-server.md) "build_app route contract" + +[open-threads.md](open-threads.md) follow-up #1. + +When PR 7.6 lands the upstream of `fastvideo/entrypoints/realtime/`, +Dreamverse should not need any code change unless the import path +renames. Decided: keep `streaming/` (post-PR-5.5 public name); ship +`realtime/__init__.py` as a re-export with `DeprecationWarning` for one +release cycle. + +### Note on `default_ltx2_image_crf` + +Dreamverse's `RealtimeRuntimeConfig` includes `default_ltx2_image_crf`. +The April 26 Dreamverse review (D-8) showed this getting passed to +`SamplingParam(...)` and **silently dropped** by the public schema. Post +`d80c2a8` (May 2 typed-config refactor), the migration target is +`request.stage_overrides.refine.image_crf` (per +[design.md](design.md) compatibility mapping table). + +**Whether `d80c2a8` actually wired this through, or it's still latent, +is unverified.** See [open-threads.md](open-threads.md) item D-8. + +## Surface 3: Continuation state (PR 7) + +`Dreamverse/server/video_generation.py:89 ContinuationState` is +Dreamverse's hand-rolled per-session state holder. PR 7 introduced the +typed equivalent at +[`fastvideo/pipelines/basic/ltx2/continuation.py`](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/continuation.py). + +### Field mapping + +| Dreamverse | PR 7 `LTX2ContinuationState` | Notes | +|---|---|---| +| `video_images: list[PIL.Image]` | `video_frames: list[np.ndarray]` (uint8 H×W×3) | numpy is leaner; Dreamverse already round-trips PIL→numpy→PIL just to add noise | +| `audio_latents: torch.Tensor` `[B, C, T, mel]` | `audio_latents: torch.Tensor` (safetensors-serialized; bf16-safe) | unchanged shape; safetensors preserves bf16 | +| `LTX2_VIDEO_CONDITIONING_FRAME_IDX` (env) | `video_conditioning_frame_idx: int` | env constant → per-state field | +| `LTX2_VIDEO_CONDITIONING_STRENGTH` (env) | `video_conditioning_strength: float` | env constant → per-state field | +| `AUDIO_CONDITIONING_NUM_FRAMES` (env) | `audio_conditioning_num_frames: int` | env constant → per-state field | +| `AUDIO_CONDITIONING_STRENGTH` (env) | `audio_conditioning_strength: float` | env constant → per-state field | +| `audio_lps` (passed into `apply_audio`) | `audio_sample_rate: int \| None` | analogous; rename worth confirming with audio team | +| Computed `prefix_sec` per segment | `video_position_offset_sec: float` | **see open question below** | +| `segment_idx` (param to `apply_*`) | `segment_index: int` | per-state field | +| `VIDEO_CONTEXT_NOISE`, `AUDIO_CONTEXT_NOISE`, `ENABLE_AUDIO_COND` | not on state | runtime policy / regularization knobs, not portable session data | +| `apply_video / apply_audio / save_video / save_audio_latents / clear` | not on PR-7 state class | state is a pure data carrier; runtime owns lifecycle policy | + +PR 7 is a strict superset of Dreamverse's data model **plus** lifts +several env globals into per-session typed fields. + +### Lifecycle mapping + +| Dreamverse pattern | `SessionStore` API | +|---|---| +| `self.continuation = ContinuationState()` per session | `state = session_store.snapshot(sid) or LTX2ContinuationState()` | +| `apply_video(req_kwargs, segment_idx)` + `apply_audio(req_kwargs, segment_idx, audio_lps)` | `state = session_store.snapshot(sid)`; runtime builds request from `state.video_frames` / `state.audio_latents` | +| `save_video(frames)` + `save_audio_latents(latents)` | runtime constructs new `LTX2ContinuationState`, `session_store.store(sid, ...)` | +| `clear()` at end of session | `session_store.drop(sid)` | + +`SessionStore` and `BlobStore` ABCs ship with thread-safe in-memory +defaults (`InMemorySessionStore`, `InMemoryBlobStore`). Dreamverse can +adopt them as-is for the local runtime; remote runtimes can plug in +redis-backed implementations later. + +### Wire format (HTTP/WS round-trip) + +Dreamverse's `FastVideoRealtimePool` already speaks the realtime +runtime's HTTP+WS protocol. When PR 7.5/7.6 land state emission on the +server side, the on-the-wire payload is the public envelope: + +```json +{ + "kind": "ltx2.v1", + "payload": { + "schema_version": 1, + "segment_index": 3, + "video_conditioning_frame_idx": 9, + "video_conditioning_strength": 0.75, + "audio_sample_rate": 24000, + "audio_conditioning_num_frames": 5, + "audio_conditioning_strength": 0.5, + "video_position_offset_sec": 0.2, + "video": {"frames_b64": ["..."]}, + "audio": {"safetensors_b64": "..."}, + "metadata": {} + } +} +``` + +JSON-serializable end-to-end; safetensors blob preserves audio dtype +(incl. bf16). For payloads above the inline threshold a `BlobStore` +indirection replaces the b64-encoded body with `{"blob_id": "..."}`; +the blob itself stays inside the runtime that produced it. + +## Migration plan per PR + +| PR | Dreamverse action | +|---|---| +| PR 6 (landed) | Typed `GeneratorConfig` available; flat-kwarg path still works via compat. Optional migration. | +| PR 7 (landed) | Typed `LTX2ContinuationState` available. ~50-line Dreamverse PR: replace `server/video_generation.py:89` import; move `apply_*`/`save_*`/`clear` off the state class onto `VideoGenerationWorker`; read knobs from typed state instead of env globals; swap `list[PIL.Image]` → `list[np.ndarray]`. | +| PR 7.5 (open) | Streaming server skeleton — Dreamverse's `runtime/factory.py` either keeps building `GPUPool` from `RealtimeRuntimeConfig` (current path), or migrates to `ServeConfig.streaming` shape and invokes `fastvideo serve --config realtime.yaml`. Dreamverse's `RuntimePool`/`RuntimeSlot` Protocol can stay in place. | +| PR 7.6 (branch ready) | GPU pool upstream — `local_runtime.py` import becomes a public import with same symbols (`RealtimeRuntimeConfig`, `GPUPool`, `get_available_gpus`). Per-GPU continuation state inside the worker becomes a `SessionStore` reference (Dreamverse doesn't see this). `request.state` / `result.state` round-trip starts working end-to-end on the local runtime. | +| PR 7.10 (planned) | `generate_async` is canonical. Dreamverse's per-segment `user_step` flow can migrate from sync `generate_video(..., **kwargs)` to consuming the typed event stream. Optional; sync wrapper stays. | + +## Dynamo backend contract + +**FastVideo does not host any Dynamo code.** The backend package +(`args.py`, `main.py`, `backend.py`, `register.py`, `health_check.py`, +adapter, Dockerfile) lives entirely in the Dynamo repo at +`components/src/dynamo/fastvideo/`, modeled on +`components/src/dynamo/sglang/`. + +FastVideo's only obligation is to expose a stable, typed Python API +that Dynamo's backend package imports. + +### Contract surface + +| Surface | Exposed as | +|---|---| +| Construction | `VideoGenerator.from_pretrained(model_path, **typed_kwargs)` (typed_kwargs = a stable subset from `GeneratorConfig`; no flat LTX2 legacy) | +| Sync execution | `generator.generate_video(request: GenerationRequest) -> VideoResult` | +| Async execution | `generator.generate_async(request: GenerationRequest) -> AsyncGenerator[VideoEvent, None]` (PR 7.10) | +| Typed request | `fastvideo.api.GenerationRequest`, `SamplingConfig`, `InputConfig` | +| Typed result | `VideoResult` with `video_bytes` or tensor frames + optional `ContinuationState` | +| Continuation | `ContinuationState(kind, payload)` — schema-versioned payloads | +| Health-check input | `VideoGenerator.default_health_check_request() -> GenerationRequest` (256x256 / 8 frames / 1 step) | +| Config dump | `GeneratorConfig.to_dict()` / `ServeConfig.to_dict()` | + +### Request/response mapping (Dynamo ↔ FastVideo) + +``` +NvCreateVideoRequest -> fastvideo.api.GenerationRequest + prompt -> sampling.prompt + size="WxH" -> sampling.width, sampling.height + seconds -> (seconds * nvext.fps) -> sampling.num_frames + input_reference -> input.image_path / input.video_path + nvext.fps -> sampling.fps + nvext.num_frames -> sampling.num_frames (overrides seconds*fps) + nvext.num_inference_steps -> sampling.num_inference_steps + nvext.guidance_scale -> sampling.guidance_scale + nvext.seed -> sampling.seed + nvext.negative_prompt -> sampling.negative_prompt + response_format -> (handled by adapter at output) + +VideoFinalEvent -> NvVideosResponse + video_bytes -> data[0].b64_json (if response_format=b64_json) + video_url (after upload) -> data[0].url (if response_format=url) + metadata.inference_time_s -> inference_time_s +``` + +All fields exist on FastVideo's typed schema after PR 6 expansion (typed +LTX2 kwargs) + PR 7.10 (`generate_async` + health check). + +### Reference: PR ai-dynamo/dynamo#7544 + +Closed draft establishing the Dynamo backend shape. Two frictions +identified: + +1. Flat legacy LTX2 kwargs — solved by PR 6. +2. Sync-only generation — solved by PR 7.10's `generate_async`. + +Next iteration of this PR (or its successor) will reopen against PR 8's +docs reference and land cleanly. + +## Open questions across surfaces + +| # | Question | Source | Status | +|---|---|---|---| +| Q-1 | Multi-model GPU pool | dreamverse_review D-1 | Deferred (production single-model) | +| Q-2 | LTX-2 prompt orchestration promotion to public | dreamverse_review D-2 | Open; consumer-side until 2nd consumer | +| Q-3 | Race-based provider fallback | dreamverse_review D-3 | Open; sequential is current public | +| Q-4 | Router upstream skip on Dreamverse | dreamverse_review D-4 | Resolved (PR 7.9 lands publicly, Dreamverse doesn't consume) | +| Q-5 / D-5 | `generate_async` cutover (audio re-encode) | dreamverse_review | **Blocked on PR 7.10** | +| D-6 | Don't upstream `realtime/local_runtime.py` | dreamverse_review | Resolved (Dreamverse switches to `streaming.gpu_pool.SubprocessGpuPool`) | +| D-7 / Q-6 | FP4Config public colocation | dreamverse_review | **Resolved May 2** — public NVFP4 landed with lazy flashinfer | +| D-8 | `ltx2_image_crf` silently dropped | dreamverse_review | **Unverified post-`d80c2a8`** — see [open-threads.md](open-threads.md) | +| D-9 | `aarch64-conda-linux-gnu-cc` triton compile failure | dreamverse_review | Operational; `ENABLE_TORCH_COMPILE=0` workaround | +| D-10 | Warmup OOM on shared GPU | dreamverse_review | Operational; idle-GPU pre-warm probe | +| D-11 | ffmpeg `Broken pipe` on disconnect | dreamverse_review | Cosmetic logging cleanup | +| — | `video_position_offset_sec` semantics (persistent vs per-segment) | dreamverse_integration | **Open — needs decision before PR 7.6 emits state** | +| — | `SessionStore` / `BlobStore` lifecycle (TTL/eviction/blob-drop) | dreamverse_integration | Open — defer to PR 7.5 design pass | + +See [decisions-log.md](decisions-log.md) for full rationale per +decision. + +## Don't / Cautions + +- **Don't pop the Dreamverse stash on this branch.** It's 3867 lines of + orphan modular refactor with broken absolute imports. +- **Don't change `RealtimeRuntimeConfig` shape without coordinating + with Dreamverse `runtime/factory.py`.** +- **Don't promise public compatibility for private Dreamverse-only + field aliases.** Those belong in the private adapter layer per design + spec. diff --git a/.agents/memory/dreamverse-integration/decisions-log.md b/.agents/memory/dreamverse-integration/decisions-log.md new file mode 100644 index 0000000000..62b627bed3 --- /dev/null +++ b/.agents/memory/dreamverse-integration/decisions-log.md @@ -0,0 +1,978 @@ +# Decisions Log — D + Q Resolutions + +Cross-doc consolidated decision log. Each entry: ID, source doc, +question/decision, rationale, current status. + +For implementation status see [pr-roadmap.md](pr-roadmap.md). For +follow-up actions see [open-threads.md](open-threads.md). + +**Last updated:** 2026-05-06 (added D-21 — chunk-stutter root cause is software libx264 encoding consuming ~22% of segment wall-time, NOT a migration regression; verified by 3 parallel explore agents that NVFP4 + torch.compile coverage matches FastVideo-internal exactly; landed opt-in NVENC build path in install_native_ffmpeg.sh + `--nvenc`/`--no-nvenc` flag in dreamverse-deploy.sh + `apps/dreamverse/server/benchmarks/benchmark_av_streaming.py` regression test + memory dir update; default codec stays `libx264` for backward compat, opt-in via `--nvenc`. Earlier: added D-12 — GpuPool layer separation, Oracle review post-#1257-merge; added D-13 — prompt enhancer / LLMProvider abstraction shape, Oracle review pre-#1258-merge; added D-14 — streaming auxiliaries cohesion, Oracle review during #1284 review cycle; added D-15 — streaming router placement + sticky/active-active deferral, Oracle review during #1286 review cycle; added D-16 — streaming router polish round 2, second-pass review on top of D-15 covering bridge cancellation hygiene, registry state machine, httpx hard-fail, replica YAML parsing, and `websockets` dep; added D-17 — strategy reversal: abandon 6-PR split in favor of single mega-PR #1288 on `will/ltx2_sr_port`; added D-18 — Option B+ chosen: Dreamverse FE+product-server move into FastVideo as `apps/dreamverse/` subfolder while generic backend stays at `fastvideo.entrypoints.streaming.*`; integration-review.md deprecated, integration-plan.md is the executable migration plan; added D-19 — D-18 executed: 5 commits land on `will/dreamverse-monorepo`, fix-up commits corrected the integration-plan's invalid "delete generic-merged, import public substitutes" assumption — generic-merged files carried product-local instead, e2e passes against migrated code with /proc-verified evidence; added D-20 — segment-2 BrokenPipe root cause was a TWO-direction silent drop of LTX-2 audio kwargs in public `VideoGenerator`). + +## Status legend + +- ✅ **Resolved** — decision made and implementation complete (or no implementation needed) +- 🟡 **Deferred** — decision made, implementation deferred to a known PR +- 🔴 **Open** — needs decision + +## Post-merge architecture decisions + +### D-26: Rebase `will/dreamverse-monorepo` directly onto `origin/main` (PR base flip) + +**Status:** ✅ Resolved 2026-05-06 (UTC; 2026-05-07 local). 66 commits cleanly replayed via `git rebase origin/main`; force-pushed via `--force-with-lease`. Local backup branch `will/dreamverse-monorepo-pre-main-rebase-backup-20260506` preserved at the pre-rebase tip `2ee839a3`. PR #1288 on `will/ltx2_sr_port` is untouched. +**Source:** User directive: "update this branch will/dreamverse-monorepo to be directly against origin/main for PR purposes instead of ltx_sr_port (don't open new PRs)". + +**Question:** `will/dreamverse-monorepo` was forked from `will/ltx2_sr_port` (PR #1288's head). Should it stay stacked on top of `will/ltx2_sr_port`, or be rebased so its PR base becomes `origin/main` directly? + +**Decision:** Rebase `will/dreamverse-monorepo` onto `origin/main` directly. This makes the branch openable as a single PR against main containing the entire 66-commit chain (40 from the legacy `will/ltx2_sr_port` work + 26 dreamverse-monorepo-specific commits including D-19/D-20/D-21/D-22). + +**Pre-rebase state:** + +``` +origin/main (c17d33bf) ← 1 new commit since 2aaeee2a (PR #1253 SSIM cosine) + │ + └─ 2aaeee2a (PR #1286 squash merge; old shared base) + ├─ 40 commits → will/ltx2_sr_port @ fbd823df (PR #1288) + └─ 40 + 26 = 66 commits → will/dreamverse-monorepo @ 2ee839a3 +``` + +**Post-rebase state:** + +``` +origin/main (c17d33bf) + │ + └─ 66 commits → will/dreamverse-monorepo @ 83829c5e (NEW SHAs, same content) + +origin/main (c17d33bf) -- still in main lineage + │ + └─ 2aaeee2a + └─ 40 commits → will/ltx2_sr_port @ fbd823df (PR #1288, UNCHANGED) +``` + +**Operation:** + +1. **Backup**: `git branch will/dreamverse-monorepo-pre-main-rebase-backup-20260506 will/dreamverse-monorepo` (local-only safety net pinning `2ee839a3`). +2. **Rebase**: `git rebase origin/main` while on `will/dreamverse-monorepo`. All 66 commits replayed conflict-free in ~30s. The single new origin/main commit `c17d33bf` (#1253 SSIM cosine regression) touches only `fastvideo/tests/ssim/*`, `fastvideo/tests/modal/ssim_test.py`, `.agents/skills/seed-ssim-references/SKILL.md`, `fastvideo/pipelines/basic/stable_audio/stages/decoding.py`, and a 56-line block in `fastvideo/entrypoints/video_generator.py`. None of our 66 commits touch the SSIM files; the `video_generator.py` overlap auto-merged because our D-20 changes (`_BATCH_EXTRA_PASSTHROUGH_KEYS`, result-dict surface) and #1253's changes are in different parts of the file. +3. **Verification (file-level, before push)**: + - Commit count: 66 (PASS) + - Co-author trailer count: 231 across 66 commits — within historical norm (some legacy `will/ltx2_sr_port` commits had partial trailers per [`authors.md`](authors.md) "Known gaps"). + - Tree-diff vs backup: only the 845-line forward delta from `c17d33bf` (expected). All D-20/D-21/D-22-introduced content (`_BATCH_EXTRA_PASSTHROUGH_KEYS` x2, "unknown field" x1, `av_chunk_interval_ms` x4, `av_chunk_publish_ms` x2, `enable-nvenc` x2, `NVENC_OVERRIDE` x4) survives intact. + - `pre-commit run --files` on the 7 most-edited files: PASS (yapf/ruff/codespell/mypy/spaces). + - Runtime pytest hung mid-import on this shared dev box (pre-existing CUDA/torch init issue affecting other commands too) — NOT a rebase regression. The same `fastvideo/tests/api/` suite passed 185/185 pre-rebase. +4. **Force-push**: `git push --force-with-lease=will/dreamverse-monorepo:2ee839a3... origin will/dreamverse-monorepo`. Updated `2ee839a3...83829c5e (forced update)`. + +**Implications:** + +- The branch can now be opened as a PR against `origin/main` containing all the LTX-2 SR port + NVFP4 + Dreamverse monorepo migration + audio kwarg fix + warmup + NVENC build + benchmarks + integration memory dir, in a single review unit. +- PR #1288 on `will/ltx2_sr_port` is untouched and continues to track its own subset of the work. If PR #1288 lands first, the duplicated commits on `will/dreamverse-monorepo` will be reconciled at the next rebase by `git rebase origin/main` dropping commits whose content is now in main (same mechanism as the post-#1286 rebase recorded in [state.md](state.md) "Post-#1286 rebase summary"). +- Local backup `will/dreamverse-monorepo-pre-main-rebase-backup-20260506 @ 2ee839a3` keeps the old chain available; recommend deleting after the new branch state is verified by running on a non-stuck dev box (or after the PR merges). + +**Watch-outs:** + +- Anyone with a checkout of the OLD `will/dreamverse-monorepo` (pre-rebase) needs to `git fetch origin will/dreamverse-monorepo --force` and discard local commits, OR rebase their local commits onto the new `83829c5e`. None observed in the runbook's worktree-sharing model. +- The trailer-count 231 (vs expected 264 for full coverage) is NOT introduced by this rebase — it's the historical gap from [`authors.md`](authors.md). Verifiable by counting trailers on the backup branch (same 231). + +### D-21: AV chunk stutter root cause — software libx264 encoding consumes ~22% of segment wall time; opt-in NVENC fix + +**Status:** 🟡 Deferred — fix landed (NVENC build + `--nvenc` flag), default unchanged so no behavior regression for operators who haven't rebuilt ffmpeg yet. Switch default to `h264_nvenc` once benchmark numbers are captured + a release notes entry is published. +**Source:** Live debug session 2026-05-06 prompted by user observation: "stuttering still between chunks" after D-20 + warmup r3 fix. 3 explore agents fanned out (NVFP4 config, torch.compile config, AV chunk pacing). + +**Question:** With D-20 (audio kwarg routing fix) + r3 warmup pass landed and warmup_success=true, why is the live deploy still stuttering between chunks? Is the migration missing some quantization or compile coverage that the FastVideo-internal reference has? + +**Investigation (3 parallel explore agents):** + +1. **NVFP4 config diff** (`bg_0629273d`): No divergence. apps/dreamverse, original Dreamverse, and FastVideo-internal ALL use `layer_profile="refine"` with the same 48-block `fp4_layers` superset, e2m1 mantissa, fp32 alpha, layout_128x4 scale, group size 16. Stage gating (`base` vs `refine`) and `transformer_refine_quant` carrier match. The only naming difference is the public surface rename `FP4Config` → `NVFP4Config`. +2. **torch.compile config diff** (`bg_97081cc5`): No divergence. ALL THREE repos use `inductor / fullgraph=True / max-autotune-no-cudagraphs / dynamic=False` and compile only the transformer + text_encoder. **VAE / audio_vae / vocoder are eager bf16 in all three** — so the migrated repo is not missing a compile pass that the original had. Only behavioral difference: FastVideo-internal calls `target.eval()` on its audio encoder; the migrated worker does not (separate D-22 follow-up). +3. **AV chunk pacing** (`bg_574e831e`): `stream_fmp4` has only one timer (`av_encode_stream_ms`) — no per-chunk instrumentation. Between `worker.generate_step()` returning and `stream_fmp4()` returning there is no significant work other than ffmpeg's own encode + chunk emission. The `main_user_step − worker_e2e ≈ 1300ms` gap is therefore mostly ffmpeg + controller relay loop, NOT IPC. Per-chunk emission cadence is driven by stdout read size (1 MiB), not fragment boundary, so non-uniform pacing is plausible. + +**Root cause synthesis:** + +| Phase | Wall-time | Compute | Compiled? | NVFP4? | +|---|---|---|---|---| +| transformer denoise (gen) | ~4500 ms | GPU | YES | YES | +| save_conditioning | ~100 ms | GPU/CPU | n/a | n/a | +| **stream_fmp4 (libx264 software encode)** | **~1300 ms** | **CPU** | **NO** | **NO** | +| TOTAL wall | ~6000 ms | producing 4.67 s playable | | | + +Realtime ratio = 4.67 s playable / 6.0 s wall = **0.78x**. FE buffer drains 1.3 s per segment until empty → inter-segment stutter. **The cause is not a migration regression** (FastVideo-internal exhibits the same architectural limit); it is the choice to use `libx264 ultrafast` software encoding on the segment-streaming hot path on machines that have idle hardware encoders. + +**Build-time finding:** `apps/dreamverse/scripts/install_native_ffmpeg.sh` was building ffmpeg with only `--enable-libx264 --enable-lto`. No `--enable-nvenc`, no nv-codec-headers prereq. So even setting `FASTVIDEO_VIDEO_CODEC=h264_nvenc` would have failed at runtime with "encoder not found". + +**Resolution (this round, default-preserving):** + +1. **`apps/dreamverse/scripts/install_native_ffmpeg.sh`** now: + - Clones `nv-codec-headers` (NVENC/NVDEC API headers; no CUDA libs) into `$INSTALL_PREFIX` so `ffnvcodec.pc` is on `pkg-config`'s search path. + - Configures ffmpeg with `--enable-cuda --enable-nvenc --enable-cuvid --enable-nvdec` plus `--extra-cflags=-I$CUDA_PREFIX/include` and `--extra-ldflags=-L$CUDA_PREFIX/lib64`. + - Drops `--enable-cuda-nvcc` and `--enable-libnpp` so the build does NOT require `--enable-nonfree` (we only need hardware encode/decode, not GPU-side filters). + - New env knobs: `ENABLE_NVENC=0|1` (default 1), `CUDA_PREFIX` (default `/usr/local/cuda`), `NV_CODEC_REF`. + - Sanity check verifies the resulting binary exposes `h264_nvenc` / `hevc_nvenc` encoders before exiting 0. + - **Emits `FASTVIDEO_VIDEO_CODEC=libx264` in the env file by default** so existing deploys are runtime-unchanged. + +2. **`.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh`** now: + - Accepts `--nvenc` / `--no-nvenc` flag (defaults to off, matches existing behavior). When `--nvenc`, exports `FASTVIDEO_VIDEO_CODEC=h264_nvenc` into the backend setsid block. + - Validates the binary actually has `h264_nvenc` encoder via `ffmpeg -encoders | grep h264_nvenc`. Fails fast if `--nvenc` is requested against a libx264-only ffmpeg with a clear hint to rerun the install script. + - `DREAMVERSE_NVENC` env var as the env-var counterpart (flag overrides). + - Banner now prints `nvenc=true|false` alongside `warmup` and `torch_compile`. + +3. **`apps/dreamverse/server/benchmarks/benchmark_av_streaming.py`** is the regression test: + - Drives `stream_fmp4` directly with synthetic 121-frame 1920x1088 video + 5-second 24kHz stereo audio (production shape). + - Sweeps `libx264` and `h264_nvenc` (auto-skips encoders not present in the active ffmpeg). + - Reports per-codec: wall_ms_min/median/p95/max, bytes_median, chunks_median, and **realtime_ratio_median** (playable / wall, must be ≥ 1.0 to avoid buffer drain). + - Exit code 1 if any codec produces realtime_ratio < 1.0; useful as a CI gate. + +**Empirical findings (this dev host, B200):** + +- `libx264 ultrafast` benchmark on production-shape input (121 frames, 1920x1088, 5s 24kHz stereo audio): wall_median=611ms, wall_p95=644ms, 8.25x realtime ratio in isolation. Captured via `apps/dreamverse/server/benchmarks/benchmark_av_streaming.py --runs 5 --codecs libx264`. +- **B200 has NO NVENC silicon.** Direct ffmpeg probe (`-c:v h264_nvenc` against a 64x64 0.2s color frame) fails with `OpenEncodeSessionEx failed: unsupported device (2): No capable devices found`. This is a hardware omission — datacenter Blackwell (B200) and some H100 SKUs prioritize compute density and ship without NVENC. Only consumer Blackwell (RTX 50-series) and select datacenter SKUs (T4, A10, A100 PCIe) have NVENC. +- The deploy script's `--nvenc` path now does TWO checks: (a) `h264_nvenc` is in the encoder list (build-time); (b) a 64x64 ffmpeg probe actually succeeds (runtime-time). On a B200, the runtime probe fails fast with a clear "no NVENC silicon on this host" error pointing at SKU-level alternatives. +- **Stutter on B200 is NOT primarily ffmpeg encoding.** The benchmark shows libx264 ultrafast at 611ms, but production logs show a 1300ms gap between `worker_e2e` and `main_user_step`. The other ~700ms is IPC + controller WebSocket relay (not measured per-chunk in current `stream_fmp4`). + +**Open / deferred:** + +- ~~D-22~~ ✅ **Resolved 2026-05-06** in `bade2c0a` — `stream_fmp4` is now fully instrumented with per-phase + per-chunk timings; gpu_pool prints a per-segment summary line. The controller-side WS-send slice is tracked separately as D-22-CTL. +- D-22-CTL: per-chunk timing in the controller's AV relay loop in `session/controller.py` (between media event arrival and `ws_send_bytes`). Worker side is captured by D-22; the controller side is the still-unmeasured remainder of the 700ms IPC+relay gap. +- D-23: B200 + RTX 5090 split — for B200-class deploys without NVENC, the stutter fix needs a different approach (pipeline gen N+1 with encode N, or larger initial FE buffer pre-fill). For RTX 5090 / T4 / A10 deploys, `--nvenc` is the answer once tested. Make the deploy default conditional: probe NVENC at boot, default `--nvenc` if available. +- D-24: re-evaluate flipping the deploy default from `libx264` to `h264_nvenc` after a real-NVENC host (RTX 5090 or H100 PCIe) benchmark is run. Tradeoff: NVENC has ~5-10% lower compression at same quality but is hardware-accelerated. For real-time streaming the latency win dominates IF the host has NVENC. +- D-25: pipeline benchmark via Python SDK (`apps/dreamverse/server/benchmarks/benchmark_pipeline.py`, landed in `f98811e0`) — captures per-stage timings via `FASTVIDEO_STAGE_LOGGING=1`. Cold-run baseline on B200 NVFP4 (no compile, no warmup): 6.99s for 5.04s playable = 0.72x realtime. Per-stage profile dominated by LTX2RefineLoRAStage (40% on cold, includes one-time LoRA conversion + adapter load), LTX2DenoisingStage (20% — base DiT 5-step denoise), LTX2TextEncodingStage (11%), DecodingStage (4%), audio_decoding/upsample/init each <1%. Future work: rerun under `compile_warm` scenario for steady-state numbers. +- The `target.eval()` call on the audio encoder that FastVideo-internal does and the migrated Dreamverse does not — surface as a separate decision once observed empirically (probably a no-op in inference path but worth the symmetry). + +**Cross-references:** + +- [D-20](decisions-log.md#d-20) (audio kwarg routing fix) made segment 2 generate cleanly. D-21 makes the segment-to-segment chain stutter-free in real-time. +- [`apps/dreamverse/server/av_streaming.py::stream_fmp4`](file:///home/william5lin/FastVideo/apps/dreamverse/server/av_streaming.py) is the hot path; the cmd builder there respects `FASTVIDEO_VIDEO_CODEC` and branches on `*_nvenc` codecs to use NVENC presets (`p1`/`p2`/...) and `-rc constqp -qp 28` instead of libx264 presets (`ultrafast`/...). +- The benchmark cross-references D-21 in its module docstring so a future contributor reading the script alone has the context. + +### D-20: Segment-2 BrokenPipe root cause — two-direction silent drop of LTX-2 audio kwargs in public `VideoGenerator` + +**Status:** ✅ Resolved 2026-05-05. 4 commits on `will/dreamverse-monorepo` (`1b686f4e`..`5eaf0a13`); pushed to origin. End-to-end verified on GPU4 (`/proc/$BE_PID/environ` + `Cached audio latents shape=(1, 8, 126, 16) for segment 2` log line + `Segment 2: relayed av chunks=22, bytes=3.8MB`). +**Source:** Live debugging session triggered by recurring `RuntimeError: ffmpeg frame writer failed: [Errno 32] Broken pipe` on segment 2 in `/tmp/opencode/dreamverse-deploy/backend-gpu4.log`. + +**Question:** Why does segment 2 of every Dreamverse session crash with a writer-side EPIPE, while segment 1 streams fine? + +**Symptom chain decoded:** + +1. ffmpeg writer thread in `apps/dreamverse/server/av_streaming.py:307-317` raises `BrokenPipeError` mid-frame. +2. ffmpeg's exit code is `0` — the `if rc != 0` branch of `stream_fmp4` never fires; only `if writer_error[0] is not None` does. +3. `rc=0 + BrokenPipeError` means ffmpeg exited cleanly *before* the writer finished pushing all frames → ffmpeg closed stdin early due to `-shortest` + audio shorter than video. +4. Manual repro confirmed: with audio 71240 samples (~2.97s) and video 112 frames (~4.67s), ffmpeg `-shortest` + 1MB pipe + 1920x1088x3 frames produces exactly this signature (`rc=0 out_bytes=751028 writer_error=BrokenPipeError(32)`). +5. The 1.7s audio undershoot is suspicious because Dreamverse's `apply_audio()` in `apps/dreamverse/server/video_generation.py:132-186` explicitly extends `audio_num_frames = NUM_FRAMES + audio_extra` (=`121 + 40 = 161`) for continuation segments. Audio should be 6.71s, not 5.01s. The kwarg was being silently dropped. + +**Root cause (TWO directions):** + +| Direction | Where | What was missing | What it broke | +|---|---|---|---| +| **Inbound** (kwargs → `batch.extra`) | `fastvideo/entrypoints/video_generator.py::_generate_video_impl` | The 5-key extraction block (`ltx2_audio_latents`, `ltx2_audio_clean_latent`, `ltx2_audio_denoise_mask`, `audio_num_frames`, `video_position_offset_sec`) that FastVideo-internal has at lines 168-183. Without it, `apply_audio`'s kwargs landed in `sampling_param.update(kwargs)` which `logger.error`'d "%s has no field %s" and dropped them. | `audio_num_frames` never reached `batch.extra` → `ltx2_denoising.py:325` fell back to `batch.num_frames=121` → audio generated for 5.01s instead of 6.71s. After `head_trim_audio_frames=49` removed the leading 2.04s, only 2.97s of audio remained for 4.67s of video. | +| **Outbound** (`batch.extra` → result dict) | `fastvideo/entrypoints/video_generator.py::_generate_single_video` | `"ltx2_audio_latents": output_batch.extra.get("ltx2_audio_latents")` in the result dict. Internal exposes it at line 556; public didn't expose it at all. | `Dreamverse._derive_next_audio_latents()` always saw `None` → `self.continuation.audio_latents` was never set → segment 2's `apply_audio` short-circuited (`if not (… and self.audio_latents is not None): return`) → no audio continuation, no `audio_num_frames` extension, no `ltx2_audio_clean_latent` carry-over. | + +The two directions hid each other: even after the inbound block was ported, segment 2 still broke until the outbound surface was added. Both must be present for audio continuation to round-trip. + +**Resolution — 4 commits on `will/dreamverse-monorepo`:** + +| SHA | Subject | +|---|---| +| `1b686f4e` | `[fix] dreamverse: pin ffmpeg native build toolchain by uname -m` | +| `265ce1a6` | `[fix] api: route LTX-2 audio kwargs through batch.extra; strict update` | +| `dab9499c` | `[feat] dreamverse-deploy: native ffmpeg + compile-off defaults` | +| `5eaf0a13` | `[feat] dreamverse-deploy: --warmup / --torch-compile CLI flags` | + +`265ce1a6` is the substantive public-API fix: ports `_BATCH_EXTRA_PASSTHROUGH_KEYS` extraction in `_generate_video_impl`, ports `_extra_overrides` consumption in `_generate_single_video` (writes into `batch.extra`), surfaces `ltx2_audio_latents` in the result dict, and converts `SamplingParam.update()` from `logger.error`-and-drop to `raise ValueError` on unknown keys so the next contributor who adds an unrecognized kwarg hits a loud failure pointing at `_BATCH_EXTRA_PASSTHROUGH_KEYS` instead of debugging a broken-pipe-shaped symptom hours later. Adds `fastvideo/tests/api/test_extra_overrides_routing.py` (7 tests pinning the contract). + +**Why the strict-update is non-negotiable:** the `logger.error`-and-continue pattern is the exact mechanism that hid this bug for the entire Dreamverse-monorepo migration window. It silently traded loud-failure-now for silent-corruption-later. Strict raise + a clear "route via `_BATCH_EXTRA_PASSTHROUGH_KEYS`" hint converts the next regression of this shape from "broken pipe in production" to "ValueError at first call". + +**Cross-references:** + +- [D-11](decisions-log.md#d-11) (Apr 26) noted "ffmpeg fragment write Broken pipe" but framed it as cosmetic (client-disconnect race). Today's was a different code path: server-side EPIPE caused by a server-internal A/V duration mismatch. Both are now resolved; D-11's fix domain (swallow on intentional disconnect) remains unchanged but lower priority. + +- The `[fix] api: route LTX-2 audio kwargs ...` commit (`265ce1a6`) is on `will/dreamverse-monorepo` only. Cherry-picking onto `will/ltx2_sr_port` so it lands as part of PR #1288's mega-PR is a deferred follow-up — see open-threads.md "Cherry-pick API audio routing fix to PR #1288". + +**Side effects of the fix:** + +- `[feat] dreamverse-deploy: native ffmpeg + compile-off defaults` (`dab9499c`) wires the native LTO+libx264+native-arch ffmpeg the team playbook mandates into every deploy via `FASTVIDEO_FFMPEG_BIN=$HOME/opt/ffmpeg-native/bin/ffmpeg` and disables `torch.compile` by default (`ENABLE_TORCH_COMPILE=0`) so segment-1 cold start drops from ~3-4min (max-autotune) to ~45s (pure inference) — required for any iterative debugging cycle to fit inside the 300s session timeout. +- `[fix] dreamverse: pin ffmpeg native build toolchain by uname -m` (`1b686f4e`) makes `apps/dreamverse/scripts/install_native_ffmpeg.sh` immune to conda envs that activate both `gcc_linux-64` and `gcc_linux-aarch64` (the aarch64 activation script sorts later and wins, so the inherited `CC=aarch64-conda-linux-gnu-cc` defeated the script's `: "${CC:=...}"` deferred default and tripped x264's compiler probe with "unknown value 'native' for '-march'"). +- `[feat] dreamverse-deploy: --warmup / --torch-compile CLI flags` (`5eaf0a13`) adds `--warmup`/`--no-warmup` and `--torch-compile`/`--no-torch-compile` flags that override the env-var defaults and accept any position relative to the positional GPU/port args (verified across 13 parser permutations). + +**Watch-outs for downstream contributors:** + +- Adding a new pipeline-specific kwarg now requires either making it a `SamplingParam` field OR adding it to `_BATCH_EXTRA_PASSTHROUGH_KEYS` in `fastvideo/entrypoints/video_generator.py`. Strict `update()` will raise `ValueError` on unknown kwargs that aren't routed through one of those paths. +- The `[fix] api:` commit (`265ce1a6`) is BACKWARD-INCOMPATIBLE for any caller that was relying on `SamplingParam.update()` to silently swallow unknown keys. If pre-existing CI breaks elsewhere on this surface, the fix is to add the legitimate kwarg to `SamplingParam` or `_BATCH_EXTRA_PASSTHROUGH_KEYS` (not to revert the strict mode). + +### D-19: D-18 executed — Dreamverse migration on `will/dreamverse-monorepo`, generic-merged files carried product-local + +**Status:** ✅ Resolved 2026-05-05. 5 commits on top of `will/ltx2_sr_port` HEAD `fbd823df`. Branch pushed to origin at `c1fe5d4c`. e2e definitively passes. +**Source:** Execution of [integration-plan.md](integration-plan.md) (D-18 plan), with one significant deviation surfaced by Oracle review. + +**Commits:** + +| SHA | Message | +|---|---| +| `08828d96` | `[feat] dreamverse-monorepo: Phase 1 — skeleton + tooling` | +| `f3a863ba` | `[feat] dreamverse-monorepo: Phase 2 — backend move + import rewires` | +| `876f7eb3` | `[feat] dreamverse-monorepo: Phase 3 — frontend + public assets` | +| `1d47ede6` | `[fix] dreamverse-monorepo: carry generic-merged files product-local` | +| `c1fe5d4c` | `[fix] dreamverse-monorepo: entrypoint + audio re-encode + verification` | + +Final stats: 164 files changed, +53294/-2 LOC, 31725 files under `apps/dreamverse/`. + +**Question:** [integration-plan.md](integration-plan.md) Phase 2 instructed "DELETE generic-merged files (`gpu_pool.py`, `av_streaming.py`, `worker_ipc.py`, `mock_server.py`, `session_init_image.py`, `session_logger.py`) from the Dreamverse copy and rewire imports to public `fastvideo.entrypoints.streaming.*` substitutes". The first execution attempt followed this instruction. Was that the right call? + +**Decision (forced by Oracle finding):** No. The "delete + import substitute" strategy assumed the public modules were API-compatible drop-ins for the Dreamverse-product modules. They are NOT. Public `fastvideo.entrypoints.streaming.GpuPool` is an abstract base class with `acquire/run/release/shutdown/health` semantics designed for a future Phase 4 streaming-runtime API. Dreamverse's product `GPUPool(gpu_ids).initialize()` has totally different shape (per-GPU subprocess workers with `slot.user_step / register_stream_queue / acquire(client_id, websocket) -> (gpu_id, slot)` semantics). Substituting one for the other made `apps/dreamverse/server/main.py:63` fail at boot with `TypeError: GpuPool() takes no arguments`. + +**Fix:** Carry all 7 generic-merged files (gpu_pool, av_streaming, worker_ipc, mock_server, session_init_image, session_logger, server_entry) into `apps/dreamverse/server/` as PRODUCT-LOCAL — same status as the rest of the Dreamverse server tree. Imports stay flat (`from gpu_pool import GPUPool`, etc.), matching Dreamverse's existing sys-path-injection convention. Public `fastvideo/entrypoints/streaming/*` reverts cleanly to its `fbd823df` state — `git diff fbd823df..c1fe5d4c -- fastvideo/entrypoints/streaming/` is empty. + +The "import public substitutes" promise becomes a future Phase 4 task: actually harmonize the APIs so Dreamverse can drop its product-local copies. Not in scope for this migration. + +**Why the e2e initially gave a false positive:** + +The first run of all 8 Playwright tests passed (5.1s) — Oracle caught that this was misleading. The `dreamverse-server` console script in `/home/william5lin/miniconda3/envs/fv-main/bin/dreamverse-server` was installed by a prior `pip install -e /home/william5lin/Dreamverse`, so `from server_entry import cli` resolved to `/home/william5lin/Dreamverse/server/...` (the canonical install), not `apps/dreamverse/server/...` (the migrated tree). The migrated code was never actually exercised. + +**Fix to entrypoint resolution** (in `c1fe5d4c`): wrapper scripts at `apps/dreamverse/scripts/dreamverse-server` (and `dreamverse-mock-server`) explicitly do: + +```bash +#!/usr/bin/env bash +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}/apps/dreamverse/server" +exec "${REPO_ROOT}/.venv/bin/python" main.py "$@" +``` + +This guarantees the migrated tree is what runs. Verified end-to-end via `/proc/$PID/cwd = /home/william5lin/FastVideo/apps/dreamverse/server` during the second e2e run. All 8 Playwright tests pass against this confirmed-migrated backend. + +**Phase 0 environment prereqs (validated 2026-05-05):** + +- `flashinfer-python` in FastVideo `.venv` — required for NVFP4 path. Without it, model load fails with `ImportError: NVFP4 quantization requires flashinfer`. +- `cerebras-cloud-sdk` and `openai` in `.venv` — required by the migrated prompt enhancer. +- For B200 / sm_100a + gcc-15 conda toolchain: nvcc rejects host compiler. Workaround: + ```bash + CUDAHOSTCXX=/usr/bin/g++-13 + NVCC_PREPEND_FLAGS="-ccbin /usr/bin/gcc-13 -allow-unsupported-compiler" + ``` + Without these, flashinfer JIT compilation fails with `error: #error -- unsupported GNU version! gcc versions later than 14 are not supported!`. + +These are **operator-side prerequisites**, not migration code defects. Documented in `apps/dreamverse/README.md` and `docs/contributing/dreamverse-development.md`. + +**E2E evidence (live, post-fix-up):** + +``` +PID 179112 cwd: /home/william5lin/FastVideo/apps/dreamverse/server +/healthz → 200 {"status":"ok","service":"ltx2-streaming-backend",...} +/readyz → 200 {"status":"ready","ready_gpu_workers":1,"total_gpus":1,...} +GPU4 mem: 50.9 GiB (NVFP4 model loaded) + +Playwright (8/8 PASS in 5.1s): + ✓ backend-health/healthz returns ok via the next.js rewrite (32ms) + ✓ backend-health/readyz reports gpu pool state (11ms) + ✓ backend-health/status endpoint exposes gpu pool snapshot (12ms) + ✓ backend-health/prompt-system-config exposes operator-tunable prompts (15ms) + ✓ backend-health/curated presets endpoint serves a non-empty list (devtools only) (19ms) + ✓ frontend-shell/main page loads and exposes the FastVideo brand chip (1.4s) + ✓ frontend-shell/composer hydrates with curated preset cards (1.3s) + ✓ preset-prompt-generation/generates the first segment from a curated preset prompt (1.8s) +``` + +**Implications:** + +- The integration-plan.md is **partially superseded** by D-19 outcome: + - Phase 2's "DELETE generic-merged" prescription is invalid; replace with "carry product-local". + - Phase 0 prereqs need the gcc-13 / `NVCC_PREPEND_FLAGS` workaround documented for B200 hosts. + - Phase 4 (in a future PR) is now responsible for actually harmonizing public-vs-Dreamverse pool APIs so the carried product-local modules can be deleted. +- `dreamverse-mock-server` script under `apps/dreamverse/scripts/` is the only canonical launcher. The conda env's legacy `/home/william5lin/miniconda3/envs/fv-main/bin/dreamverse-server` should NOT be used (it points at canonical Dreamverse repo). +- The audio re-encode handling in `apps/dreamverse/server/video_generation.py` was carried per `c1fe5d4c`; verify the exact shape (restored from source vs deferred) when reading the commit. +- Once this branch lands as a PR + merges, the Dreamverse repo can be archived per [integration-plan.md](integration-plan.md) Phase 7. + +**Open follow-ups:** + +- Open a PR for `will/dreamverse-monorepo` (target main, base on `will/ltx2_sr_port` until #1288 merges). +- Re-Oracle the post-fix-up state to confirm the prior FAIL is now PASS. +- Eventually move audio re-encode into a public module (Phase 4) so the carried product file can be slimmed. +- Eventually do real Phase 4 API harmonization between Dreamverse pool and public `fastvideo.entrypoints.streaming.GpuPool` so the 7 carried product files can be deleted. + +### D-18: Option B+ — Dreamverse becomes `apps/dreamverse/` subfolder under FastVideo + +**Status:** ✅ Resolved 2026-05-05. [integration-plan.md](integration-plan.md) is the executable migration plan; [integration-review.md](integration-review.md) is deprecated but kept for drift audit + OSS precedents. +**Source:** User decision after reviewing [integration-review.md](integration-review.md)'s Option D recommendation. + +**Question:** [integration-review.md](integration-review.md) recommended **Option D** — Dreamverse stays a separate repo, generic backend (streaming runtime, GPU pool, prompt enhancer, router) merges into `fastvideo.entrypoints.streaming.*`. The user reviewed this and chose a different shape: keep the generic-backend principle from Option D but ALSO move the Dreamverse FE + product server into FastVideo as a subfolder (`apps/dreamverse/`). Combination is "Option B+" (Option B layout with Option D's backend principle). + +**Decision:** Option B+. Concrete shape: + +- **One repo**: `hao-ai-lab/FastVideo`. Dreamverse repo gets archived after migration completes. +- **Python ML library** stays at root: `fastvideo/`, `fastvideo-kernel/`. +- **Generic backend** stays at `fastvideo.entrypoints.streaming.*` (already there per #1257/#1258/#1284/#1286/#1288). +- **Dreamverse product** moves into `apps/dreamverse/{server,web,prompts,serve_configs,scripts}/`. +- **Tooling**: uv workspace for Python (`[tool.uv.workspace] members = ["apps/dreamverse/server"]`), standalone pnpm for the FE (no root `package.json`), split CI workflows with path-filter triggers. + +**Rationale:** + +- Drops the cross-repo coordination overhead identified in the post-#1286 rebase cycle (D-17 handled by consolidating into mega-PR; D-18 prevents the next round of cross-repo coordination from happening). +- Keeps the architectural separation Option D recommended (FastVideo owns reusable runtime; product owns product). The boundary is now `apps/dreamverse/` directory rather than two repos. +- Single repo means atomic cross-cutting refactors (e.g. GpuPool API change + Dreamverse adoption) ship as one PR. +- OSS precedents support the shape (chainlit uv-workspace + pnpm; open-webui Python + Svelte with paths-ignore CI). The librarian explicitly noted no precedent for "Python ML library + Next.js product merged into library namespace" — but this isn't that pattern. Dreamverse goes into a sibling directory, NOT into `fastvideo.entrypoints.dreamverse.*`. Library namespace stays clean. + +**Why not Option D (separate repos):** + +- Each upstream merge into FastVideo invalidates Dreamverse's lockfile/imports; the post-#1286 rebase showed this requires coordination overhead that scales with feature velocity. +- Cross-repo contract tests catch shape drift but not behavior drift. +- Two repos means two `AGENTS.md`, two CI configs, two release stories, two Dependabot dashboards. + +**Why not Option C (full merge into `fastvideo.entrypoints.dreamverse.*`):** + +- Forces FastVideo to ship Tailwind config + curated preset JSON + Next.js build artifacts. +- Locks Dreamverse product cadence to FastVideo PyPI releases. +- Librarian: "no 1:1 precedent for Python ML library + Next.js product merged into library namespace" — argues against this. + +**Why not Option B (subfolder, but generic backend folded into `apps/dreamverse/server/`):** + +- Other consumers (Dynamo, future streaming clients) need the backend without the Dreamverse product. Folding the backend under `apps/dreamverse/server/` would force Dynamo to either depend on `apps/` paths (ugly) or carry a fork. + +**Implications:** + +- [integration-review.md](integration-review.md) is **deprecated** (banner header + reading-guide demotion). Kept in tree for drift audit + OSS precedent reference. +- [integration-plan.md](integration-plan.md) is the **canonical executable plan** with 7 phases (Phase 0: land #1288; Phase 1: skeleton + tooling; Phase 2: backend move; Phase 3: FE move; Phase 4: promote generic-pending; Phase 5: prompt enhancer fork retirement; Phase 6: CI/release cutover; Phase 7: archive Dreamverse repo). +- Dreamverse repo will be **archived** at end of Phase 7 — not before. +- Dreamverse history does NOT migrate cross-repo via `git mv` (technical limitation); original history stays in archived Dreamverse repo, and Phase 2 PR body records the source SHA(s). +- New top-level `apps/` directory created — must be excluded from FastVideo PyPI wheel via `[tool.setuptools.packages.find] exclude = ["apps*", ...]`. +- Drift items from [integration-review.md](integration-review.md) get folded into specific phases of [integration-plan.md](integration-plan.md) (e.g. health routes → Phase 4, DR-1 → Phase 5). + +**Open questions deferred to phase planning:** + +- DR-2 (`cerebras_ifm`): public Literal vs Dreamverse-side custom provider — decide before Phase 5. +- VPO (`video_position_offset_sec` semantics): persistent vs per-segment — decide in Phase 4. +- Cross-repo history: fresh import vs `git subtree` import — decide before Phase 2. +- CORS / write-endpoint security policy: dev-only vs auth vs firewall — decide before Phase 6. + +### D-17: Abandon 6-PR split — land everything as single mega-PR #1288 + +**Status:** ✅ Resolved 2026-05-05. PR #1287 closed; PR #1288 opened on `will/ltx2_sr_port` covering the full chain. +**Source:** User decision after observing the post-#1286 rebase + re-slice cycle. + +**Question:** The original plan ([STACK.md](../../../STACK.md), [pr-roadmap.md](pr-roadmap.md)) called for the remaining `will/ltx2_sr_port` content (after PRs 7.5/7.6/7.7/7.8/7.9 landed) to ship as 6 stacked PRs: 7.10 (#1287, generate_async), 8 (server contract docs), LTX-2 SR runtime, NVFP4, post-fixes, agents-cleanup. PR #1287 was opened on 2026-05-05 as the first slice. Should the remaining 5 slices be opened sequentially as planned, or should everything be consolidated into one PR? + +**Decision:** Consolidate. Close #1287; open one mega-PR (#1288) on `will/ltx2_sr_port` covering all 34 commits / 71 files / +13,074 LOC at once. + +**Rationale:** + +- The post-#1286 rebase + re-slice cycle exposed real overhead: backup branch, interactive rebase with manual `drop` directives, force-push, re-slice 6 bookmarks, push next slice as new remote, open new PR, update memory dir. Repeating that 6 more times for the remaining slices accumulates substantial review-coordination overhead with diminishing structural benefit. +- The 6 layers are not independent in the way that landed PRs 7.5-7.9 were. PR 7.10 (`generate_async`) is the only API-shape change; PR 8 is docs+tests on top; LTX-2 SR / NVFP4 / post-fixes / agents-cleanup are feature/fix/docs work that doesn't shape the public API. Reviewing them as one ordered diff is at least as easy as reviewing 6 stacked PRs whose dependencies must be tracked manually. +- Single PR keeps CI / merge queue simpler and avoids the 6-PR cascade where every upstream merge invalidates the chain below it. + +**Implications:** + +- [STACK.md](../../../STACK.md) (top-level, 10-PR split tracker) is **deprecated**. Kept in tree as a historical artifact with the merged half (PRs 1-4 of the 10) accurate. Safe to delete in a follow-up. +- [authors.md](authors.md), [`CO-AUTHORS.md`](../../../CO-AUTHORS.md) — co-author roster is unchanged; trailers still apply per-commit on every commit in the consolidated PR. +- [runbook.md](runbook.md) — "After a PR merges (re-slice protocol)" section replaced by a simpler "After PR #1288 merges" section. +- Local split bookmarks (`will/api_7.10`, `will/api_8`, `will/ltx2_sr_runtime`, `will/ltx2_nvfp4`, `will/ltx2_post_fixes`, `will/agents_cleanup`) are no longer maintained; safe to delete locally. +- `origin/will/api_7.10` — pushed during the #1287 cycle; can be deleted on origin once #1287 close-cleanup completes. + +**Watch outs:** + +- The PR is large (71 files, +13,074 LOC). Reviewers will need commit-by-commit review; the PR body structures the layers in commit order to make this tractable. +- If #1288 becomes too large to merge cleanly later (e.g. main moves significantly underneath it), the fallback is to re-split — but the current expectation is to land it as-is. + +### D-12: `GpuPool` layer separation — keep distinct from `VideoGenerator` + +**Status:** ✅ Resolved (interim) + 🟡 Deferred long-term shape to PR 7.10. +**Source:** Oracle review on 2026-05-04, post-PR-#1257 merge. + +**Question:** Should `fastvideo.entrypoints.streaming.GpuPool` (PR #1257) be +folded into `fastvideo.entrypoints.video_generator.VideoGenerator`, or kept +separate? Three alternatives were evaluated: + +| Alt | Approach | Verdict | +|---|---|---| +| A | Status quo — `VideoGenerator` (single inference call) and `GpuPool` (multi-session orchestration) stay separate | ✅ Correct as **interim** | +| B | `VideoGenerator` absorbs the pool's role (`from_pretrained_pool`, `acquire/release/run`) | ❌ **Wrong layer.** Conflates execution with serving scheduler. | +| C | `GpuPool` becomes a thin **session-aware async executor** over PR 7.10's `generate_async` | ✅ Correct **long-term destination** | + +**Decision:** Alt A as interim; evolve toward Alt C once PR 7.10 lands +`generate_async`. Do NOT pursue Alt B. + +**Rationale:** + +- `VideoGenerator` is a library handle — "execute one request, possibly + across ranks via `MultiprocExecutor`/`RayDistributedExecutor`." +- `GpuPool` is serving infrastructure — "schedule N concurrent sessions + across N independent replicas, with sticky session-to-GPU affinity for + cache locality." +- These are different layers driven by different consumers (a Python + script doing `gen.generate(req)` vs. a WebSocket server with sticky + sessions). Folding them muddies both surfaces. + +**Key finding — `MultiprocExecutor` and `SubprocessGpuPool` are orthogonal, +not redundant:** + +| Layer | Job | Granularity | +|---|---|---| +| `MultiprocExecutor` (`fastvideo/worker/`) | TP/SP shard ONE inference call across N GPU ranks | per-call | +| `streaming_generator.py` (existing real-time path) | Per-frame streaming via `MultiprocExecutor.submit_step`/`get_result` | per-step within one generator | +| `SubprocessGpuPool` (`entrypoints/streaming/`, PR #1257) | Serve N concurrent sessions on N replicas, sticky-bound | per-session | + +Both spawn subprocesses because **CUDA contexts demand process boundaries**, +not because they solve the same problem. Sharing low-level lifecycle +utilities (process spawn, queue plumbing, shutdown) is a future refactor; +unifying the abstractions is wrong. + +**Sticky binding stays in the pool, NOT in `VideoGenerator`:** sticky +session-to-GPU affinity is a serving policy driven by LTX-2's per-GPU +continuation cache (last-9-decoded-frames + audio-latents). Different +consumers want different policies — stateless OpenAI HTTP wants +per-request leasing; LTX-2 streaming wants sticky affinity; per-frame +real-time streaming wants a continuous queue. Keeping policy in the pool +keeps `VideoGenerator` policy-free. + +**Specific risks flagged in PR #1257 (already merged):** + +| Risk | Mitigation (when relevant) | +|---|---| +| `GpuPool.run() -> Any` is sync — fine for whole-segment dispatch, blocks on cancellation | Replace with `run_async() -> AsyncIterator[VideoEvent]` in PR 7.10 cycle (`generate_async` makes this trivial) | +| `PoolAssignment.gpu_id: int` assumes one-GPU-per-worker | Don't lock as public API. Future may need `device_ids: list[int]` for topology-aware pooling (one worker = group of GPUs running internal `MultiprocExecutor`) | +| `GpuPool` could be documented as the canonical FastVideo serving API | Mark as **experimental / server-internal** in docstring until PR 7.10 lands. Don't include in user-facing API docs yet | +| Memory: N processes = N model replicas (~10-50 GB each) | Expected for concurrent serving with crash isolation. CUDA IPC weight sharing loses isolation; CPU-shared-memory loading helps host RAM not device. Real scalable path is topology-aware pooling later. | + +**Action items (carried into post-7.10 cycle):** + +- [ ] Update `GpuPool` ABC docstring to note "API may change post-PR-7.10" +- [ ] Plan to replace `run()` with `run_async() -> AsyncIterator[VideoEvent]` in PR 7.10 cycle +- [ ] Don't promote `gpu_id: int` to public API; revisit shape post-7.10 +- [ ] Consider clarifying field naming (e.g. `worker_id` is the stable identifier; `gpu_id` is current-impl detail) +- [ ] When opening 7.10's PR, have it consume `generate_async` from `GpuPool.run_async` end-to-end + +**Open thread it touches:** PR 7.10 (`open-threads.md` item D — generate_async) +unblocks Alt C and is the natural place to land the API shape change. + +### D-15: Streaming router (PR #1286) — keep in-repo, defer sticky / active-active + +**Status:** ✅ Resolved (interim). Pre-merge polishes applied. Three follow-up +items tracked. +**Source:** Oracle review on 2026-05-05, during PR #1286 review cycle. + +**Question:** Where should the multi-replica WebSocket router live? Should it +ship at all (vs. delegating to nginx/envoy)? Should sticky session routing +or weighted/round-robin balancing be in the initial PR? + +| Alt | Approach | Verdict | +|---|---|---| +| A | Status quo — `fastvideo/entrypoints/streaming/router/`, FastAPI-based, single-primary failover, lazy `httpx`/`websockets` imports | ✅ **Keep** | +| B | Move to separate package `fastvideo-router/` | ❌ **Premature** — adds packaging/release/compat overhead before evidence of independent adoption | +| C | Fold router into the streaming server itself (one app, mode flag) | ❌ Conflates router/generator lifecycles, mode-dependent config, drags inference deps into routing deployments | +| D | Replace with reverse proxy (nginx/envoy/HAProxy) recipes | ❌ Not as the SOLE answer — mature proxies don't naturally emit FastVideo typed `gpu_unavailable` frames or evolve with FastVideo session semantics. Recommend external proxies as a complement at high scale. | +| E | Add sticky session routing now | ❌ **Defer** — implementing correctly depends on where `session_id` is available (URL/header is easy, first JSON frame is invasive). Reconnects are rare today. | +| F | Add weighted / round-robin now | ❌ **Defer** — active-active without sticky routing is worse for LTX-2 continuation locality than active-passive failover | + +**Decision:** Alt A — keep current shape. Apply pre-merge polishes; preserve +forward-compat for sticky routing. + +**Rationale:** + +- Python router is justified as a FastVideo-aware control-plane component, + not a replacement for Envoy/HAProxy. It can emit typed + `gpu_unavailable` frames, evolve with FastVideo session semantics, + and ship local/dev deployment without ceremony. +- The current abstraction is small + testable: `RouterConfig`, + `ReplicaRegistry`, `ReplicaStatus`, `HttpProbe` (Protocol/structural alias). + Adding strategy registries / telemetry interfaces / active-active policies + now would be over-engineering. +- Active-passive (single primary) is the right MVP for LTX-2 streaming — + preserves continuation cache locality (D-12 sticky binding rationale) + better than naive active-active. +- The biggest architectural risk isn't placement; it's accidentally baking + in unstated semantics. Define single-primary behavior + config validation + now so future active-active or sticky routing becomes additive. + +**Pre-merge polishes applied (per gemini + Oracle review):** + +| # | What | Why | +|---|---|---| +| 1 | `ReplicaRegistry.select()` docstring rewrite | gemini flagged "round-robin via insertion order" claim was misleading — implementation always returns `[0]`. Replaced with explicit "first healthy primary, else first healthy non-primary; this MVP picks first match within tier; round-robin/weighted deferred". | +| 2 | Refactored `run_health_check_loop` to share single `httpx.AsyncClient` across the loop's lifetime via `_build_default_probe()` async context manager | gemini flagged per-probe client instantiation as inefficient. With ~1 probe/second default polling, TCP/TLS handshake overhead is non-trivial; now reuses connection. Tests inject probes directly so the path stays bypassable. | +| 3 | Probe all replicas concurrently per cycle via `asyncio.gather(..., return_exceptions=True)` | gemini flagged sequential probes risk falling behind `health_check_interval_seconds` if replicas time out. Now per-cycle wall time = max(probe latencies), not sum. | +| 4 | `RouterConfig.__post_init__` validation | Oracle recommended: empty replicas, non-positive intervals/timeouts, thresholds < 1, non-`http(s)://` URLs, and >1 primary all `raise ValueError`. Surfaces misconfiguration at config-load instead of confusing runtime failures. | +| 5 | Migrated `@app.on_event("startup"/"shutdown")` to `@contextlib.asynccontextmanager`-based `_lifespan()` | Pre-merge — FastAPI deprecated the old API. Was tracked as the 7.9 caveat in pr-roadmap.md. | + +**One review comment intentionally not implemented:** + +| Comment | Decision | +|---|---| +| gemini medium: `_load_router_config` duplicates `fastvideo.api.parser.parse_config` logic | Kept manual flat-from-nested mapping. The YAML schema has nested `health_check:` block but `RouterConfig` is flat; using `parse_config` directly would require either restructuring `RouterConfig` to have a nested `HealthCheckConfig` (schema change beyond this PR's scope) or accepting incomplete parsing. Manual mapping is intentional and well-typed. | + +All 4 review threads marked resolved on the GitHub PR. + +**Action items (deferred):** + +- [ ] Track sticky session routing extensibility — when needed, add + `ReplicaRegistry.select(routing_key: str | None = None)` so registry + evolution is additive; document upfront where `session_id` should + appear (URL/header preferred over first JSON frame to avoid + buffering/peeking) +- [ ] Track `_bridge_session()` backpressure note — fine for MVP because + `websockets` library provides basic transport backpressure, but at + high scale add max_size/timeouts or recommend Envoy/HAProxy in front +- [ ] If active-active multi-primary becomes a requirement, define + behavior (round-robin within healthy primaries, weighted, sticky-by-key) + rather than letting `select()` silently pick `[0]` + +**Watch outs:** + +- `session_id` in WebSocket URL/headers is the cleanest sticky-routing + hook. If it ends up only in the first JSON message, sticky routing + later will require buffering/peeking before backend selection. +- Multi-primary configs are now explicitly rejected by validation; + documented + enforced. +- `_bridge_session()` is fine for MVP (the libraries provide basic + backpressure), but not production-grade for edge load. Document the + limit. + +**Open thread it touches:** open-threads.md items #13 (sticky routing), +#14 (bridge backpressure), #15 (multi-primary semantics). + +### D-16: Streaming router polish round 2 — second-pass fixes on top of D-15 + +**Status:** ✅ Resolved. Applied as `[fix] streaming: router polish — bridge +cancel + state machine + deps` (`a152cb77` on `will/api_7.9`, `40e265b8` on +`will/ltx2_sr_port`). +**Source:** Second-pass review on PR #1286, 2026-05-05, after D-15's pre-merge +polishes landed. + +**Question:** D-15 closed the structural review (placement, sticky/active-active +deferral, basic `__post_init__` validation). On a second pass through the same +files, five latent issues surfaced that weren't covered by gemini's first pass +or Oracle's structural review. Apply them on top of the merged D-15 polishes, +or queue for a follow-up PR? + +**Decision:** Apply on top of `will/api_7.9` directly. All five are bug-class +or DX-class — none are scope-expanding architecture changes — so folding them +into PR #1286 keeps the router landing in one reviewable unit instead of +shipping a router PR plus an immediate follow-up fix PR. + +**Fixes applied:** + +| # | File | What | Why | +|---|---|---|---| +| 1 | `router/main.py::_bridge_session` | Replaced `asyncio.gather()` with `wait(FIRST_COMPLETED)` + explicit `cancel()`/drain + `_is_normal_disconnect()` classifier | `gather` waited for both directions; on client disconnect, the backend-reader task leaked and stayed pending. Backend `ConnectionClosed` also surfaced as an unhandled exception in server logs. New shape: first task to finish triggers explicit cancel of the other, both are drained, and only non-routine exceptions re-raise. | +| 2 | `router/registry.py::record_success` | Split state transitions: `UNKNOWN -> HEALTHY` is now immediate on first successful probe; only `UNHEALTHY -> HEALTHY` remains gated by `recovery_threshold` | Previously a fresh registry needed `recovery_threshold` consecutive successes before any replica was selectable. With default `recovery_threshold=2` and `health_check_interval=1s`, that meant 2-3s of `gpu_unavailable` rejections at startup. Now the first probe promotes immediately; recovery gating still protects against flapping replicas. | +| 3 | `router/registry.py::_build_default_probe` | Missing `httpx` now raises `RuntimeError` with install hint instead of yielding a "disabled" probe stub | Previous behavior: silently returned `(0.0, "httpx not installed; ...")` for every probe, which `record_failure` then folded into `UNHEALTHY` after `failure_threshold` cycles. Operators saw replicas drop UNHEALTHY with a confusing reason and no clear remediation. Hard-fail at startup is the right surface. | +| 4 | `router/config.py::__post_init__` | Extended D-15 polish #4 with: rejects `urlparse(url).path not in ("", "/")`, rejects `query`/`fragment`, rejects duplicate URLs across replicas | D-15's validation rejected non-`http(s)://` URLs and >1 primary; it didn't catch `http://host/api` (the router appends `/health` and `/v1/stream` itself, so a base-URL with path yields malformed routes) or `[{url: x}, {url: x}]` (replica registry keys by URL — duplicates would silently collapse to one entry, masking the misconfiguration). | +| 5 | `cli/router_serve.py::_load_router_config` | Replaced silent list-comprehension filter (`for r in replicas_raw if isinstance(r, dict) and r.get("url")`) with per-index `raise ValueError` | Original parser silently dropped malformed YAML entries. A single typo in `replicas[2].url` would yield 2 replicas instead of 3 with no log line. New shape: explicit per-index error message ("missing required key 'url'", "must be a mapping"). | +| 6 | `pyproject.toml::[streaming]` extra | Added `websockets` as explicit dep | `router/main.py::_bridge_session` does `import websockets` lazily and raises `RuntimeError` if missing. The `[streaming]` extra was an implicit transitive — anyone installing only `[streaming]` (and not the broader requirements) hit the runtime error. Now explicit. | + +**Tests added (7 cases in `fastvideo/tests/entrypoints/streaming/test_router.py`):** + +- `TestUnknownToHealthyImmediate.test_first_success_promotes_unknown` — first probe success transitions `UNKNOWN -> HEALTHY` regardless of `recovery_threshold` +- `TestUnknownToHealthyImmediate.test_unhealthy_recovery_still_gated_by_threshold` — `UNHEALTHY -> HEALTHY` still requires `recovery_threshold` successes +- `TestConfigValidation.test_rejects_path_in_url` / `test_rejects_query_in_url` / `test_rejects_fragment_in_url` / `test_rejects_duplicate_urls` / `test_accepts_trailing_slash` — `__post_init__` URL validation matrix + +**Verification:** 17/17 router tests pass on both branches. `pre-commit run` +clean (yapf / ruff / codespell / mypy). `lsp_diagnostics` clean on changed +regions; the one pre-existing `Task` generic-type warning at `main.py:37` is +unrelated and predates this commit. + +**In-flight pre-commit corrections (not part of the 6 fixes themselves):** + +- yapf auto-reformatted 4 files (kept verbatim). +- ruff `UP038`: rewrote `isinstance(exc, (CancelledError, WebSocketDisconnect))` + to `isinstance(exc, CancelledError | WebSocketDisconnect)`. +- mypy `[misc]`: renamed loop var `exc` (inside `for task in done`) to + `task_exc` to avoid name collision with the outer + `except ImportError as exc` binding. + +**Open thread it touches:** None new. Item #14 (bridge backpressure) and +item #13 (sticky routing) from D-15 remain deferred — this round addressed +**cancellation/disconnect** semantics on the bridge, which is distinct from +**throughput backpressure**. Item #14 still applies: at higher load, add +`_bridge_session()` max-size + timeout limits or recommend Envoy/HAProxy +in front. + +### D-14: Streaming auxiliaries (PR #1284) — cohesion + concrete-vs-Protocol scoping + +**Status:** ✅ Resolved (interim). Two polish items applied during review; one +operational caveat tracked. +**Source:** Oracle review on 2026-05-04, during PR #1284 review cycle. + +**Question:** Is PR #1284's bundle of 4 streaming-server auxiliary modules +(`prompt/safety.py`, `prompt/rewrite.py`, `session_logger.py`, +`mock_server.py`) correctly scoped? Should `mock_server` live in production +module path? Should `PromptSafetyFilter` be a Protocol? Should the bundle +have been split into 4 PRs? + +| Alt | Approach | Verdict | +|---|---|---| +| A | Status quo — single PR, 4 modules under `streaming/`, mock_server in production path, concrete safety filter | ✅ **Keep** | +| B | Split into 4 separate PRs | ❌ Process overhead, not architectural improvement | +| C | Move `mock_server.py` into `tests/` | ❌ Would reduce discoverability + install-time usability of `python -m fastvideo.entrypoints.streaming.mock_server` | +| D | Move `session_logger.py` to `streaming/observability/` (or top-level `fastvideo/observability/`) | ❌ Premature — currently session-shaped + streaming-specific; promote when a non-streaming consumer appears | +| E | Convert `PromptSafetyFilter` to Protocol (like `LLMProvider`) | ❌ Premature abstraction — only one classifier exists; small duck-typed surface preserves future Protocol introduction without breaking the concrete | +| F | Convert `MockGenerator` to Protocol | ❌ Same — small duck-typed surface; no second mock generator exists | + +**Decision:** Alt A — keep current shape. Apply two polish items from +Oracle's review before merge. + +**Rationale:** + +- "Streaming-server auxiliaries" is cohesive enough at 730 LOC with + isolated modules + tests. Each module has independent code path but + shared deployment context (the streaming server boots them all). +- `mock_server.py` in production path is a strength: reuses + `build_app()` for protocol parity. Hiding it under `tests/` would lose + `python -m fastvideo.entrypoints.streaming.mock_server` CLI access for + FE devs. +- Concrete `PromptSafetyFilter` matches "ship what we have, abstract + later" pattern. Internal had multi-classifier composition; public + ships single + leaves chaining as a Dreamverse-side concern (per D-2). +- Same pattern for `MockGenerator`: small duck-typed `_GeneratorLike` + surface lets a second mock implementation drop in without inheritance. +- `threading.Lock` (not `asyncio.Lock`) in `session_logger.py` is + correct — writes come from real encoder/control threads via + `run_in_executor`, not from coroutines directly. `asyncio.Lock` would + be the wrong primitive for cross-thread concurrency. + +**Pre-merge polishes applied (per Oracle):** + +| Polish | What | Why | +|---|---|---| +| 1 | Removed `RewriteOptions.user_system_prompt_override` | Inert public field — was declared but never threaded through to `enhancer.rewrite()`. Shipping unused public options is more likely to bite than any structural choice. Re-add when actually wired through. | +| 2 | Sanitized `session_id` filename in `session_logger.SessionLogger._get_file()` | Defense-in-depth: today session_id is server-generated UUID, but a future code path that accepts client-supplied ids would otherwise allow path traversal via `../`. Added `_FILENAME_SANITIZE_RE = re.compile(r"[^A-Za-z0-9._-]")` + sub before `os.path.join`. | + +**Operational caveat tracked (not a code change):** + +- `SafetyDecision.UNAVAILABLE` is treated as `ALLOW` by callers — a + policy choice that's correct for an opt-in safety filter, but + callers should log loudly so operators know the filter is degraded. + Tracked as open-threads.md item #12. + +**Pre-merge review feedback (4 of 4 resolved on the GitHub PR):** + +| # | File:Line | Severity | Issue | Fix applied | +|---|---|---|---|---| +| 1 | `session_logger.py:57` | High | `log()` race vs `close()` — `KeyError` on `_locks[session_id]` | Atomic capture in `_get_file()`; master `_registry_lock`; `with lock, contextlib.suppress(ValueError):` | +| 2 | `rewrite.py:71` | Medium | `re.compile()` in hot path | Module-level `_LEADING_MARKER_RE`, top-level `import re` | +| 3 | `safety.py:105` | Medium | `_ensure_loaded()` race on concurrent fastText load | `_load_lock = threading.Lock()` + double-check pattern | +| 4 | `pyproject.toml:145` | Medium | `streaming` extra missing `prompt-safety` | Added to aggregator | + +All 4 review threads marked resolved via GraphQL `resolveReviewThread`. + +**Action items (deferred):** + +- [ ] Track `SafetyDecision.UNAVAILABLE` log loudness in + open-threads.md item #12 — when streaming server starts using the + safety filter, ensure operator-visible logging on `UNAVAILABLE` + results +- [ ] If a second safety classifier appears (Perspective API, Detoxify, + custom rules), promote `PromptSafetyFilter` to a Protocol — same + pattern as `LLMProvider` per D-13 +- [ ] If a second mock generator appears (different frame patterns, + different latency models), promote `MockGenerator` to a Protocol + +**Open thread it touches:** PR #1284 itself; future safety-classifier +Protocol promotion; future observability module extraction. + +### D-13: Prompt enhancer / `LLMProvider` abstraction shape — keep streaming-scoped + +**Status:** ✅ Resolved (interim) + 🟡 Three deferred polishes after metrics or 2nd consumer. +**Source:** Oracle review on 2026-05-04, pre-PR-#1258-merge. + +**Question:** Is PR #1258's `fastvideo.entrypoints.streaming.prompt.*` module +correctly designed? Should it be (a) Protocol-based vs ABC, (b) under +`streaming/` vs top-level `fastvideo.prompt.*`, (c) closed 3-op enum vs +open `complete()` API? + +| Alt | Approach | Verdict | +|---|---|---| +| A | Status quo — `streaming/prompt/*`, Protocol provider, fixed 3 ops, lazy `httpx`, per-call `AsyncClient` | ✅ **Keep** | +| B | Move to top-level `fastvideo.prompt.*` (decouple from streaming) | ❌ **Premature.** No second consumer exists yet. | +| C | Convert `LLMProvider` Protocol → ABC with default impls + retry classification | ❌ **Wrong direction.** Biases extension toward OpenAI shape; `_openai_compat.py` already factors that as helper not inheritance. | + +**Decision:** Alt A as interim. Promote to Alt B only when a second +non-streaming consumer (OpenAI server, batch generation, tooling) actually +needs the prompt enhancer. Don't pursue Alt C. + +**Rationale:** + +- Public contract is tiny — `name: str` + `async complete(LLMRequest) -> LLMResponse`. ABC adds zero value. +- `_openai_compat.py` is the right place for shared logic — helper, not base class. Anthropic / local / custom providers stay first-class. +- The 3 ops (enhance / auto_extend / rewrite) are LTX-2 streaming concepts. `auto_extend` (continue prompt sequence) and `rewrite` (multi-line alternatives) come directly from session UX. Calling this "the FastVideo prompt API" misrepresents that. + +**Specific risks flagged in PR #1258 (already merged-pending review):** + +| Risk | Mitigation (when relevant) | +|---|---| +| API publicity — calling this "the FastVideo prompt API" before a second consumer exists | Document module as "streaming-server prompt enhancement" in user-facing docs; keep it nested under `entrypoints/streaming/` | +| `httpx.AsyncClient` per-call (no connection pooling) | Acceptable for ~6-10 calls per LTX-2 session; LLM latency dominates. Add optional `client_factory` parameter LATER if metrics show connect overhead is meaningful. | +| 3 fixed operations could constrain future generic use | Closed enum is right for application-level orchestration. Future generic consumers should either call `provider.complete()` directly, or get a thin separate enhancer that shares the provider/fallback machinery. | +| `register_provider(priority=-1)` semantics rely on Python's negative-index `list.insert` | Cosmetic concern; docstring is clear. Could be tightened to explicit branch later. | +| `runtime_checkable` Protocol with `name: str` instance attribute — static type checkers may miss missing `name` | Acceptable; runtime check via `isinstance(p, LLMProvider)` works for plugin discovery. | + +**Action items (deferred):** + +- [ ] Document `fastvideo.entrypoints.streaming.prompt.*` as streaming-scoped in user-facing docs (PR 12 docs migration); avoid promoting as framework-level +- [ ] Add optional `client_factory` parameter to providers when metrics justify pooling +- [ ] Plan future move to `fastvideo.prompt.*` (with import shim) when second non-streaming consumer materializes +- [ ] Track Q-2 reactivation: promote LTX-2 prompt orchestration (locked segments, segment-prompts JSON parsing) to public `fastvideo.entrypoints.streaming.prompt.ltx2_orchestration` when a second LTX-2-style consumer appears + +**Open thread it touches:** Dreamverse migration (open-threads.md DR-1) +will be the first real test of the public surface. Lessons learned there +inform whether Alt B becomes feasible. + +## D-decisions (from `dreamverse_review.md`, Apr 26) + +### D-1: Realtime runtime → streaming GpuPool migration shape + +**Status:** ✅ Resolved. + +Internal `RealtimeRuntimeConfig` had a multi-model registry + +flattened sampling defaults. Public `SubprocessGpuPool` is single-model ++ uses per-request `SamplingConfig`. + +**Decision:** Drop multi-model registry on integration branch (not used +in production). Construct `GeneratorConfig` for chosen model and pass to +`SubprocessGpuPool`. Move sampling defaults to a server-side +`default_request: GenerationRequest` template. + +**Risk:** Migration branch surfaces missing-model errors if a flow +silently relied on registry to swap models per-session. Integration +tests exercise at least one segment per supported model id before +merging. + +### D-2: PR 7.7 prompt enhancer API surface narrower than internal + +**Status:** ✅ Resolved. + +Public `PromptEnhancer.enhance/auto_extend/rewrite` returns +`LLMResponse(content, provider, model, latency_ms, fallback_used)`. +Internal returns `EnhanceResult(prompt, fallback_used, error, ...)` / +`RewriteResult(prompts, ..., rollout_id, rollout_label, ...)`. + +**Decision:** Adapt at the call site via +`Dreamverse/server/prompting/_internal_compat.py` shim. Locked-segment / +next-segment-index plumbing stays Dreamverse-side. Public stays minimal +and provider-agnostic. + +**Open question (Q-2):** Promote LTX-2-specific orchestration into +`fastvideo.entrypoints.streaming.prompt.ltx2_orchestration` once a +second consumer appears. Logged for future review. + +### D-3: Multi-stage provider race vs. sequential fallback + +**Status:** ✅ Resolved (public stays sequential). + +Internal enhancer runs all providers in a stage in parallel +(`_run_provider_race`). Public enhancer runs sequentially with +retryable-error fallback. + +**Decision:** Public stays sequential for PR 7.7. Race is a +Dreamverse-specific tail-latency optimization that depends on parallel +API budgets. + +**Risk / Q-3:** First-segment latency on Dreamverse may regress +slightly when Cerebras has a bad minute (sequential waits 20s before +trying Groq). If real production concern, add public +`concurrency: int = 1` knob behind a race path — but only after measuring. + +### D-4: Skip PR 7.9 router for the integration branch + +**Status:** ✅ Resolved. + +Internal stack ships `router/main.py` for multi-replica load balancing. +Dreamverse deployment uses single replica per region. + +**Decision:** Land PR 7.9 publicly (upstream the surface). Skip wiring +into Dreamverse integration branch. Dreamverse's `server/main.py` does +not import from `router/`. + +### D-5: Audio re-encode (PR 7.10) needed for streaming, deferred + +**Status:** 🟡 Deferred to PR 7.10. + +Internal streaming server's per-step path runs `_re_encode_audio` inside +`_stream_av_fmp4_events` so each fMP4 segment ships with +continuation-conditioning audio. Whole-segment `pool.run()` path doesn't +need this. + +**Decision:** Land PR 7.10's `generate_async` publicly. Dreamverse +integration branch initially keeps using `pool.run()` (whole segment, no +re-encode). Follow-up branch swaps to `generate_async` + audio re-encode. + +**Open question (Q-5):** Acceptable for first switch, or does +Dreamverse audio quality regress vs. internal until 7.10 wires in? + +### D-6: `realtime/local_runtime.py` is NOT upstreamed + +**Status:** ✅ Resolved. + +It was the FastVideo-internal precursor to `streaming.gpu_pool`. +Upstreaming both would create two GPU pool implementations in public. + +**Decision:** Don't upstream `realtime/local_runtime.py`. Dreamverse +switches to `streaming.gpu_pool.SubprocessGpuPool` on integration +branch. Internal module can be deleted at follow-up. + +### D-7 / Q-6: `FP4Config` is private-only + +**Status:** ✅ **Resolved May 2.** + +April 26: `Dreamverse/server/video_generation.py:271` imported +`fastvideo.layers.quantization.fp4_config.FP4Config` from +FastVideo-internal only. The 411-line module hard-imported `flashinfer`. + +**Two options at the time:** + +1. Colocate publicly with `flashinfer` as optional extra + `pip install fastvideo[fp4]`; refactor `FP4QuantizeMethod` to take + layer-prefix list from a pipeline-config field instead of hardcoding + ltx2 paths. +2. Keep private — Dreamverse imports from internal via thin shim. + +**Recommendation at the time:** option 1 once API refactor settles. + +**Resolution:** May 2 work chose option 1. +- `365a66c7` upstreamed FP4Config with lazy `flashinfer` import in + loader helper (no public hard-dep) +- `94c983a2` renamed FP4 → NVFP4 to disambiguate from MX-FP4 / OCP-FP4 +- `42b30bf9` wired through `fastvideo.layers.quantization` + +See [quantization.md](quantization.md) for full details. + +### D-8: `ltx2_image_crf` silently dropped by public schema + +**Status:** 🔴 **Unverified post-`d80c2a8`.** + +April 26: Dreamverse's `server/video_generation.py:406` passed +`ltx2_image_crf=0.0` to `SamplingParam(...)`. Public +`fastvideo.api.sampling_param.SamplingParam` did NOT have this field; +the BE logged ERROR and silently dropped the kwarg. + +**Migration target** (per [design.md](design.md) compatibility map): +`request.stage_overrides.refine.image_crf`. + +**Resolution status:** `d80c2a8` (May 2) refactored +`server/video_generation.py` to use typed `GeneratorConfig` + +`preset_overrides["refine"]`. Whether this PR routed `image_crf` +through the typed `stage_overrides` path or left it silently dropped is +unverified. See [open-threads.md](open-threads.md). + +### D-9: `aarch64-conda-linux-gnu-cc` triton compile failure + +**Status:** ✅ Resolved (operational). + +Conda env injected an ARM cross-compiler ahead of `gcc` on `$PATH`, so +`torch._inductor`'s triton launcher failed compilation. Setting +`ENABLE_TORCH_COMPILE=0` bypasses it. + +**Long-term fix:** clean conda env's compiler shadowing or add +`CC=gcc` override in Dreamverse's worker bootstrap. + +### D-10: Warmup OOM on shared GPU + +**Status:** ✅ Resolved (operational). + +When `CUDA_VISIBLE_DEVICES` lands on a GPU another tenant uses, LTX-2 +warmup fails with OOM. Picking an idle GPU (4-7 in test setup) is a +manual step. + +**Improvement:** pre-warm probe that checks free memory before booting +the pool would prevent this. + +### D-11: ffmpeg fragment write `Broken pipe` + +**Status:** ✅ Resolved (cosmetic). + +When WS client closes before backend finishes streaming first segment, +ffmpeg hits `[Errno 32] Broken pipe`. Currently propagates to +"User step failed". Cosmetic — swallowing pipe-broken on intentional +disconnect would clean up logs. + +## Q-questions (from `streaming-server-upstream-plan.md`, Apr 17) + +### Q-1: Router placement (in-repo or separate package) + +**Status:** ✅ Resolved (in-tree). + +**Recommendation at the time:** separate package `fastvideo-router/` or +`fastvideo/contrib/router/`; defer final call to PR 7.9. + +**Resolution:** PR 7.9 implementation places router in-tree at +`fastvideo/entrypoints/streaming/router/`. + +### Q-2: Session ID authority + +**Status:** ✅ Resolved (server-generated). + +**Recommendation:** server-generated UUID; accept externally provided +session ID only for resume flows. + +### Q-3: Torch compile kwargs typing (opaque vs full vs hybrid) + +**Status:** ✅ Resolved (hybrid). + +**Recommendation:** hybrid — type the common four (`backend`, +`fullgraph`, `mode`, `dynamic`) + allow `extras: dict[str, Any]`. + +**Resolution:** PR 6 + NVFP4 `221cb20a` shipped exactly this hybrid. + +### Q-4: Prompt safety / fasttext dependency + +**Status:** ✅ Resolved (optional extra). + +**Recommendation:** ship as optional extra `pip install fastvideo[prompt-safety]`. + +**Resolution:** PR 7.8 implements as optional extra. + +### Q-5: Audio-specific tensor payloads in continuation + +**Status:** ✅ Resolved (typed `LTX2ContinuationState`). + +`ltx2_audio_clean_latent`, `ltx2_audio_denoise_mask`, +`ltx2_audio_latents` not in pre-refactor public schema. + +**Recommendation:** classify as opaque fields inside +`LTX2ContinuationState.payload`, not top-level sampling fields. + +**Resolution:** PR 7's typed `LTX2ContinuationState` lifts these into +typed fields (see [cross-repo-surfaces.md](cross-repo-surfaces.md) +field mapping table). + +### Q-6: Dynamo subpackage home + +**Status:** ✅ Resolved (lives in Dynamo repo). + +**Resolution:** No Dynamo code in FastVideo. Full backend package +(handler, adapter, registration, health check) owned by Dynamo repo at +`components/src/dynamo/fastvideo/`, same pattern as vllm/sglang. +FastVideo only guarantees the public API contract. + +### Q-7 (was Q-6 in dreamverse_review): How to land FP4Config publicly + +**Status:** ✅ Resolved May 2 — option 1 (colocate publicly). + +See D-7 above. + +### Q-8: Disaggregation readiness contract test + +**Status:** 🟡 Recommended; not yet shipped. + +PR ai-dynamo/dynamo#7544 is aggregated-only. `ContinuationState` hybrid +already supports future prefill/decode split. + +**Recommendation:** PR 7.10 explicitly validate `ContinuationState` +survives round-trip through Dynamo-style RPC (pickle or JSON), even +though Dynamo isn't using it today. Cheap regression guard. + +### Q-9: Dynamo progress/status passthrough + +**Status:** 🟡 Deferred until Dynamo clarifies. + +`NvVideosResponse` has `status` and `progress` fields. + +**Recommendation:** PR 7.10 stays aggregated-final-only to match PR +#7544 shape; revisit after Dynamo clarifies their streaming/progress +semantics. + +## Cross-doc questions still 🔴 OPEN + +These need decisions; tracked also in [open-threads.md](open-threads.md): + +| ID | Question | Source | Why it matters | +|---|---|---|---| +| **D-8** | Did `d80c2a8` route `ltx2_image_crf` correctly, or is it still silently dropped? | dreamverse_review | Latent silent-drop bug; FP4-disabled paths may degrade | +| **VPO** | `video_position_offset_sec` — persistent accumulation (a) vs per-segment hint (b) | dreamverse_integration | Needs decision before PR 7.6 emits state | +| **SBS** | `SessionStore` / `BlobStore` lifecycle (TTL/eviction/blob-drop on state replacement) | dreamverse_integration | Needs decision in PR 7.5 design pass | +| **#1** | Migrate `/healthz`+`/readyz`+`/status` into FastVideo `build_app` | streaming-upstream-plan + handoff | Closes BE_FLAVOR=fastvideo FE-compatibility | +| **#3** | Add `cerebras_ifm` to public `PromptEnhancerConfig.provider` Literal | handoff | Internal supports it; public schema doesn't | +| **#4** | Expose `layer_profile` on typed `engine.quantization` | handoff | Removes Dreamverse's `experimental["pipeline_config"]` dodge | +| **#5** | Typed `dit_config.quant_config` carrier (design TBD) | handoff | Eliminates the `experimental["pipeline_config"]` escape hatch entirely | diff --git a/.agents/memory/dreamverse-integration/design.md b/.agents/memory/dreamverse-integration/design.md new file mode 100644 index 0000000000..a0d0e00c8b --- /dev/null +++ b/.agents/memory/dreamverse-integration/design.md @@ -0,0 +1,332 @@ +# Design — Typed Public Inference API + +Synthesis of the FastVideo public inference API refactor design philosophy. +For PR-by-PR execution see [pr-roadmap.md](pr-roadmap.md). For the streaming +extension see [streaming-server.md](streaming-server.md). + +**Last updated:** 2026-05-03. + +## Why the refactor + +The pre-refactor public boundary mixed three concerns through `**kwargs`: + +- `VideoGenerator.from_pretrained(..., **kwargs)` mixed engine/runtime, + pipeline init, and component overrides. +- `VideoGenerator.generate_video(..., **kwargs)` mixed prompt+inputs, + sampling, output, and model-specific workflow knobs. +- Unknown keys silently filtered or merely logged → API drift hard to detect. +- Multi-stage models (LTX-2 two-stage, Hunyuan15 SR, LongCat distill+refine) + exposed via ad hoc top-level flags. + +This was already painful for LTX2/Dreamverse and would worsen as more +multi-stage pipelines came in. + +## Core decision + +FastVideo has: + +1. **Typed nested public schema** — `RunConfig`, `ServeConfig`, + `GeneratorConfig`, `GenerationRequest`, `ContinuationState`. +2. **Model-owned named pipeline presets** — `ltx2_two_stage`, + `longcat_distill_refine`, `hunyuan15_sr_1080p`, etc. All 13 model families + landed presets in PR 4. +3. **Semantic stage overrides by stage name** — + `request.stage_overrides["refine"] = LTX2RefineStageOverride(...)`. +4. **Optional advanced explicit plans** for power users — `GenerationPlan` + (escape hatch only; not the canonical surface). +5. **YAML-first CLI** with dotted overrides — + `fastvideo generate --config run.yaml --request.sampling.seed 42`. + +The canonical user experience: choose a model → choose a preset → override +a few typed fields → generate. Dicts/YAML/JSON are supported as +serialization, but parse immediately into typed objects with strict +unknown-key validation. + +## Schema surface + +Implemented in [`fastvideo/api/`](file:///home/william5lin/FastVideo/fastvideo/api/): + +| Type | Role | +|---|---| +| `RunConfig` | Offline envelope: `generator` + `request` | +| `ServeConfig` | Serving envelope: `generator` + `server` + `default_request` + optional `streaming` | +| `GeneratorConfig` | `model_path`, `revision`, `trust_remote_code`, `engine`, `pipeline` | +| `EngineConfig` | parallelism / offload / compile / quantization / flags | +| `PipelineSelection` | `workload_type`, `preset`, `preset_version`, `components`, `preset_overrides`, `experimental` | +| `GenerationRequest` | `prompt`, `negative_prompt`, `inputs`, `sampling`, `runtime`, `output`, `stage_overrides`, `state`, `plan`, `extensions` | +| `ContinuationState` | Opaque envelope `{kind: str, payload: dict[str, Any]}` | +| `GenerationPlan` | Advanced/escape-hatch only; `{stages: list[PlannedStage], final_stage: str|None}` | + +Files: + +| File | Role | +|---|---| +| [`schema.py`](file:///home/william5lin/FastVideo/fastvideo/api/schema.py) | All public dataclasses | +| [`parser.py`](file:///home/william5lin/FastVideo/fastvideo/api/parser.py) | `from_dict`, `to_dict`, `load_yaml`, `load_json`, validation | +| [`overrides.py`](file:///home/william5lin/FastVideo/fastvideo/api/overrides.py) | Dotted override application | +| [`compat.py`](file:///home/william5lin/FastVideo/fastvideo/api/compat.py) | Legacy kwargs translation (~370 lines, scheduled for death PRs 14-17) | +| [`presets.py`](file:///home/william5lin/FastVideo/fastvideo/api/presets.py) | Preset registry | +| [`sampling_param.py`](file:///home/william5lin/FastVideo/fastvideo/api/sampling_param.py) | Internal `SamplingParam` adapter (canonical home since PR 4) | +| [`results.py`](file:///home/william5lin/FastVideo/fastvideo/api/results.py) | `GenerationResult` / `VideoResult` | +| [`errors.py`](file:///home/william5lin/FastVideo/fastvideo/api/errors.py) | Path-aware validation errors | + +## Boundary normalization rule + +Every public inference entrypoint normalizes into typed config objects +before touching legacy internals (`FastVideoArgs`, `SamplingParam`). +Includes Python constructors, `generate*` calls, CLI `generate`, CLI +`serve`, OpenAI server request translation, streaming server request +translation. + +Legacy internals (`FastVideoArgs`, `SamplingParam`) may remain temporarily, +but only behind a typed normalization boundary. + +## Strict-by-default validation + +All structured inputs are strict: + +- Unknown keys → error +- Wrong types → error +- Invalid stage names → error +- Incompatible state/preset combinations → error + +The only intentional escape hatches: + +- `generator.pipeline.experimental` — for in-flight features without typed home +- `request.extensions` — same, request-side + +These bypass validation by design. Intent: shrink as presets absorb +model-specific fields. New fields should not land in `experimental` / +`extensions` without a plan to either promote them to typed fields or +remove them within two PR cycles. + +Error format includes nested path: + +``` +Invalid field: request.stage_overrides.refine.num_inference_steps +Expected int, got "two" +Preset: ltx2_two_stage +Stage: refine +``` + +## Request mutation tracking + +When a `GenerationRequest` is parsed from raw dict (YAML/JSON/Python), +FastVideo tracks which fields the user explicitly provided vs. which got +schema defaults. Matters for `request_to_sampling_param()` — explicit +values override model defaults; schema defaults do NOT. + +Mechanics: + +- At parse time, original raw dict + baseline snapshot stored on the request. +- Dataclass field mutations (e.g. `request.sampling.seed = 7`) captured via + lightweight `__setattr__` dirty-path recording. +- Dict-typed field mutations (e.g. `del request.stage_overrides["refine"]`) + detected at access time by diffing current dict vs. baseline. +- Setting a field to its schema default value IS captured as explicit, so + it overrides model defaults. +- Raw dict reconciled lazily when `normalize_generation_request()` is called. + +## Schema purity (model-specific fields still in shared schema) + +Remain for back-compat during initial migration; targeted for migration +into preset-owned typed override classes: + +| Field | Owner | Migration target | +|---|---|---| +| `SamplingConfig.height_sr` / `width_sr` / `num_inference_steps_sr` | Hunyuan15 SR | `HunyuanSRStageOverride` (PR 10) | +| `SamplingConfig.guidance_scale_2`, `boundary_ratio` | Wan2.2, LingBotWorld | preset-owned (per-family PR) | +| `InputConfig.mouse_cond`, `keyboard_cond`, `grid_sizes` | MatrixGame | `request.extensions` or typed input config | +| `InputConfig.c2ws_plucker_emb` | LingBotWorld | `request.extensions` or typed input config | +| `InputConfig.refine_from`, `stage1_video` | LongCat | `LongCatRefineStageOverride` inputs (PR 9) | + +LTX-2 multi-modal CFG knobs (`ltx2_modality_scale_video/_audio`, +`ltx2_rescale_scale`, `ltx2_stg_scale_video/_audio`, +`ltx2_stg_blocks_video/_audio`) still leak into shared `SamplingParam` but +only LTX-2 reads them today. Migration to typed `LTX2SamplingOverride` is +deferred to per-model migration sweep. + +**LTX-2 CFG-force fix landed in PR 6**: defaults moved from `3.0/7.0` to +`1.0/1.0` to stop force-enabling CFG for non-LTX-2 families. +`ltx2_base` preset still sets `3.0/7.0` explicitly. Regression guard: +`test_presets.py::TestPresetDefaultTypes::test_ltx2_cfg_defaults_are_off`. + +## Continuation state + +Public surface: + +```python +@dataclass +class ContinuationState: + kind: str # e.g. "ltx2.v1" + payload: dict[str, Any] +``` + +Internally, model-specific typed subclasses (e.g. `LTX2ContinuationState` +at [`fastvideo/pipelines/basic/ltx2/continuation.py`](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/continuation.py)). + +Payload must be JSON-serializable or use opaque blob-ID indirection for +large tensors — supports both stateless OpenAI client round-trip AND +future Dynamo prefill/decode disaggregation. + +Hybrid model: server-held for streaming WS, client-round-trip for +stateless HTTP. See [streaming-server.md](streaming-server.md) D-1. + +## Pipeline package structure (target) + +Per-family colocation under `pipelines/basic//`: + +``` +fastvideo/pipelines/basic// +├── _pipeline.py # pipeline implementation(s) +├── presets.py # user-facing presets (DONE in PR 4) +├── pipeline_configs.py # engine/arch config (from configs/pipelines/) +└── stages/ # model-specific stages (optional, if >2 files) +``` + +What stays shared: + +- `configs/pipelines/base.py` — `PipelineConfig` base class +- `configs/models/` — architecture defs (dits/, vaes/, encoders/) +- `pipelines/stages/` — shared stages only (denoising, encoding, decoding, + text_encoding, timestep_preparation, ...) + +What's gone (PR 4): + +- `fastvideo/configs/sample/` — directory removed entirely; defaults + absorbed into per-family `presets.py`. +- All 12 `*_SamplingParam` subclass files — `SamplingParam` lives at + `fastvideo/api/sampling_param.py`; defaults flow through + `SamplingParam.from_pretrained()` → `_from_preset()`. + +What's pending: `configs/pipelines/.py` colocation, optional +`pipelines/stages/_*.py` colocation. Per-model migration PRs +(6/9/10) include the colocation step for that family. + +## YAML examples + +### Run config + +```yaml +generator: + model_path: /models/ltx2 + engine: + num_gpus: 1 + parallelism: {tp_size: -1, sp_size: -1} + offload: {dit: false, text_encoder: false, vae: false, pin_cpu_memory: true} + pipeline: + workload_type: t2v + preset: ltx2_two_stage + components: + config_root: /models/ltx2-config + upsampler_weights: /models/ltx2-refine + lora_path: /models/ltx2-refine-lora + preset_overrides: + refine: {enabled: true, add_noise: true} + +request: + prompt: "a fox running through snow" + sampling: {num_frames: 121, height: 1024, width: 1536, num_inference_steps: 8, seed: 42} + output: {save_video: true, return_state: true} + stage_overrides: + refine: {num_inference_steps: 2, guidance_scale: 1.0} +``` + +### Serve config + +See [`Dreamverse/serve_configs/streaming_demo.yaml`](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml) +for a canonical example matching internal/ui defaults (LTX-2 distilled, +NVFP4, 121 frames @ 1088×1920 24fps, 5 inference steps, 2-step refine). + +## Compatibility mapping (legacy → typed) + +| Legacy field | New path | +|---|---| +| `model_path` | `generator.model_path` | +| `num_gpus` | `generator.engine.num_gpus` | +| `tp_size` / `sp_size` | `generator.engine.parallelism.{tp_size,sp_size}` | +| `dit_cpu_offload` | `generator.engine.offload.dit` | +| `enable_torch_compile` | `generator.engine.compile.enabled` | +| `torch_compile_kwargs` | split: `generator.engine.compile.{backend,fullgraph,mode,dynamic}` + `.extras` | +| `enable_torch_compile_text_encoder` | `generator.engine.compile.text_encoder_enabled` | +| `prompt_txt` | `request.inputs.prompt_path` | +| `image_path` / `video_path` | `request.inputs.{image_path,video_path}` | +| `output_path` / `save_video` / `return_frames` | `request.output.*` | +| `seed` / `num_frames` / `height` / `width` / `fps` / `num_inference_steps` / `guidance_scale` | `request.sampling.*` | +| `enable_teacache` / `return_trajectory_*` | `request.runtime.*` | + +LTX-2 specific (private adapter, NOT public compat promise): + +| Legacy LTX-2 field | New path | +|---|---| +| `config_model_path` | `generator.pipeline.components.config_root` | +| `ltx2_refine_enabled` | `generator.pipeline.preset_overrides.refine.enabled` | +| `ltx2_refine_upsampler_path` | `generator.pipeline.components.upsampler_weights` | +| `ltx2_refine_lora_path` | `generator.pipeline.components.lora_path` | +| `ltx2_refine_num_inference_steps` | `request.stage_overrides.refine.num_inference_steps` | +| `ltx2_refine_guidance_scale` | `request.stage_overrides.refine.guidance_scale` | +| `ltx2_refine_add_noise` | `generator.pipeline.preset_overrides.refine.add_noise` | +| `ltx2_image_crf` | `request.stage_overrides.refine.image_crf` | +| `return_continuation_state` | `request.output.return_state` | + +LongCat: + +| Legacy | New | +|---|---| +| `refine_from` / `stage1_video` | `request.inputs.{refine_from,stage1_video}` | +| `t_thresh` / `spatial_refine_only` / `num_cond_frames` | `request.stage_overrides.refine.*` | + +## External inspirations (and limits) + +| Source | Useful idea | Don't copy | +|---|---|---| +| Ray | YAML-first config interchange | Ray's package layout | +| SGL `multimodal_gen` | Split instance/request config; dict input parsed into typed objects; merge user overrides on model defaults | `SamplingParams._adjust(ServerArgs)` (request depending on engine config); broad weakly-typed request bags | +| vLLM-Omni | Model-owned pipeline presets; explicit stage topology; per-stage default sampling | Positional `sampling_params_list`; serving-engine stage-index semantics in primary Python API | + +## Naming guidance + +- Public schema names namespaced under `fastvideo.api` +- Don't export from top-level `fastvideo/__init__.py` until migration further along +- `RunConfig` / `ServeConfig` get sufficient disambiguation from training + config via the namespace +- Future rename to `EngineQuantizationConfig` reserved if a collision + arises (deferred) + +## Public Python API (canonical form) + +```python +from fastvideo import VideoGenerator +from fastvideo.api import ( + GeneratorConfig, GenerationRequest, + EngineConfig, OutputConfig, + PipelineSelection, SamplingConfig, +) + +generator = VideoGenerator.from_pretrained( + config=GeneratorConfig( + model_path="/models/ltx2", + engine=EngineConfig(num_gpus=1), + pipeline=PipelineSelection(workload_type="t2v", preset="ltx2_two_stage"), + ) +) + +result = generator.generate( + GenerationRequest( + prompt="a fox running through snow", + sampling=SamplingConfig(num_frames=121, height=1024, width=1536, + num_inference_steps=8, seed=42), + output=OutputConfig(save_video=True, return_state=True), + ) +) +``` + +Accepted constructor forms: + +```python +VideoGenerator.from_pretrained(config=GeneratorConfig(...)) +VideoGenerator.from_config(GeneratorConfig(...)) +VideoGenerator.from_file("run.yaml") +VideoGenerator.from_pretrained("model-id", num_gpus=2, ...) # stable convenience +VideoGenerator.from_pretrained(model_path, **legacy_kwargs) # compat (deprecated PR 13) +``` diff --git a/.agents/memory/dreamverse-integration/integration-plan.md b/.agents/memory/dreamverse-integration/integration-plan.md new file mode 100644 index 0000000000..e79ad021c7 --- /dev/null +++ b/.agents/memory/dreamverse-integration/integration-plan.md @@ -0,0 +1,1380 @@ +# Integration Plan — Option B+ (Monorepo subfolder + generic backend) + +**Status:** Approved plan for execution; do not treat as historical audit. + +**Last updated:** 2026-05-05. + +**Decision owner:** User decision in the 2026-05-05 planning thread. + +**Target branch:** `will/ltx2_sr_port` in +[`/home/william5lin/FastVideo`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/runbook.md#L11-L16). + +**Supersedes:** +[`integration-review.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L543-L563) +for the recommendation only. The drift audit, citations, and open-thread +inventory in that document remain reference material. + +**Companion docs:** + +- Current integration snapshot: + [`state.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/state.md#L10-L20). +- PR status and #1288 scope: + [`pr-roadmap.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L38-L42). +- Typed public API design: + [`design.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L24-L43). +- Cross-repo Dreamverse surfaces: + [`cross-repo-surfaces.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L13-L20). +- Active follow-ups and drift items: + [`open-threads.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L15-L45). +- Streaming server route contract: + [`streaming-server.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L232-L259). +- Memory index and repo paths: + [`README.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/README.md#L73-L80). +- Execution runbook: + [`runbook.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/runbook.md#L79-L124). + +--- + +## Decision recap + +Option B+ is the selected end state. + +Option B+ combines two earlier options: + +1. **Option B layout principle:** Dreamverse becomes a product subfolder in + the FastVideo repository. +2. **Option D backend principle:** reusable backend code remains generic under + `fastvideo.entrypoints.streaming.*`, not under a Dreamverse namespace. + +This differs from the prior Option D recommendation in +[`integration-review.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L543-L563): + +- Prior recommendation: keep Dreamverse product in a separate repo and merge + only reusable backend pieces into FastVideo. +- New decision: move Dreamverse product code into the FastVideo repo under + `apps/dreamverse/`, while keeping reusable backend code generic. + +The repo decision is now: + +- One canonical repo: `hao-ai-lab/FastVideo`. +- Dreamverse repo is archived or redirected after migration. +- FastVideo Python library remains at the repository root. +- FastVideo kernel package remains in `fastvideo-kernel/`. +- Generic streaming backend remains at `fastvideo.entrypoints.streaming.*`. +- Dreamverse product server and web app move to `apps/dreamverse/`. + +The backend boundary stays aligned with the typed-public-boundary rule: public +inputs normalize into typed objects before legacy internals are touched +([design](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L73-L83)). + +The generic backend work already in flight through PRs #1257, #1258, #1284, +#1286, and #1288 stays on that path. #1288 is the current consolidated vehicle +for the remaining `will/ltx2_sr_port` chain +([roadmap](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L38-L42)). + +--- + +## Target architecture + +### Repo layout (after migration) + +```text +FastVideo/ +├── fastvideo/ # Python ML library, public API, entrypoints +│ └── entrypoints/{openai,streaming}/ # existing OpenAI + generic streaming backend +├── fastvideo-kernel/ # separate CUDA/custom-kernel package +├── examples/serving/streaming_demo.yaml +├── docs/contributing/dreamverse-development.md +├── apps/ +│ └── dreamverse/ +│ ├── README.md, AGENTS.md, arch.md, design.md, gpu-pool.{svg,drawio} +│ ├── server/ +│ │ ├── pyproject.toml, main.py, config.py, video_generation.py +│ │ ├── session/{controller.py,messages.py} +│ │ ├── routes/presets.py +│ │ ├── prompts/ +│ │ └── tests/ +│ ├── web/ # standalone Next.js frontend +│ ├── prompts/ # curated product content if separated +│ ├── serve_configs/ # product deployment YAMLs +│ └── scripts/ # setup and launch scripts +├── .github/workflows/ +│ ├── ci-dreamverse-backend.yml +│ └── ci-dreamverse-frontend.yml +├── pyproject.toml, uv.lock +└── .pre-commit-config.yaml +``` + +FastVideo already has a Python-package layout with `fastvideo/`, +`fastvideo-kernel/`, `examples/`, `docs/`, and `tests/` +([codebase map](file:///home/william5lin/FastVideo/.agents/memory/codebase-map/README.md#L5-L75)). +Option B+ adds `apps/dreamverse/` without moving the library or kernel paths. + +### Module boundary + +| Area | Owns | Does not own | Evidence / reference | +|---|---|---|---| +| `fastvideo.api` | `GeneratorConfig`, `GenerationRequest`, `ServeConfig`, `VideoResult`, `VideoEvent`, `ContinuationState` | Product route policy or curated content | Typed schema surface is documented in [design](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L45-L72). | +| `fastvideo.entrypoints.streaming.*` | Generic WebSocket server, GPU pool, streaming protocol, fMP4 stream, mock server, prompt enhancer, safety/rewrite helpers, router | Dreamverse-only curated presets, prompt UX routes, frontend devtools | Streaming target package is documented in [streaming-server](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L164-L195). | +| `apps/dreamverse/server.*` | Product FastAPI composition, env defaults, curated content, Dreamverse WebSocket/session flow, product worker wrapper | Generic reusable FastVideo backend code | Product-only routes stay Dreamverse-side per route contract [streaming-server](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L239-L255). | +| `apps/dreamverse/web` | Next.js product UI and browser tests | Python package release or FastVideo core CI | New standalone frontend CI is introduced in this plan. | +| `apps/dreamverse/prompts` | Curated product prompt content and presets | Generic prompt provider protocol | DR-1 keeps product prompt orchestration Dreamverse-side unless a second consumer appears [open-threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L440-L455). | +| `examples/serving/` | Public-safe serving examples | Product deployment secrets or private paths | Missing streaming demo config is tracked as drift item #7 [integration-review](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L259-L276). | + +### Import contract + +`apps/dreamverse/server/*` may import the following FastVideo surfaces: + +```python +# Public root re-exports +from fastvideo import VideoGenerator, PipelineConfig, SamplingParam + +# Typed public schema, results, presets +from fastvideo.api import GeneratorConfig, GenerationRequest, ServeConfig, ... + +# Reusable streaming runtime +from fastvideo.entrypoints.streaming import build_app, ... +from fastvideo.entrypoints.streaming. import ... + +# Pipeline + arch configs (typed dataclasses, public surface for advanced wiring) +from fastvideo.configs.pipelines import ... +from fastvideo.configs.models import ... + +# High-level entrypoints +from fastvideo.entrypoints.video_generator import VideoGenerator +``` + +It must NOT import from internal library implementation paths: + +```python +from fastvideo.pipelines... import ... # internal pipeline impl +from fastvideo.models... import ... # internal model impl +from fastvideo.layers... import ... # internal layer impl +from fastvideo.worker... import ... # internal worker IPC +from fastvideo.fastvideo_args import ... # legacy args; use GeneratorConfig +``` + +**Why the relaxation from `{api, entrypoints.streaming}` only**: Dreamverse's +[`video_generation.py`](file:///home/william5lin/Dreamverse/server/video_generation.py#L260-L420) +currently imports from `fastvideo.configs`, `fastvideo.entrypoints.video_generator`, +and uses `VideoGenerator.from_pretrained()` — these are public surfaces by design +([design.md](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L295-L332)) +even though they live outside `fastvideo.api/`. Restricting to only `api` + +`entrypoints.streaming` would force a 578-LOC rewrite of `video_generation.py` +which is out of scope for the migration. The contract enforces the +**meaningful** boundary: no internal `pipelines/models/layers/worker` paths. + +Rationale: + +- Dreamverse's three integration surfaces are pipeline construction, realtime + runtime, and continuation state + ([cross-repo-surfaces](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L13-L20)). +- The target public surface for reusable streaming runtime is + `fastvideo.entrypoints.streaming/` + ([streaming-server](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L55-L60)). +- Dynamo and Dreamverse should consume stable typed APIs, not private modules + ([cross-repo contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L188-L210)). + +Add a contract test in Phase 2: + +```python +# apps/dreamverse/server/tests/test_import_contract.py +import ast +from pathlib import Path + +ALLOWED_PREFIXES = ( + "fastvideo.api", + "fastvideo.entrypoints.streaming", + "fastvideo.entrypoints.video_generator", + "fastvideo.configs", +) +ALLOWED_EXACT = ("fastvideo",) +FORBIDDEN_PREFIXES = ( + "fastvideo.pipelines", + "fastvideo.models", + "fastvideo.layers", + "fastvideo.worker", + "fastvideo.fastvideo_args", +) + + +def test_dreamverse_server_imports_only_public_fastvideo_surfaces() -> None: + root = Path(__file__).resolve().parents[1] + bad: list[tuple[str, int, str]] = [] + for path in root.rglob("*.py"): + if "/tests/" in path.as_posix(): + continue + for node in ast.walk(ast.parse(path.read_text(), filename=str(path))): + names = ( + [a.name for a in node.names] if isinstance(node, ast.Import) + else [node.module] if isinstance(node, ast.ImportFrom) and node.module + else [] + ) + for name in names: + if not name: + continue + if name.startswith(FORBIDDEN_PREFIXES): + bad.append((str(path.relative_to(root)), node.lineno, name)) + + assert bad == [], f"Forbidden internal imports: {bad}" +``` + +Treat this as the import-boundary gate for all future product-server changes. + +--- + +## Tooling stack + +### Python build: uv workspace + +Use uv workspaces for the Python side. + +Precedent and rationale: + +- Official uv workspace docs: https://docs.astral.sh/uv/concepts/workspaces/ +- The docs describe workspaces as suitable for a FastAPI web application + alongside libraries. +- Similar workspace patterns are used by chainlit, rerun-io/rerun, + microsoft/autogen, and langgenius/dify. +- Hatch workspace remains an alternative, but uv is already present in + FastVideo's root `pyproject.toml` + ([pyproject](file:///home/william5lin/FastVideo/pyproject.toml#L89-L115)). + +Root `pyproject.toml` currently uses setuptools and discovers packages by +excluding assets, docker, docs, and scripts +([pyproject](file:///home/william5lin/FastVideo/pyproject.toml#L1-L3), +[package find](file:///home/william5lin/FastVideo/pyproject.toml#L158-L162)). + +Apply this root diff in Phase 1: + +```diff +diff --git a/pyproject.toml b/pyproject.toml +@@ + [tool.uv] + prerelease = "allow" + ++[tool.uv.workspace] ++members = ["apps/dreamverse/server"] ++ + [tool.uv.sources] + torch = [ +@@ + [tool.setuptools.packages.find] +-exclude = ["assets*", "docker*", "docs", "scripts*"] ++exclude = ["assets*", "docker*", "docs", "scripts*", "apps*"] + + [tool.wheel] +-exclude = ["assets*", "docker*", "docs", "scripts*"] ++exclude = ["assets*", "docker*", "docs", "scripts*", "apps*"] +``` + +Create `apps/dreamverse/server/pyproject.toml` in Phase 1: + +```toml +[project] +name = "dreamverse-server" +version = "0.1.0" +description = "Dreamverse product server" +requires-python = ">=3.10" +dependencies = [ + "fastvideo[streaming]", + "fastapi==0.129.0", + "uvicorn==0.41.0", + "aiofiles", + "websockets", +] + +[project.optional-dependencies] +test = [ + "httpx", + "pytest", + "pytest-asyncio", +] + +[tool.uv] +package = false + +[tool.uv.sources] +fastvideo = { workspace = true } + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +asyncio_mode = "auto" +``` + +Notes: + +- `package = false` keeps this as an application workspace member, not a + package to publish. +- `fastvideo = { workspace = true }` forces local workspace resolution during + development and CI. +- Product server release is Docker/deploy workflow, not PyPI. + +### Frontend build: standalone pnpm + +Do not add a root `package.json`. + +Keep all frontend tooling under: + +```text +apps/dreamverse/web/package.json +apps/dreamverse/web/pnpm-lock.yaml +apps/dreamverse/web/playwright.config.* +``` + +Rationale: + +- FastVideo remains primarily a Python ML library. +- Python contributors should not need Node or pnpm for normal work. +- This intentionally diverges from chainlit's root JS workspace pattern and + follows the simpler open-webui-style split. + +The frontend CI works from `apps/dreamverse/web/` directly. + +### Pre-commit + +Root `.pre-commit-config.yaml` currently excludes generated or heavyweight +paths, including `.agents`, `examples`, and `fastvideo/models` +([pre-commit](file:///home/william5lin/FastVideo/.pre-commit-config.yaml#L4-L17)). + +Extend the global exclude for the Node frontend only. + +Do not exclude `apps/dreamverse/server/`; Python hooks must cover product +server code. + +Apply this Phase 1 diff: + +```diff +diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml +@@ + fastvideo/models/.*| ++ apps/dreamverse/web/.*| + examples/.*| +``` + +Verification: + +```bash +pre-commit run --files \ + pyproject.toml \ + .pre-commit-config.yaml \ + apps/dreamverse/server/pyproject.toml +``` + +Memory-dir edits are excluded from yapf/ruff/mypy; only the filename-space +check applies +([runbook](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/runbook.md#L81-L93)). + +### `.gitignore` + +Root `.gitignore` currently contains Python, docs, asset, output, and local +tooling ignores, and notes that Node artifacts are only ignored under `ui/` +([gitignore](file:///home/william5lin/FastVideo/.gitignore#L38-L94)). + +Add Dreamverse-scoped Node ignores only, **AND unignore product public assets** (the root currently ignores `*.png`, `*.jpg`, `*.mp4`, `*.gif` per [`.gitignore:6-9`](file:///home/william5lin/FastVideo/.gitignore#L6-L9)): + +```diff +diff --git a/.gitignore b/.gitignore +@@ + # Next.js / Node artifacts under ui/: see ui/.gitignore ++ ++# Next.js / Node artifacts under apps/dreamverse/web/ ++apps/dreamverse/web/node_modules/ ++apps/dreamverse/web/.next/ ++apps/dreamverse/web/out/ ++apps/dreamverse/web/coverage/ ++apps/dreamverse/web/test-results/ ++apps/dreamverse/web/playwright-report/ ++apps/dreamverse/web/.env.local ++apps/dreamverse/web/.env.development.local ++apps/dreamverse/web/.env.test.local ++apps/dreamverse/web/.env.production.local ++ ++# Unignore migrated Dreamverse product assets — root .gitignore globally ++# ignores *.png/*.jpg/*.mp4/*.gif, but apps/dreamverse/web/public/ MUST ++# be tracked (logo, icons, k2.png, etc.). ++!apps/dreamverse/web/public/** ++!apps/dreamverse/web/prompts/** ++!apps/dreamverse/server/prompts/** ++!apps/dreamverse/gpu-pool.svg ++!apps/dreamverse/gpu-pool.drawio +``` + +Do not add global `node_modules/` or `.next/` ignores. Keep the pattern scoped +so unrelated JS projects do not inherit accidental behavior. **Verify before +committing Phase 3** with `git check-ignore -v apps/dreamverse/web/public/k2.png` +(should report nothing once the unignore lands). + +### CI workflows + +Existing FastVideo CI remains in place: + +- Buildkite GPU and test matrix lives at + [`.buildkite/pipeline.yml`](file:///home/william5lin/FastVideo/.buildkite/pipeline.yml#L199-L280) + and uses path-based monorepo diff watch lists. +- Full suite is triggered by GitHub Actions and Buildkite + ([trigger workflow](file:///home/william5lin/FastVideo/.github/workflows/ci-trigger-full-suite.yml#L52-L83), + [Buildkite full suite](file:///home/william5lin/FastVideo/.buildkite/pipeline.yml#L282-L438)). +- Pre-commit workflow currently runs on pull requests + ([ci-precommit](file:///home/william5lin/FastVideo/.github/workflows/ci-precommit.yml#L1-L32)). +- Docs workflow already uses explicit docs paths + ([infra-docs](file:///home/william5lin/FastVideo/.github/workflows/infra-docs.yml#L3-L18)). +- FastVideo PyPI publish remains version-driven on root `pyproject.toml` + ([publish-fastvideo](file:///home/william5lin/FastVideo/.github/workflows/publish-fastvideo.yml#L1-L72)). + +Add two new workflows. + +#### `.github/workflows/ci-dreamverse-frontend.yml` + +```yaml +name: Dreamverse Frontend + +on: + pull_request: + branches: [main] + paths: + - 'apps/dreamverse/web/**' + - '.github/workflows/ci-dreamverse-frontend.yml' + push: + branches: [main] + paths: + - 'apps/dreamverse/web/**' + - '.github/workflows/ci-dreamverse-frontend.yml' + +permissions: + contents: read + +defaults: + run: + working-directory: apps/dreamverse/web + +jobs: + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # IMPORTANT: pnpm/action-setup MUST run BEFORE setup-node when using + # cache: pnpm — setup-node otherwise can't find pnpm to populate cache. + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + cache-dependency-path: apps/dreamverse/web/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm run typecheck --if-present + + - name: Unit tests + run: pnpm run test --if-present + + - name: Build + run: pnpm run build + + # NOTE: Playwright tests require a running backend. Until Phase 4 lands + # `/healthz`/`/readyz`/`/status`/`/prompt-system-config`/`/curated-presets` + # in `apps/dreamverse/server/main.py`, scope frontend CI to build + unit + # tests only. Re-enable Playwright in Phase 4 once health/preset routes + # are mounted by `apps/dreamverse/server/main.py` (which can wrap public + # `fastvideo.entrypoints.streaming.build_app` and add product routes). + # + # - name: Install Playwright browsers + # run: pnpm exec playwright install --with-deps chromium + # + # - name: Playwright (re-enable in Phase 4) + # run: pnpm exec playwright test +``` + +**Playwright config update needed** when moving FE: `Dreamverse/apps/web/playwright.config.ts` line 39 currently uses `npm run dev`; change to `pnpm run dev` post-move ([source](file:///home/william5lin/Dreamverse/apps/web/playwright.config.ts#L39)). + +#### `.github/workflows/ci-dreamverse-backend.yml` + +```yaml +name: Dreamverse Backend + +on: + pull_request: + branches: [main] + paths: + - 'apps/dreamverse/server/**' + - 'pyproject.toml' + - 'uv.lock' + # apps/dreamverse/server depends on these public surfaces — changes + # there must trigger Dreamverse backend tests: + - 'fastvideo/api/**' + - 'fastvideo/entrypoints/streaming/**' + - 'fastvideo/entrypoints/video_generator.py' + - 'fastvideo/configs/**' + - '.github/workflows/ci-dreamverse-backend.yml' + push: + branches: [main] + paths: + - 'apps/dreamverse/server/**' + - 'pyproject.toml' + - 'uv.lock' + - 'fastvideo/api/**' + - 'fastvideo/entrypoints/streaming/**' + - 'fastvideo/entrypoints/video_generator.py' + - 'fastvideo/configs/**' + - '.github/workflows/ci-dreamverse-backend.yml' + +permissions: + contents: read + +jobs: + backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Sync Dreamverse backend workspace (locked) + run: uv sync --locked --package dreamverse-server --extra test + + - name: Backend tests + run: uv run --locked --package dreamverse-server --extra test pytest apps/dreamverse/server/tests/ -q +``` + +Extend existing workflows where appropriate. + +Recommended diffs: + +```diff +diff --git a/.github/workflows/ci-precommit.yml b/.github/workflows/ci-precommit.yml +@@ + pull_request: + branches: [main] ++ paths-ignore: ++ - 'apps/dreamverse/web/**' +``` + +```diff +diff --git a/.github/workflows/ci-trigger-full-suite.yml b/.github/workflows/ci-trigger-full-suite.yml +@@ + pull_request_target: + types: [labeled, synchronize] ++ paths-ignore: ++ - 'apps/dreamverse/**' +``` + +Do not add `paths-ignore` to workflows already constrained by non-Dreamverse +`paths`, such as docs deploy or PyPI publish, unless a later CI audit finds +actual false triggers. + +### Release + +FastVideo PyPI release remains unchanged: + +- Root package is still `fastvideo`. +- Version-driven publish remains tied to root `pyproject.toml` version changes + ([publish workflow](file:///home/william5lin/FastVideo/.github/workflows/publish-fastvideo.yml#L23-L72)). +- `apps*` is excluded from setuptools and wheel discovery. + +Dreamverse product release is separate: + +- Backend: Docker or deployment workflow rooted at `apps/dreamverse/server/`. +- Frontend: Vercel or frontend deploy workflow rooted at `apps/dreamverse/web/`. +- No PyPI publish for `apps/dreamverse/server`. +- No root `package.json`. + +Add deployment workflows only after Phase 6, once the product runs from the +monorepo CI path. + +### OSS precedents used for this plan + +| Project | URL | Pattern applied here | +|---|---|---| +| uv workspaces | https://docs.astral.sh/uv/concepts/workspaces/ | Authoritative Python workspace model. | +| Hatch monorepo | https://hatch.pypa.io/latest/how-to/environment/workspace/ | Alternative workspace model; not selected. | +| chainlit | https://github.com/Chainlit/chainlit | uv workspace precedent plus **per-language CI split** (separate `check-frontend.yaml` / `check-backend.yaml` workflows path-filtered by directory). Not a PR-level split — independent of D-17 single-mega-PR decision. | +| open-webui | https://github.com/open-webui/open-webui | Frontend path filtering (`paths-ignore` on backend-only changes) and separate release tracks. **Note:** open-webui has a root `package.json`; we are choosing standalone-pnpm despite the precedent, to avoid forcing Python-only contributors to install Node. | +| streamlit | https://github.com/streamlit/streamlit | Split Python and JS testing in one repo. | +| gradio | https://github.com/gradio-app/gradio | Python package plus JS workspace precedent. | +| full-stack-fastapi-template-nextjs | https://github.com/nemanjam/full-stack-fastapi-template-nextjs | Separate frontend/backend build and deploy workflows. | +| rerun-io/rerun, microsoft/autogen, langgenius/dify | various | uv workspace examples with workspace members and sources. | + +### Test strategy + +The migration spans CPU-only logic, GPU-required inference, and end-to-end +WebSocket flows. Each phase's verification gate must specify which test +class runs where, because not all tests can run on `ubuntu-latest` CI. + +#### Test taxonomy + +| Class | Marker | Runs in | What it covers | Examples | +|---|---|---|---|---| +| **Unit** | (none / `unit`) | `ci-dreamverse-backend.yml` (ubuntu-latest CI) + locally | Pure logic, no GPU, no live service. Mocked FastVideo backends, schema validation, helper functions. | `test_config.py`, `test_rewrite_prompt_payload.py`, `test_session_init_image.py`, the new `test_import_contract.py` | +| **Integration (fakes)** | `integration` | `ci-dreamverse-backend.yml` + locally | FastAPI test client + in-process fakes/mocks for GPU pool. Validates routes, request/response shapes, session state machine. | `test_health_endpoints.py`, `test_mock_server.py`, `test_entrypoints.py`, `test_prompt_safety.py`, `test_batching.py` (deleted) | +| **Live-service GPU** | `gpu` (skip-by-default in CI) | **Local GPU4 manual QA** + Buildkite-Modal (when added) | Real `fastvideo serve` process + real model weights + real WebSocket round-trips. Validates LTX-2 streaming, NVFP4 wiring, continuation state, frame emission. | `test_realtime_stress.py` (947 LOC), `test_session_logging.py` (1278 LOC) — these spin up real workers per their current shape | +| **Frontend unit / build** | (n/a — pnpm) | `ci-dreamverse-frontend.yml` (ubuntu-latest, no GPU) | Vitest + tsc + Next.js build. No backend needed. | `apps/dreamverse/web/src/**/*.test.ts(x)` | +| **Frontend Playwright E2E** | (n/a — pnpm) | **Local GPU4 manual QA** until Phase 4 lands public health routes; then `ci-dreamverse-frontend.yml` against a mock backend OR a deployed staging | Real browser → real backend WebSocket flow. Requires `/healthz`, `/readyz`, `/status`, `/prompt-system-config`, `/curated-presets`, `/v1/stream`. | `apps/dreamverse/web/e2e/{backend-health,frontend-shell,preset-prompt-generation}.spec.ts` | +| **FastVideo public contract** | (none) | Existing FastVideo CI (`ci-precommit` + Buildkite for GPU) | Schema/shape guards that this migration must not break. | `fastvideo/tests/contract/test_dreamverse_shape.py`, `test_dynamo_shape.py`, `test_generate_async.py` | +| **FastVideo SSIM regression** | (Buildkite path-filter) | Buildkite-Modal | Inference-quality gates for ported models. | `fastvideo/tests/ssim/test_*.py` | + +#### Adding the `gpu` marker + +In Phase 1, add to root `pyproject.toml`: + +```diff + [tool.pytest.ini_options] ++markers = [ ++ "gpu: requires a real GPU + model weights; skip in ubuntu-latest CI", ++] +``` + +In `apps/dreamverse/server/pyproject.toml` `[tool.pytest.ini_options]`, set +the default for the backend test command to skip GPU tests: + +```toml +[tool.pytest.ini_options] +addopts = "-m 'not gpu'" +markers = ["gpu: requires real GPU"] +``` + +GPU-dependent tests must add `@pytest.mark.gpu` at module or function level +during the Phase 2 move. Specifically: `test_realtime_stress.py` and +`test_session_logging.py` per their current LOC and live-service shape. + +#### Local GPU4 verification hook + +This dev node has 8× B200 GPUs. **GPU4** is the operator's chosen target for +migration smoke tests. The original `dreamverse-server` was launched with +`CUDA_VISIBLE_DEVICES=4`, which makes physical GPU 4 appear as logical GPU 0 +inside the process — so the Dreamverse-side log shows `Selected GPU ids: [0]` +even though it physically holds GPU 4. **Always launch with `CUDA_VISIBLE_DEVICES=4`** +to keep the same physical pin during smoke runs. + +##### Phase 0 prerequisites for the GPU4 hook (validated 2026-05-05) + +A live smoke test of `fastvideo serve` from `will/ltx2_sr_port` HEAD +`d23e71c2` ran on GPU4 and revealed two missing optional deps in the +FastVideo `.venv`: + +- **`flashinfer-python`** — required for the NVFP4 path. Without it, the + server fails at boot with `ImportError: NVFP4 quantization requires + flashinfer. Install with 'pip install flashinfer-python'.` +- **`flash_attn`** — optional. Without it, attention falls back to Torch + SDPA (`Cannot use FlashAttention-2 backend ... Using Torch SDPA backend.`). + Functional but slower per inference step. + +For a fast smoke that doesn't need NVFP4, comment out or omit +`engine.quantization` in the serve config — the loader auto-falls-back +to bf16. The serve config in +[`Dreamverse/serve_configs/streaming_demo.yaml`](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L26-L29) +explicitly says: "Hosts without flashinfer / NVFP4: comment out the +`engine.quantization` block below — the loader falls back to bf16 +automatically when no quant_config is set." + +For production-equivalent NVFP4 smoke, install both deps in the FastVideo +`.venv`: + +```bash +.venv/bin/pip install flashinfer-python flash-attn --no-build-isolation +``` + +This is a **Phase 0 prerequisite** to document in +`docs/contributing/dreamverse-development.md` (Phase 1). + +##### Reclaim and redeploy commands + +```bash +# Stop the running Dreamverse-side server holding GPU4 +sudo kill 2453227 # or use the supervisor that owns it + +# Confirm GPU4 is free +nvidia-smi --query-gpu=index,memory.used --format=csv | grep -E '^4,' + +# Deploy the new public FastVideo streaming server pinned to GPU4 +CUDA_VISIBLE_DEVICES=4 uv run --locked --package dreamverse-server \ + fastvideo serve --config apps/dreamverse/serve_configs/streaming_demo.yaml \ + --host 0.0.0.0 --port 8009 + +# Smoke-test from another terminal +curl -s http://localhost:8009/health | jq . +curl -s http://localhost:8009/readyz | jq . # Phase 4+ only +# Drive the FE against it: cd apps/dreamverse/web && pnpm run dev +``` + +This is the **manual QA gate** for any phase that touches the live-service +path (Phase 2 backend move, Phase 3 frontend move, Phase 4 health-route +promotion, Phase 5 prompt enhancer retirement). The verification gate for +each phase calls out whether the GPU4 smoke is required or optional. + +**Cleanup after each test session:** + +```bash +# Kill the test deployment +pkill -f 'fastvideo serve --config' + +# Restart the canonical Dreamverse-side server with the same GPU pin +cd /home/william5lin/Dreamverse +setsid bash -c 'CUDA_VISIBLE_DEVICES=4 exec .venv/bin/dreamverse-server > /tmp/dv.log 2>&1 < /dev/null' & disown + +# Confirm restart +sleep 3 && pgrep -af dreamverse-server | head -3 +``` + +**Verified 2026-05-05**: this kill/redeploy/teardown cycle was executed +end-to-end on `will/ltx2_sr_port` HEAD `d23e71c2`. Public `fastvideo serve` +booted in ~24 s on GPU4 with bf16 fallback (no flashinfer in `.venv`), +served `/health` → 200 `{"status":"ok","sessions":0,"stream_mode":"av_fmp4"}`, +and confirmed empirically that `/healthz`, `/readyz`, `/status`, +`/prompt-system-config`, and `/curated-presets` all 404 (matching drift +table item #4 → Phase 4 promotion target). Smoke artifacts at +`/tmp/opencode/fv_smoke/` (serve.log, smoke.yaml). + +#### Per-phase test responsibilities (summary) + +| Phase | Unit | Integration (fakes) | GPU live (GPU4) | Frontend build | Frontend E2E (Playwright) | +|---|:---:|:---:|:---:|:---:|:---:| +| 0 (#1288 land) | Existing FastVideo suite | Existing | Optional sanity | n/a | n/a | +| 1 (skeleton) | New empty pkg `pytest --collect-only` | n/a | n/a | n/a | n/a | +| 2 (backend move) | **Required** in CI | **Required** in CI | **Required manual** on GPU4 (smoke) | n/a | n/a | +| 3 (FE move) | Backend still green | Backend still green | Optional | **Required** in CI | **Manual on GPU4 only** (FE CI Playwright deferred to Phase 4) | +| 4 (promote pending) | Streaming tests must add coverage | Required | **Required manual** on GPU4 | Required | **Required manual** on GPU4 → re-enable FE-CI Playwright at end | +| 5 (DR-1 / DR-2) | Required | Prompt-shim tests required | **Required manual** prompt-flow on GPU4 | Required | **Required manual** preset-prompt-generation spec on GPU4 | +| 6a-6f | All CI green | All CI green | Optional during 6b/6c deploy dry runs | All CI green | Required against staging in 6c | +| 7 (archive) | n/a | n/a | n/a | n/a | n/a | + +--- + +## File-by-file migration map + +### PRODUCT files (move into `apps/dreamverse/`) + +| Source path | LOC | Target path | Notes | +|---|---:|---|---| +| `Dreamverse/server/main.py` | 148 | `apps/dreamverse/server/main.py` | FastAPI app composition; imports change to `from fastvideo.entrypoints.streaming import build_app` etc. | +| `Dreamverse/server/config.py` | 303 | `apps/dreamverse/server/config.py` | Dreamverse-specific env/model registry/prompt paths. | +| `Dreamverse/server/video_generation.py` | 578 | `apps/dreamverse/server/video_generation.py` | LTX-2 product-specific worker wrapper + continuation conditioning. | +| `Dreamverse/server/session/controller.py` | 1948 | `apps/dreamverse/server/session/controller.py` | Per-WebSocket session controller — Dreamverse protocol/UI flow. | +| `Dreamverse/server/session/messages.py` | 21 | `apps/dreamverse/server/session/messages.py` | Queue DTOs; promote only if generic. | +| `Dreamverse/server/routes/presets.py` | 234 | `apps/dreamverse/server/routes/presets.py` | Curated preset HTTP routes, product content. | +| `Dreamverse/server/tests/test_config.py` | 154 | `apps/dreamverse/server/tests/test_config.py` | Product config tests. | +| `Dreamverse/server/tests/test_entrypoints.py` | 99 | `apps/dreamverse/server/tests/test_entrypoints.py` | Product CLI tests. | +| `Dreamverse/server/tests/test_realtime_stress.py` | 947 | `apps/dreamverse/server/tests/test_realtime_stress.py` | E2E websocket stress, product-specific. | +| `Dreamverse/server/tests/test_session_logging.py` | 1278 | `apps/dreamverse/server/tests/test_session_logging.py` | Product session flow tests. | +| `Dreamverse/server/tests/conftest.py` | 14 | `apps/dreamverse/server/tests/conftest.py` | Test path bootstrap. | +| `Dreamverse/server/prompts/*.md` | — | `apps/dreamverse/server/prompts/*.md` | System prompt content: 4 files plus README. | +| `Dreamverse/apps/web/**` | — | `apps/dreamverse/web/**` | Entire Next.js frontend. | +| `Dreamverse/AGENTS.md`, `arch.md`, `design.md`, `gpu-pool.{svg,drawio}` | — | `apps/dreamverse/` | Product-level docs and diagrams. | +| `Dreamverse/scripts/install_native_ffmpeg.sh`, `smoke_local.sh` | — | `apps/dreamverse/scripts/` | Product launch/setup scripts. | +| `Dreamverse/serve_configs/streaming_demo.yaml` | — | `apps/dreamverse/serve_configs/streaming_demo.yaml` | Product deployment config; also copy to `examples/serving/streaming_demo.yaml`. | +| `Dreamverse/.agents/skills/launch-demo/scripts/*.sh` | — | `apps/dreamverse/scripts/launch/*.sh` | Demo launch helpers. | + +### GENERIC-MERGED files (delete from Dreamverse copy; import public FastVideo) + +| Source path | Already on FastVideo public at | Action | +|---|---|---| +| `Dreamverse/server/gpu_pool.py` (1053 LOC) | `fastvideo.entrypoints.streaming.gpu_pool` | Delete; `apps/dreamverse/server/main.py` imports public API. | +| `Dreamverse/server/prompt_enhancer.py` (1933 LOC) | `fastvideo.entrypoints.streaming.prompt.*` (partial) | **DEFER deletion to Phase 5.** Public `PromptEnhancer` does not yet expose Dreamverse-side methods (`resolve_rewrite_model` per [`controller.py:203`](file:///home/william5lin/Dreamverse/server/session/controller.py#L203), `get_prompt_config` / `save_prompt_config` per [`routes/presets.py:95`](file:///home/william5lin/Dreamverse/server/routes/presets.py#L95)). Carry the file into `apps/dreamverse/server/prompt_enhancer.py` during Phase 2 unmodified. Phase 5 implements DR-1 compat shim + retires the fork. | +| `Dreamverse/server/av_streaming.py` (434 LOC) | `fastvideo.entrypoints.streaming.stream` | Delete. | +| `Dreamverse/server/worker_ipc.py` (161 LOC) | `fastvideo.entrypoints.streaming.protocol` | Delete. | +| `Dreamverse/server/mock_server.py` (1207 LOC) | `fastvideo.entrypoints.streaming.mock_server` | Delete. | +| `Dreamverse/server/session_init_image.py` (103 LOC) | `fastvideo.entrypoints.streaming.session_init_image` | Delete. | +| `Dreamverse/server/session_logger.py` (44 LOC) | `fastvideo.entrypoints.streaming.session_logger` | Delete. | +| `Dreamverse/server/tests/test_gpu_pool.py` (126 LOC) | covered by FastVideo tests | Delete. | +| `Dreamverse/server/tests/test_mock_server.py` (360 LOC) | covered by FastVideo tests | Delete. | +| `Dreamverse/server/tests/test_prompt_enhancer.py` (1176 LOC) | covered by FastVideo tests | Delete, or keep product-specific subset only. | +| `Dreamverse/server/tests/test_session_init_image.py` (74 LOC) | covered by FastVideo tests | Delete. | + +Generic-merged ownership is consistent with the existing upstreaming map in +[`streaming-server.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L14-L31). + +### GENERIC-PENDING files (schedule for promotion) + +| Source path | LOC | Target path on FastVideo public | +|---|---:|---| +| `Dreamverse/server/runtime.py` | 21 | `fastvideo.entrypoints.streaming.runtime` or absorb into server state. | +| `Dreamverse/server/prompt_safety.py` | 222 | `fastvideo.entrypoints.streaming.prompt.safety` by extending existing safety module. | +| `Dreamverse/server/rewrite_prompt_payload.py` | 98 | `fastvideo.entrypoints.streaming.prompt.rewrite` by extending existing rewrite module. | +| `Dreamverse/server/utils.py` | 21 | absorb into appropriate streaming/API utility. | +| `Dreamverse/server/routes/health.py` | 101 | extend `fastvideo.entrypoints.streaming.server.build_app` to expose `/healthz`, `/readyz`, `/status`. | +| `Dreamverse/server/benchmarks/benchmark_*.py` | ~900 total | `fastvideo/tests/benchmarks/` or `benchmarks/` at FastVideo root. | +| `Dreamverse/server/tests/test_health_endpoints.py` | 163 | follow health route merge. | +| `Dreamverse/server/tests/test_prompt_safety.py` | 51 | follow prompt safety merge. | +| `Dreamverse/server/tests/test_rewrite_prompt_payload.py` | 89 | follow rewrite merge. | +| `Dreamverse/server/tests/test_benchmark_*.py` | ~470 total | follow benchmark merge. | + +The health route gap is tracked as open item #1 +([open-threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99)). + +### DELETE files (no longer needed) + +| Source path | LOC | Why | +|---|---:|---| +| `Dreamverse/server/server_entry.py` | 15 | CLI shim no longer needed in monorepo. | +| `Dreamverse/server/routes/__init__.py` | 1 | Empty package marker. | +| `Dreamverse/server/session/__init__.py` | 5 | Empty package marker. | +| `Dreamverse/server/tests/test_batching.py` | 149 | Obsolete ORCA batching protocol. | + +--- + +## Phased migration plan + +### Phase 0 — Land #1288 (in flight) + +**Purpose:** Ensure the generic FastVideo backend substrate is available before +moving Dreamverse product code into the monorepo. + +**Files / scope:** + +- `fastvideo/api/` typed results and event classes. +- `fastvideo/entrypoints/video_generator.py` async generation path. +- `fastvideo/entrypoints/streaming/` generic streaming server, GPU pool, + prompt, safety, rewrite, mock server, router, and tests. +- LTX-2 SR runtime and NVFP4 wiring included in #1288. +- Contract tests for Dreamverse and Dynamo shapes. + +**Source:** #1288 is open and mergeable on `will/ltx2_sr_port` +([roadmap](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L38-L42)). + +**Steps:** + +1. Wait for #1288 to merge to `origin/main`. +2. Fetch main and verify the content is present. +3. Update memory-dir state after merge per runbook + ([runbook](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/runbook.md#L51-L70)). +4. Start Phase 1 from updated `main` or a new branch based on it. + +**Verification gate:** + +- #1288 merge commit exists on `origin/main`. +- `fastvideo.api.VideoEvent` and `VideoGenerator.generate_async` are importable. +- Existing API/contract/NVFP4/LTX-2 smoke baseline remains green; May 2 baseline + is documented in + [`state.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/state.md#L142-L149). + +**Rollback:** + +- If #1288 does not merge, stop. Do not move Dreamverse product code. +- If #1288 merges and regresses, revert or fix #1288 before Phase 1. +- Do not create `apps/dreamverse/` until generic backend import targets are + stable on the branch used for migration. + +### Phase 1 — Add `apps/` skeleton + +**Purpose:** Establish monorepo tooling and empty product directories before a +mass move. This should be a small PR. + +**Files to add:** + +- `apps/dreamverse/README.md` +- `apps/dreamverse/server/pyproject.toml` +- `apps/dreamverse/server/tests/.gitkeep` +- `apps/dreamverse/web/.gitkeep` +- `apps/dreamverse/prompts/.gitkeep` +- `apps/dreamverse/serve_configs/.gitkeep` +- `apps/dreamverse/scripts/.gitkeep` +- `docs/contributing/dreamverse-development.md` + +**Files to modify:** + +- `pyproject.toml` +- `uv.lock` — **regenerated** when adding the workspace member; commit alongside. +- `.pre-commit-config.yaml` +- `.gitignore` +- `mkdocs.yml`, if `docs/contributing/dreamverse-development.md` is added to + the Developer Guide nav. Current nav lives at + [`mkdocs.yml`](file:///home/william5lin/FastVideo/mkdocs.yml#L169-L180). +- `fastvideo/entrypoints/streaming/gpu_pool.py` for D-12-A docstring caveat. + +**Steps:** + +1. Create the directory skeleton. +2. Add the server `pyproject.toml` shown in the tooling section. +3. Add `[tool.uv.workspace] members = ["apps/dreamverse/server"]` to root + `pyproject.toml`. +4. Add `apps*` to root setuptools and wheel excludes. +5. Add scoped Dreamverse web ignores + product-asset unignores to `.gitignore`. +6. Add `apps/dreamverse/web/.*` to pre-commit global exclude. +7. Add `docs/contributing/dreamverse-development.md` with local dev commands: + backend `uv run --locked --package dreamverse-server --extra test pytest ...`; + frontend `cd apps/dreamverse/web && pnpm install && pnpm run build`. +8. Run `uv lock` to regenerate `uv.lock` with the new workspace member; commit + the lock change in the same PR. +9. Land D-12-A docstring caveat: mark `GpuPool` experimental/server-internal + until `run_async()` lands. This is tracked in + [`open-threads.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L294-L308). + +**Verification gate:** + +- `uv sync --locked --package dreamverse-server --extra test` succeeds (proves + `uv.lock` is in sync with the workspace declaration). +- `pre-commit run --files pyproject.toml uv.lock .pre-commit-config.yaml .gitignore apps/dreamverse/server/pyproject.toml docs/contributing/dreamverse-development.md` succeeds. +- `uv run --with build python -m build --sdist --wheel .` still excludes + `apps*` from the FastVideo wheel/sdist (verify with `unzip -l dist/*.whl | grep apps` returns nothing). +- `lsp_diagnostics` is clean for changed Python files. +- No root `package.json` exists. +- `git check-ignore -v apps/dreamverse/web/public/k2.png` reports nothing + (asset unignore works) — only verifiable in Phase 3 once the file exists, + but the rule lands in Phase 1. + +**Rollback:** + +- Revert the Phase 1 PR. +- No product files have moved yet, so rollback does not affect Dreamverse repo. +- If only uv workspace sync fails, revert the workspace stanza and keep the + empty directory PR blocked until the pyproject shape is fixed. + +### Phase 2 — Move PRODUCT files (server) + +**Purpose:** Move the Dreamverse product backend into the monorepo while +rewiring imports to public FastVideo surfaces. + +**Files to move:** + +- `Dreamverse/server/main.py` → `apps/dreamverse/server/main.py` +- `Dreamverse/server/config.py` → `apps/dreamverse/server/config.py` +- `Dreamverse/server/video_generation.py` → `apps/dreamverse/server/video_generation.py` +- `Dreamverse/server/session/controller.py` → `apps/dreamverse/server/session/controller.py` +- `Dreamverse/server/session/messages.py` → `apps/dreamverse/server/session/messages.py` +- `Dreamverse/server/routes/presets.py` → `apps/dreamverse/server/routes/presets.py` +- `Dreamverse/server/tests/test_config.py` → `apps/dreamverse/server/tests/test_config.py` +- `Dreamverse/server/tests/test_entrypoints.py` → `apps/dreamverse/server/tests/test_entrypoints.py` +- `Dreamverse/server/tests/test_realtime_stress.py` → `apps/dreamverse/server/tests/test_realtime_stress.py` +- `Dreamverse/server/tests/test_session_logging.py` → `apps/dreamverse/server/tests/test_session_logging.py` +- `Dreamverse/server/tests/conftest.py` → `apps/dreamverse/server/tests/conftest.py` + +**Files NOT moved as PRODUCT (deleted in Phase 2 because public substitute exists):** + +- `Dreamverse/server/gpu_pool.py` — replaced by `fastvideo.entrypoints.streaming.gpu_pool` +- `Dreamverse/server/av_streaming.py` — replaced by `fastvideo.entrypoints.streaming.stream` +- `Dreamverse/server/worker_ipc.py` — replaced by `fastvideo.entrypoints.streaming.protocol` +- `Dreamverse/server/mock_server.py` — replaced by `fastvideo.entrypoints.streaming.mock_server` +- `Dreamverse/server/session_init_image.py` — replaced by `fastvideo.entrypoints.streaming.session_init_image` +- `Dreamverse/server/session_logger.py` — replaced by `fastvideo.entrypoints.streaming.session_logger` + +**Files MOVED AS-IS into `apps/dreamverse/server/` despite being technically generic — these are GENERIC-PENDING that haven't landed publicly yet, and Dreamverse won't boot without them:** + +| Source path | Target | Reason carried into Phase 2 | +|---|---|---| +| `Dreamverse/server/runtime.py` (21) | `apps/dreamverse/server/runtime.py` | [`main.py:19`](file:///home/william5lin/Dreamverse/server/main.py#L19) and `controller.py` import `runtime` for the runtime singletons; promote to public in Phase 4. | +| `Dreamverse/server/utils.py` (21) | `apps/dreamverse/server/utils.py` | Used by `controller.py` for timestamps + segment-cap helpers; absorb into public utility in Phase 4. | +| `Dreamverse/server/prompt_safety.py` (222) | `apps/dreamverse/server/prompt_safety.py` | Imported by `main.py` startup; promote to `fastvideo.entrypoints.streaming.prompt.safety` in Phase 4. | +| `Dreamverse/server/rewrite_prompt_payload.py` (98) | `apps/dreamverse/server/rewrite_prompt_payload.py` | Imported by `routes/presets.py` and `controller.py`; promote to `fastvideo.entrypoints.streaming.prompt.rewrite` in Phase 4. | +| `Dreamverse/server/routes/health.py` (101) | `apps/dreamverse/server/routes/health.py` | Mounted at startup by `main.py`; needed for FE Playwright tests. Promote to `fastvideo.entrypoints.streaming.server.build_app` in Phase 4. | +| `Dreamverse/server/prompt_enhancer.py` (1933) | `apps/dreamverse/server/prompt_enhancer.py` | DR-1 fork; replaced in Phase 5 with public + thin compat shim. | + +These are **explicit shims** — Phase 4 is responsible for their promotion to public surfaces and corresponding deletion from `apps/dreamverse/server/`. + +**Required import rewrites in moved PRODUCT files:** + +- `from .gpu_pool import GpuPool` → `from fastvideo.entrypoints.streaming.gpu_pool import GpuPool` +- `from .av_streaming import ...` → `from fastvideo.entrypoints.streaming.stream import ...` +- `from .worker_ipc import ...` → `from fastvideo.entrypoints.streaming.protocol import ...` +- `from .mock_server import ...` → `from fastvideo.entrypoints.streaming.mock_server import ...` +- `from .session_init_image import ...` → `from fastvideo.entrypoints.streaming.session_init_image import ...` +- `from .session_logger import ...` → `from fastvideo.entrypoints.streaming.session_logger import ...` +- Typed request/result/state imports → `fastvideo.api` +- `runtime`, `utils`, `prompt_safety`, `rewrite_prompt_payload`, `routes.health`, `prompt_enhancer` — kept relative (`from .runtime import ...`) inside `apps/dreamverse/server/` until Phase 4/5 promotes them. + +**Required path rewrites in moved files:** + +- [`config.py:13`](file:///home/william5lin/Dreamverse/server/config.py#L13): `_APP_ROOT / "apps" / "web"` → `_APP_ROOT / "web"` (since `_APP_ROOT` will resolve to `apps/dreamverse/` in the new layout). +- [`apps/web/next.config.ts:11`](file:///home/william5lin/Dreamverse/apps/web/next.config.ts#L11): `outputFileTracingRoot: path.resolve(__dirname, "../..")` → `path.resolve(__dirname, "../../..")` (one extra `..` since the FE is one level deeper in the monorepo). +- [`playwright.config.ts:39`](file:///home/william5lin/Dreamverse/apps/web/playwright.config.ts#L39): `command: "npm run dev"` → `command: "pnpm run dev"` (matches Phase 1 tooling decision). +- Any hardcoded `../FastVideo` paths in scripts/configs → make repo-root-relative since they now share a repo. + +**Steps:** + +1. **Cross-repo move with explicit history disposition.** `git mv` does NOT preserve history across repositories. Choose one: + - **(Recommended) Fresh import**: `git mv` from `Dreamverse/server/...` → `apps/dreamverse/server/...` within the new monorepo as a single commit; original Dreamverse repo retains full history; mention the move source SHA in the commit body. + - **(Alternative) Subtree merge**: use `git subtree add --prefix=apps/dreamverse ` to import history. Heavier; usually unnecessary if Dreamverse repo is being archived (history stays accessible there). + - The decision **must be documented in the Phase 2 PR body**, including the SHA of the originating Dreamverse commit being imported. +2. Rewire imports to public streaming and API modules per the rewrite list above. +3. Apply path rewrites listed above. +4. Add the import contract test from the Target architecture section. +5. Keep `routes/presets.py` product-local. +6. Keep `session/controller.py` product-local. +7. Verify D-8 `ltx2_image_crf` flow after migration: + - Source item is tracked in + [`open-threads.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L51-L68). + - The known concern is that `ltx2_image_crf` may still be silently dropped + unless it reaches `request.stage_overrides.refine.image_crf`. + - Add a Dreamverse-shape contract test pinning the field once the trace is verified. + +**Verification gate:** + +- **CI / unit / integration**: + `uv run --locked --package dreamverse-server --extra test pytest apps/dreamverse/server/tests/ -m 'not gpu' -q` + succeeds (skips GPU-marked tests; runs in `ci-dreamverse-backend.yml`). +- Import contract test (`test_import_contract.py`) passes. +- `lsp_diagnostics` is clean for changed Python files. +- `pre-commit run --files apps/dreamverse/server/*.py apps/dreamverse/server/session/controller.py apps/dreamverse/server/session/messages.py apps/dreamverse/server/routes/presets.py apps/dreamverse/server/tests/*.py` succeeds. +- D-8 trace is documented in the PR body and either passes or opens a blocking + fix item. +- No imports from internal `fastvideo.pipelines`, `fastvideo.models`, + `fastvideo.layers`, or `fastvideo.worker` remain under + `apps/dreamverse/server/`. +- **Manual QA on GPU4** (required, not optional): + - Reclaim GPU4 (kill running dreamverse-server PID 2453227). + - `CUDA_VISIBLE_DEVICES=4 uv run --locked --package dreamverse-server fastvideo serve --config apps/dreamverse/serve_configs/streaming_demo.yaml --host 0.0.0.0 --port 8009` boots cleanly. + - `curl -s http://localhost:8009/health` returns 200. + - `pytest apps/dreamverse/server/tests/ -m gpu -q` against the live deploy passes + (this exercises `test_realtime_stress.py` and `test_session_logging.py` + with a real GPU + real model weights). + - Capture output in PR description. + - Cleanup: `pkill -f 'fastvideo serve --config'` and restart the canonical + Dreamverse-side server. + +**Rollback:** + +- Revert the Phase 2 PR. +- Dreamverse source repo remains canonical until Phase 7, so no production + rollback is needed. +- If only one public import is missing, add a temporary product-local shim only + if it is explicitly deleted in Phase 4 or Phase 5. +- If the GPU4 manual QA reveals a real-service bug, the Phase 2 PR is + blocked; fix the bug and re-run the GPU4 smoke before merging. + +### Phase 3 — Move FE + content + +**Purpose:** Move the Dreamverse frontend, prompts, serve configs, launch +scripts, and product docs after backend tests are green. + +**Files to move:** + +- `Dreamverse/apps/web/**` → `apps/dreamverse/web/**` +- `Dreamverse/server/prompts/*.md` → `apps/dreamverse/server/prompts/*.md` +- `Dreamverse/AGENTS.md` → `apps/dreamverse/AGENTS.md` +- `Dreamverse/arch.md` → `apps/dreamverse/arch.md` +- `Dreamverse/design.md` → `apps/dreamverse/design.md` +- `Dreamverse/gpu-pool.svg` → `apps/dreamverse/gpu-pool.svg` +- `Dreamverse/gpu-pool.drawio` → `apps/dreamverse/gpu-pool.drawio` +- `Dreamverse/scripts/install_native_ffmpeg.sh` → `apps/dreamverse/scripts/install_native_ffmpeg.sh` +- `Dreamverse/scripts/smoke_local.sh` → `apps/dreamverse/scripts/smoke_local.sh` +- `Dreamverse/serve_configs/streaming_demo.yaml` → `apps/dreamverse/serve_configs/streaming_demo.yaml` +- `Dreamverse/serve_configs/streaming_demo.yaml` → `examples/serving/streaming_demo.yaml` as a public-safe copy. +- `Dreamverse/.agents/skills/launch-demo/scripts/*.sh` → `apps/dreamverse/scripts/launch/*.sh` + +**CI additions:** + +- Add `.github/workflows/ci-dreamverse-frontend.yml`. +- Ensure `.gitignore` covers scoped Next.js and Playwright artifacts. + +**Steps:** + +1. Move frontend tree without restructuring package internals. +2. Update relative paths in frontend scripts/configs from old repo root to + `apps/dreamverse/` or FastVideo root as appropriate. +3. Move product prompts and docs. +4. Copy `streaming_demo.yaml` to `examples/serving/streaming_demo.yaml` with + public-safe comments. The missing example config is tracked in + [`integration-review.md`](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L259-L276). +5. Add frontend workflow. +6. Add docs/contributing link if not already added in Phase 1. + +**Verification gate:** + +- `cd apps/dreamverse/web && pnpm install --frozen-lockfile` succeeds. +- `cd apps/dreamverse/web && pnpm run build` succeeds. +- `cd apps/dreamverse/web && pnpm run test --if-present` succeeds (Vitest + tsc). +- **Frontend CI Playwright is intentionally DEFERRED to Phase 4** — at Phase 3 + the public `build_app` does not yet expose `/healthz`+`/readyz`+`/status`+ + `/prompt-system-config`+`/curated-presets`. The `ci-dreamverse-frontend.yml` + scaffold from Phase 1 keeps Playwright steps commented out until Phase 4 + reactivates them. No PR note required. +- **Manual GPU4 Playwright smoke** (recommended): on this dev node, run + `apps/dreamverse/web/e2e/frontend-shell.spec.ts` against the GPU4-deployed + backend from Phase 2 manual QA + a `pnpm run dev` frontend at port 5274 + to confirm shell hydration. `backend-health.spec.ts` and + `preset-prompt-generation.spec.ts` will fail until Phase 4 — that is + expected; document as deferred. +- `uv run --locked --package dreamverse-server --extra test pytest apps/dreamverse/server/tests/ -m 'not gpu' -q` + still succeeds. +- `examples/serving/streaming_demo.yaml` parses with FastVideo serve config. + +**Rollback:** + +- Revert the Phase 3 PR. +- If frontend CI is the only failing component, revert only the frontend move + and keep Phase 2 backend in monorepo. +- If the public example config is wrong, revert the example copy separately; + product deployment config can remain under `apps/dreamverse/serve_configs/`. + +### Phase 4 — Promote GENERIC-PENDING + close drift items + +**Purpose:** Finish generic backend promotions that should not live in +Dreamverse product code. + +**Files likely to touch:** + +`fastvideo/entrypoints/streaming/{server.py,health.py,gpu_pool.py,session_store.py,blob_store.py,prompt/safety.py,prompt/rewrite.py}`, +`fastvideo/tests/entrypoints/streaming/`, `apps/dreamverse/server/{main.py,config.py}`. + +**Work items:** + +1. Add `/healthz`, `/readyz`, `/status` to generic `build_app` ([contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L232-L259), [open item](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99)). +2. Add `GpuPool.run_async() -> AsyncIterator[VideoEvent]`; keep sync `run()` as collector ([D-12](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L47-L116), [D-12-B](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L310-L325)). +3. Decide and document `video_position_offset_sec` semantics ([VPO](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L119-L144)). +4. Define SessionStore/BlobStore lifecycle policy ([SBS](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L277-L292)). +5. Promote or absorb `runtime.py`, `utils.py`, prompt safety, rewrite payload, + and health tests according to the generic-pending table. +6. Update Dreamverse server imports to consume the new generic public modules. + +**Verification gate:** + +- FastVideo streaming tests pass (existing CI suite). +- Dreamverse backend tests pass (`-m 'not gpu'` in CI; `-m gpu` on GPU4 manual). +- Contract tests for `/healthz`, `/readyz`, `/status` cover both healthy and + not-ready states (added in this phase to `fastvideo/tests/entrypoints/streaming/`). +- `run_async()` cancellation test proves client disconnect can stop mid-work. +- VPO decision is documented in code comments/tests. +- SessionStore/BlobStore lifecycle policy has tests for replacement cleanup and + disconnect expiry if implemented. +- **Frontend CI Playwright RE-ENABLED** at end of this phase: uncomment the + Playwright steps in `ci-dreamverse-frontend.yml` (deferred since Phase 1) + AND change CI to either (a) run against the mock backend + `fastvideo.entrypoints.streaming.mock_server`, or (b) require a deployed + staging URL via env var. +- **Manual GPU4 full E2E**: deploy `fastvideo serve` on GPU4 with the new + health routes, run all 3 Playwright specs (`backend-health`, `frontend-shell`, + `preset-prompt-generation`) against it. All 3 must pass — this is the + re-baseline after Phase 3's deferral. Capture output in PR. + +**Rollback:** + +- Each generic-pending promotion should be a focused PR. +- If a generic promotion regresses FastVideo, revert that PR and keep the + product-local code temporarily under `apps/dreamverse/server/`. +- If health route compatibility is wrong, revert route additions but keep + tests skipped only with an explicit tracking issue. + +### Phase 5 — Replace prompt enhancer fork (DR-1) + `cerebras_ifm` decision (DR-2) + +**Purpose:** Remove the 1933-LOC Dreamverse prompt enhancer fork and keep only +product-specific prompt orchestration. + +**Files likely to touch:** `apps/dreamverse/server/prompting/_internal_compat.py` (new), `main.py`, `config.py`, `session/controller.py`, product prompt tests, and only if DR-2 picks public provider: `fastvideo/entrypoints/streaming/prompt/providers/` plus `fastvideo/api/schema.py`. + +**DR-1 migration shape:** create `_internal_compat.py` over public `PromptEnhancer`, preserve product response shapes, keep locked segment metadata / rollout IDs / lenient JSON fallback product-local, then delete the full fork ([DR-1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L174-L206)). + +**DR-2 decision:** pick public `cerebras_ifm` provider + Literal, or Dreamverse-side custom provider registered with public enhancer. Default: Dreamverse-side unless another user needs IFM ([DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229)). + +**Verification gate:** + +- Product prompt tests pass through `_internal_compat.py`. +- Public prompt enhancer tests remain green. +- `cerebras`, `groq`, and selected `cerebras_ifm` path have unit tests or + mocked-provider tests. +- Playwright preset prompt generation test passes against live BE+FE, matching + the existing Dreamverse-side verification gate + ([open-threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L495-L498)). + +**Rollback:** + +- Keep the deleted fork recoverable in git history. +- If product prompt behavior regresses, revert Phase 5 only and restore the + product-local fork temporarily. +- Do not roll back Phase 2 or Phase 3 for a prompt-specific regression. + +### Phase 6 — Switch CI/release + +**Purpose:** Make monorepo CI/deploy authoritative after code migration. **Splits into 6 substeps to avoid coupling unrelated changes.** + +**Files:** `ci-dreamverse-{backend,frontend}.yml` (already added in Phase 1/3), new `deploy-dreamverse-{backend,frontend}.yml`, `apps/dreamverse/README.md`, `docs/contributing/dreamverse-development.md`, README links. + +#### Phase 6a — CI path-filter proof +- Land a no-op test PR that touches only `apps/dreamverse/web/README.md` and verify ONLY `ci-dreamverse-frontend.yml` runs (not Buildkite full suite, not `ci-precommit` if `paths-ignore` applied). +- Land a no-op test PR that touches only `apps/dreamverse/server/README.md` and verify ONLY `ci-dreamverse-backend.yml` + `ci-precommit` run. +- Land a no-op test PR that touches `fastvideo/api/__init__.py` (a no-op comment) and verify BOTH FastVideo CI AND `ci-dreamverse-backend.yml` run (because the path filter includes `fastvideo/api/**`). +- Document the verification PR URLs in the Phase 6a merge commit. + +#### Phase 6b — Backend deploy dry run +- Add `deploy-dreamverse-backend.yml` with a manual dispatch trigger only (no `push` trigger yet). +- Run a manual deploy to a **staging** environment (not production). +- Verify the deployed backend serves `/healthz`, `/readyz`, `/status`, `/v1/stream` correctly using public Dreamverse FE pointing at the staging URL. + +#### Phase 6c — Frontend deploy dry run +- Add `deploy-dreamverse-frontend.yml` (Vercel or Docker depending on team choice) with manual dispatch only. +- Deploy to a Vercel preview / staging Docker. +- Verify product Playwright e2e against the staging FE+BE pair. + +#### Phase 6d — Staging cutover +- Switch the staging environment to use ONLY the monorepo deploys. +- Disable the Dreamverse repo's deploy workflows for staging. +- Run for at least 24-48 hours to observe stability. + +#### Phase 6e — Old-repo CI freeze +- Disable Dreamverse repo CI workflows (set them to manual-only or remove). +- Update Dreamverse repo's README to point to the FastVideo monorepo for new contributions. +- DO NOT archive the repo yet — that's Phase 7. + +#### Phase 6f — Production cutover +- Switch production deploy to use monorepo workflows. +- Add `push: branches: [main]` triggers to deploy workflows now that they're proven. +- Update DNS / load-balancer / external link integrations. +- Monitor for 1 week before Phase 7. + +**Verification gate:** Each substep has its own gate; do not proceed to next substep without the prior one passing. Final gate after 6f: monorepo production deploys are stable for 1 week with no Dreamverse-repo dependency in the production path. + +**Rollback:** Each substep is independently revertible. Rolling back Phase 6f means switching deploy back to Dreamverse repo (which Phase 6e left functional but disabled). Rolling back Phase 6e means re-enabling Dreamverse CI. The 6a-6c substeps create no production dependency and can be reverted by deleting workflow files. + +### Phase 7 — Archive Dreamverse repo + +**Purpose:** Finish consolidation only after monorepo product CI/deploy are proven. + +**Steps:** commit Dreamverse README redirect to `hao-ai-lab/FastVideo/apps/dreamverse/`, tag/branch final Dreamverse state for rollback, disable old CI, archive repo, update external links, and update this memory dir. + +**Verification gate:** backend and frontend monorepo deploys are live; product docs and Dreamverse README redirect point to FastVideo; no production job depends on the old repo. + +**Rollback:** unarchive Dreamverse, restore backup branch/tag, disable monorepo deploy, and point external links back until fixed. + +--- + +## Risk register + +| Risk | Severity | Likelihood | Mitigation | Owner | +|---|---|---:|---|---| +| Build-system complexity from adding a product app to a Python ML repo | High | Medium | Use uv workspace for Python only; keep frontend standalone; exclude `apps*` from wheel. | FastVideo build owner | +| Python contributors needing Node | Medium | Medium | No root `package.json`; frontend CI and commands live under `apps/dreamverse/web/`. | Frontend owner | +| CI cost increase | Medium | Medium | Add Dreamverse path-specific workflows; add `paths-ignore` to broad workflows; rely on Buildkite monorepo diff watch lists. | CI owner | +| FastVideo PyPI release accidentally includes Dreamverse app | High | Low | Add `apps*` to `[tool.setuptools.packages.find]` and `[tool.wheel]` excludes; verify built wheel contents. | Release owner | +| Release cadence coupling | Medium | Medium | Keep FastVideo PyPI version release unchanged; Dreamverse uses Docker/Vercel deploy from app paths. | Release owner | +| Frontend tooling drift | Medium | Medium | Pin pnpm lockfile in `apps/dreamverse/web/`; no root JS workspace. | Frontend owner | +| Security surface enlargement | Medium | Medium | Product routes stay in `apps/dreamverse/server`; only generic health/streaming routes go into FastVideo. | Backend owner | +| Product-specific API leakage into `fastvideo.*` | High | Medium | Enforce import/module boundary; keep curated presets and prompt UX product-local. | Architecture owner | +| Migration regression | High | Medium | Phase gates; backend before frontend; can stop after any phase with Dreamverse repo still usable until Phase 7. | Migration owner | +| Lost git history on moved files | High | High | **`git mv` does not preserve history across repositories.** Choose between (a) fresh import (no history; record source SHA in PR body; original history stays in archived Dreamverse repo) or (b) `git subtree add` / `git filter-repo` import (preserves history but heavier). Document the choice in Phase 2 PR. | Migration owner | +| Migrated public assets ignored by root `.gitignore` | High | High | Root ignores `*.png`/`*.jpg`/`*.mp4`/`*.gif`; Phase 1 must add `!apps/dreamverse/web/public/**` etc. unignore rules. Verify with `git check-ignore -v` before Phase 3 commits. | Migration owner | +| CORS open to `*` in product server | Medium | Medium | [`Dreamverse/server/main.py:99`](file:///home/william5lin/Dreamverse/server/main.py#L99) sets `allow_origins=["*"]`. After move, decide: (a) keep dev-only and document; (b) tighten to known FE origins; (c) deployment-firewall. Document in `apps/dreamverse/server/README.md`. | Backend owner | +| Write endpoints (`/prompt-system-config`, `/curated-presets/append`) | Medium | Medium | [`routes/presets.py:106`](file:///home/william5lin/Dreamverse/server/routes/presets.py#L106) exposes write routes. Decide auth/rate-limit policy; either gate behind devtools origin OR add auth before production cutover. | Backend owner | +| Prompt enhancer behavior regression | High | Medium | Build DR-1 shim with product tests before deleting fork; keep rollback path to restore fork. | Prompt owner | +| `cerebras_ifm` production gap | Medium | Medium | Decide DR-2 before Phase 5 merge; default Dreamverse-side provider unless public user exists. | Product owner | +| Health route compatibility gap | High | Medium | Land `/healthz`, `/readyz`, `/status` in generic build_app with tests before FE switches to FastVideo flavor. | Streaming owner | +| `video_position_offset_sec` ambiguity | Medium | Medium | Decide VPO semantics in Phase 4; encode in tests. | Audio/runtime owner | +| Session/blob memory leak | Medium | Medium | Define lifecycle policy before high-traffic deploy; add TTL/replacement cleanup tests. | Runtime owner | +| Existing FastVideo CI false positives on frontend-only changes | Medium | Medium | Apply path filters; verify with a frontend-only test PR. | CI owner | +| Dreamverse repo archive too early | High | Low | Archive only after monorepo staging deploy and production deploy are proven. | Release owner | + +--- + +## Verification gates + +| Phase | Required checks | Stop condition | +|---|---|---| +| 0 | #1288 merged; public typed events and `generate_async` import; baseline API/contract tests green. | #1288 missing or regressed. | +| 1 | uv workspace sync; pre-commit on changed root/app files; wheel excludes `apps*`; no root `package.json`. | FastVideo package build includes app code or uv workspace cannot sync. | +| 2 | Dreamverse backend tests green; import contract green; D-8 trace complete; lsp/pre-commit clean. | Product server imports private FastVideo modules or D-8 is silently dropped. | +| 3 | Frontend install/build/test/Playwright green; backend tests still green; public example config parses. | Frontend cannot build from `apps/dreamverse/web/`. | +| 4 | Streaming tests green; health routes covered; `run_async()` cancellation covered; VPO/SBS decisions encoded. | Generic backend regression or unresolved route contract. | +| 5 | Prompt shim tests green; selected IFM path works; product Playwright preset prompt flow green. | Prompt behavior differs from product expectations. | +| 6 | Backend-only and frontend-only CI PRs trigger correct workflows; staging deploy succeeds. | Existing FastVideo CI is noisy for frontend-only changes or deploy fails. | +| 7 | Monorepo production deploy proven; old repo redirect committed; no jobs depend on old repo. | Any production dependency still points to Dreamverse repo. | + +Global verification requirements: + +- `lsp_diagnostics` clean on changed Python files. +- `pre-commit run --files ` for non-excluded files. +- Relevant tests pass before each phase merges. +- Do not run GPU-heavy full suite for frontend-only changes. +- Capture command output in PR descriptions. + +--- + +## Rollback strategy + +| Phase | Rollback path | Data loss risk | Notes | +|---|---|---:|---| +| 0 | Do not proceed until #1288 is fixed or reverted. | Low | No Dreamverse files moved. | +| 1 | Revert skeleton/tooling PR. | Low | Empty dirs and root tooling only. | +| 2 | Revert backend move PR; Dreamverse repo remains source of truth. | Low | Move commits do NOT preserve cross-repo history (`git mv` only preserves within a single repo). Original Dreamverse history stays in the archived Dreamverse repo. The Phase 2 PR body must record the originating Dreamverse SHA(s) for traceability. | +| 3 | Revert frontend/content move PR; keep backend if green. | Low | Product assets still exist in Dreamverse repo. | +| 4 | Revert individual generic promotion PR; temporarily keep product-local shim. | Medium | Avoid reverting unrelated generic backend changes. | +| 5 | Restore prompt fork from previous commit; keep shim behind flag if useful. | Medium | Prompt behavior rollback only. | +| 6 | Disable monorepo deploy workflows; re-enable Dreamverse repo CI/deploy. | Low | Code can remain in monorepo while deploy routes old repo. | +| 7 | Unarchive Dreamverse repo; restore from backup branch; turn off monorepo deploy. | Medium | Requires external link rollback. | + +Rollback principles: + +- Never archive Dreamverse before monorepo deploy is proven. +- Keep each phase as a separate PR or commit group. +- Keep generic backend promotions independent from product moves. +- Keep `apps*` wheel exclusion in place even if product phases are reverted. +- If frontend migration fails, do not roll back generic backend work. + +--- + +## Open questions / decisions needed + +| Decision | Needed by | Default / note | Source | +|---|---|---|---| +| `cerebras_ifm` provider path | Phase 5 | Dreamverse-side custom provider unless another public user exists | [DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229) | +| `video_position_offset_sec` semantics | Phase 4 | Decide persistent vs per-segment; decision is overdue | [VPO](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L361-L364) | +| SessionStore/BlobStore lifecycle | Phase 4 | Define TTL/LRU/hard max/blob cleanup/disconnect expiry | [SBS](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L277-L292) | +| Health route response shapes | Phase 4 | Match Dreamverse `/healthz`, `/readyz`, `/status` before FE switch | [health routes](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99) | +| Benchmark destination | Phase 4 | Pick `fastvideo/tests/benchmarks/` or root `benchmarks/` before moving files | migration table above | +| Standalone upsampler CLI | Deferred | Defer or port to FastVideo CLI | [upsampler](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L239-L258) | +| Layerwise offload utility | Deferred | Defer unless a deployment requires it | [offload](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L224-L238) | + +--- + +## Action items + +| # | Pri | Effort | Phase | Owner | Action | +|---:|---|---|---|---|---| +| 1 | P0 | L | 0 | FastVideo backend | Land #1288 and update memory state. | +| 2 | P0 | S | 1 | Build | Add uv workspace, `apps*` excludes, web pre-commit exclude, scoped `.gitignore`. | +| 3 | P0 | S | 1 | Docs | Add `docs/contributing/dreamverse-development.md` and optional `mkdocs.yml` nav. | +| 4 | P1 | M | 2 | Product backend | Move backend files and add import contract test. | +| 5 | P1 | S | 2 | Runtime | Trace D-8 `ltx2_image_crf` and add/adjust test. | +| 6 | P1 | M | 3 | Frontend | Move Next.js frontend and land frontend CI. | +| 7 | P2 | S | 3 | Examples | Copy `streaming_demo.yaml` to `examples/serving/streaming_demo.yaml`. | +| 8 | P1 | M-L | 4 | Streaming | Add `/healthz`, `/readyz`, `/status` to `build_app` with tests. | +| 9 | P2 | M | 4 | Streaming | Add `GpuPool.run_async()` and cancellation propagation. | +| 10 | P2 | S | 4 | Audio/runtime | Decide VPO semantics and encode in tests. | +| 11 | P2 | M | 4 | Runtime | Define SessionStore/BlobStore lifecycle policy. | +| 12 | P1 | M | 5 | Prompt | Replace prompt enhancer fork with `_internal_compat.py` shim. | +| 13 | P1 | S-M | 5 | Product | Decide and implement `cerebras_ifm` path. | +| 14 | P1 | M | 6 | CI | Prove split backend/frontend CI and no FastVideo CI noise on frontend-only changes. | +| 15 | P1 | M | 6 | Release | Add product deploy workflows from monorepo paths. | +| 16 | P2 | S | 7 | Release | Archive Dreamverse repo after redirect commit and monorepo deploy proof. | +| 17 | P3 | S-M | Deferred | CLI | Decide standalone upsampler CLI. | +| 18 | P3 | M | Deferred | Performance | Defer layerwise offload until deployment evidence exists. | + +--- + +## Cross-references + +- Repo paths: [README](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/README.md#L73-L80). +- Typed API and schema: [design core](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L24-L43), [schema surface](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L45-L72). +- Continuation state: [cross-repo mapping](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L108-L146). +- Streaming routes: [route contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L232-L259). +- GpuPool and router decisions: [D-12](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L47-L116), [D-15](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L118-L201). +- Open work and verification: [open threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L15-L45), [gates](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L480-L498). +- Runbook: [verification commands](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/runbook.md#L79-L124). +- Previous tradeoff analysis: [integration review](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L371-L563). + +--- + +## Comparison to `integration-review.md` (now superseded for recommendation) + +`integration-review.md` recommended constrained Option D: generic backend in +FastVideo, Dreamverse product in a separate repo +([recommendation](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/integration-review.md#L543-L563)). + +The user chose Option B+ instead. + +Changed recommendation: Dreamverse product moves into `apps/dreamverse/`; generic backend stays at `fastvideo.entrypoints.streaming.*`; Dreamverse server does not become `fastvideo.entrypoints.dreamverse.*`; frontend remains standalone with no root `package.json`; FastVideo PyPI excludes `apps*`. + +Still authoritative from `integration-review.md`: drift audit findings, OSS comparison material not contradicted by Option B+, health route gap, DR-1/DR-2 prompt work, D-8 `ltx2_image_crf`, VPO/SBS decisions, missing public streaming demo config, and the decision to defer layerwise offload / standalone upsampler CLI unless product need is proven. + +Operationally, this plan is the execution document. Use it for Phase 1 onward. diff --git a/.agents/memory/dreamverse-integration/integration-review.md b/.agents/memory/dreamverse-integration/integration-review.md new file mode 100644 index 0000000000..504f7a7178 --- /dev/null +++ b/.agents/memory/dreamverse-integration/integration-review.md @@ -0,0 +1,888 @@ +# Integration Review — Drift Audit + Path Forward + +> # ⚠️ DEPRECATED — superseded by [integration-plan.md](integration-plan.md) +> +> This document recommended **Option D** (Dreamverse stays a separate repo; +> generic backend merges into FastVideo). On 2026-05-05 the team chose +> **Option B+** instead (Dreamverse FE + product server move into FastVideo +> as `apps/dreamverse/`; generic backend stays at +> `fastvideo.entrypoints.streaming.*` per Option D's principle). +> See [decisions-log.md D-18](decisions-log.md#d-18) for the strategy +> reversal rationale and [integration-plan.md](integration-plan.md) for the +> executable migration plan. +> +> **What's still authoritative in this file:** +> - **Part 1 — Drift audit** (the 17-row drift summary table). The drift +> findings remain valid; the migration plan in `integration-plan.md` +> folds them into specific phases. +> - **OSS precedent citations** (vLLM, BentoML, Ray Serve, TGI+ChatUI, +> Transformers.js, ComfyUI, AUTOMATIC1111). Reused in `integration-plan.md`. +> +> **What's superseded:** +> - **Part 2 — Recommendation (Option D)**. Replaced by Option B+ in the +> new plan. Read `integration-plan.md` for the current decision. +> - **Part 3 — Action items**. Replaced by the phased migration plan. +> +> Kept in tree for historical reference and audit trail. Do not delete. + +**Last updated:** 2026-05-05 (deprecated header added). + +**Scope:** FastVideo public `will/ltx2_sr_port` at the requested audit +anchor `b36bdbc9`; Dreamverse `will/integrate-public-fastvideo` at +`ec8ef92`; FastVideo-internal `will/rebase-nbv` as read-only comparison. + +**Memory-dir context:** the current integration memory snapshot tracks the +same public mega-PR lineage as `will/ltx2_sr_port`, with PRs #1257, +#1258, #1284, and #1286 already merged, #1287 closed, and #1288 open as +the consolidated landing vehicle for LTX-2 SR runtime, NVFP4, +`generate_async`, Dynamo contract, and memory-dir cleanup. Source: +[memory index](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/README.md#L8-L19) +and [D-17](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L19-L45). + +**Bottom line:** Zero core typed API drift — typed construction, typed +continuation state, NVFP4 wiring, and Dynamo-facing async events are either +already public or in #1288. **Real drift remains on the realtime-runtime +contract surface (`/healthz` / `/readyz` / `/status` routes per +[cross-repo-surfaces.md](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L74-L88)) +and on operational/product edges**: stale Dreamverse docs/scripts, a +1933-LOC Dreamverse prompt-enhancer fork, two unresolved per-session +fields (`ltx2_image_crf` D-8, `video_position_offset_sec` VPO), one +missing example config, and two internal-only utilities whose product +relevance is not yet proven. + +--- + +## Part 1 — Drift audit + +### Methodology + +1. **Compared three repositories and branches.** + - FastVideo public: `/home/william5lin/FastVideo`, branch + `will/ltx2_sr_port`. + - Dreamverse: `/home/william5lin/Dreamverse`, branch + `will/integrate-public-fastvideo`. + - FastVideo-internal: `/home/william5lin/FastVideo-internal`, branch + `will/rebase-nbv`. + - Canonical repo paths are listed in the integration memory index: + [repo paths](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/README.md#L72-L79). + +2. **Scoped the audit to the ultimate integration goal.** + - Dreamverse should depend on public `fastvideo`, not + `FastVideo-internal`. + - FastVideo should own the reusable backend subset that Dreamverse + currently needs from internal: streaming runtime, GPU pool, router, + prompt enhancer, NVFP4, continuation state, and typed generation. + - Dynamo should consume FastVideo through typed public Python APIs, not + through private modules. + - The three Dreamverse surfaces are documented as pipeline construction, + realtime runtime, and continuation state: + [cross-repo surfaces](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L13-L20). + +3. **Separated intentional refactor from drift.** + - A path rename is not drift if the public branch contains the same + responsibility under the typed design. + - A deleted file is not drift if the public design intentionally + consolidated it. + - A private alias is not drift if the public schema exposes a typed + replacement with contract tests. + - This matches the typed-public-boundary rule in + [design.md](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L24-L43). + +4. **Used memory docs for rationale and worktree files for concrete proof.** + - API schema and public exports: + [schema](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L68-L85), + [api exports](file:///home/william5lin/FastVideo/fastvideo/api/__init__.py#L49-L109). + - Streaming server current routes: + [build_app](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/server.py#L88-L160). + - Dreamverse dependency state: + [pyproject server extra](file:///home/william5lin/Dreamverse/pyproject.toml#L17-L22), + [uv lock editable source](file:///home/william5lin/Dreamverse/uv.lock#L716-L722). + - Contract tests: + [Dreamverse shape](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dreamverse_shape.py#L1-L26), + [Dynamo shape](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dynamo_shape.py#L1-L19), + [generate_async](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_generate_async.py#L1-L7). + +5. **Did not treat product-only Dreamverse behavior as FastVideo drift.** + - Dreamverse keeps a local product server and Next.js UI today: + [README baseline](file:///home/william5lin/Dreamverse/README.md#L5-L16). + - Product-only routes, curated presets, devtools, and frontend-specific + behavior belong in Dreamverse unless a second non-Dreamverse consumer + needs them. + +6. **Risk scale used below.** + - **P0:** blocks Dreamverse from running without FastVideo-internal. + - **P1:** blocks clean `BE_FLAVOR=fastvideo` or Dynamo/public API use. + - **P2:** reproducibility or maintenance drag. + - **P3:** optional parity or future memory/perf improvement. + +### Findings: zero core typed API drift + +The public branch is aligned with the goal on the **core typed API +surface** (construction, request, continuation state, async events). +The table below lists items that look like drift only if compared by +path name or legacy field name. They are intentional public refactors +or already guarded by tests. **Note:** the realtime-runtime _contract_ +surface (FE-required health routes) is a separate matter — see "real +drift items" §4 below. + +| Investigated item | Drift? | Evidence | Conclusion | +|---|---:|---|---| +| Dreamverse surface 1: pipeline construction | No | Dreamverse migrated from flat kwargs to typed `GeneratorConfig` at `d80c2a8`; mapping documented in [cross-repo surfaces](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L22-L47). | Stable public surface exists. | +| Dreamverse surface 2: realtime runtime | No on architecture; some route work remains | Runtime migration target is public `streaming/`, not internal `realtime/`: [streaming upstream](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L14-L31). | Rename/refactor is intentional. | +| Dreamverse surface 3: continuation state | No | Public typed `ContinuationState` plus LTX-2 state mapping are documented in [cross-repo surfaces](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L108-L146). | Public state is a superset of Dreamverse's data carrier. | +| Internal `fastvideo/entrypoints/realtime/` | No | Public design chooses parallel `fastvideo/entrypoints/streaming/`: [layout decision](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L55-L60), [current build_app](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/server.py#L88-L160). | Intentional rename plus typed-config rewrite. | +| Internal `configs/sample/` presets | No | Public PR 4 intentionally deleted `configs/sample/` and moved defaults to per-family presets plus `fastvideo/api/sampling_param.py`: [design](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L194-L205), [PR roadmap](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L21-L29). | Intentional consolidation. | +| LTX-2 pipeline presets | No | Public target is model-owned named presets and per-family colocation: [design](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L175-L205). | Public layout matches design. | +| Internal `use_fp4_linear` flag | No | Public typed quant carrier is `engine.quantization.transformer_quant`; schema field exists in [schema](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L68-L85), compat resolves it in [compat.py](file:///home/william5lin/FastVideo/fastvideo/api/compat.py#L267-L279). | Replaced by typed NVFP4 surface. | +| Public-only `transformer_quant` field | No | Public `FastVideoArgs` pins typed quant to `dit_config.quant_config`: [fastvideo_args](file:///home/william5lin/FastVideo/fastvideo/fastvideo_args.py#L220-L228), [apply logic](file:///home/william5lin/FastVideo/fastvideo/fastvideo_args.py#L260-L279). | Public superset, not drift. | +| Internal `config_model_path` | No | Public typed home is `generator.pipeline.components.config_root`: [design mapping](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L261-L269), [compat mapping](file:///home/william5lin/FastVideo/fastvideo/api/compat.py#L295-L299). | Alias is covered. | +| Internal flat video request fields | No | Public `GenerationRequest` nests `inputs`, `sampling`, `runtime`, `output`, `state`, `extensions`: [schema](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L193-L204). Internal legacy fields live in internal protocol at [protocol.py](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/openai/protocol.py#L64-L82). | Intentional request refactor. | +| Dreamverse typed init kwargs | No | Contract test asserts current Dreamverse load kwargs all land on typed fields, not `experimental`: [test_dreamverse_shape](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dreamverse_shape.py#L44-L135). | Guard in place. | +| Dreamverse request path | No | Contract test asserts request fields round-trip through typed `GenerationRequest`: [test_dreamverse_shape](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dreamverse_shape.py#L153-L197). | Guard in place. | +| Dynamo native backend shape | No | FastVideo's only obligation is stable typed Python API: [cross-repo contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L188-L210). | Dynamo should stay out of FastVideo. | +| `generate_async` event API | No | API exists in [video_generator](file:///home/william5lin/FastVideo/fastvideo/entrypoints/video_generator.py#L264-L332), event types exist in [results.py](file:///home/william5lin/FastVideo/fastvideo/api/results.py#L109-L164). | #1288 covers the async contract. | +| Dynamo request mapping | No | Authoritative source is the contract test [test_dynamo_shape](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dynamo_shape.py#L90-L175) which asserts `req.prompt`, `req.sampling.{height,width,num_frames,fps,num_inference_steps,guidance_scale,seed,negative_prompt}`, and `req.inputs.{image_path,video_path}` against the actual nested [`GenerationRequest` schema](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L193-L204). The `streaming-server.md` Dynamo mapping table mis-cites a `prompt -> sampling.prompt` path that no longer exists; the test is correct, the doc is stale and tracked for refresh. | Guard in place; companion doc needs minor refresh. | +| Public API exports | No | `VideoEvent`, `VideoResult`, and typed schema classes are exported from [fastvideo.api](file:///home/william5lin/FastVideo/fastvideo/api/__init__.py#L49-L109). | Integration imports resolve. | +| FastVideo-internal FP4/NVFP4 paths | No | Public NVFP4 files and roles are documented in [quantization](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/quantization.md#L24-L35); actual `NVFP4Config` documents lazy FlashInfer and public naming in [nvfp4_config.py](file:///home/william5lin/FastVideo/fastvideo/layers/quantization/nvfp4_config.py#L1-L19). | Public is typed superset. | +| AbsMaxFP8 refactor | No for Dreamverse | Public quant registry includes `AbsMaxFP8` and `NVFP4`: [quantization init](file:///home/william5lin/FastVideo/fastvideo/layers/quantization/__init__.py#L1-L8). AbsMaxFP8 failure is tracked as separate tech debt: [open threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L100-L117). | Not Dreamverse blocker. | +| Internal realtime API regression test | No | Public contract tests replace it: [Dreamverse contract](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dreamverse_shape.py#L1-L26), [Dynamo contract](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dynamo_shape.py#L1-L19), [generate_async tests](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_generate_async.py#L91-L230). | Better scoped guards exist. | +| Dreamverse dependency declaration | No | `server` extra declares `fastvideo>=0.1.7`: [pyproject](file:///home/william5lin/Dreamverse/pyproject.toml#L17-L22). Dev lock resolves editable public `../FastVideo`: [uv.lock](file:///home/william5lin/Dreamverse/uv.lock#L716-L722), [package source](file:///home/william5lin/Dreamverse/uv.lock#L777-L780). | Dependency is already switched in metadata/lock. | + +#### Core conclusion for the zero-typed-drift section + +The public typed API no longer needs to mirror `FastVideo-internal` file +paths. The correct test is whether Dreamverse and Dynamo can express their +needs through public typed objects and public entrypoints. On that test, +the **typed core** is covered (construction, request, continuation, +async events). The **runtime contract** still has health-route gaps — +see real drift §4. On the **typed core**: + +- `GeneratorConfig` and `GenerationRequest` cover construction and calls: + [schema surface](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L45-L72). +- `ServeConfig.streaming` covers the server envelope: + [schema](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L244-L279). +- `generate_async` covers streaming, OpenAI, and Dynamo on one substrate: + [video_generator](file:///home/william5lin/FastVideo/fastvideo/entrypoints/video_generator.py#L264-L332). +- Contract tests now encode the cross-repo shapes: + [Dreamverse](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dreamverse_shape.py#L70-L214), + [Dynamo](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dynamo_shape.py#L170-L331), + [async events](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_generate_async.py#L91-L273). + +### Findings: real drift items requiring action + +#### 1. Dreamverse README and bootstrap script still point at FastVideo-internal + +- **Priority:** P0 for a clean public-dependency story. +- **Effort:** Small. +- **Owner:** Dreamverse repo. +- **Evidence:** Dreamverse metadata already points at public FastVideo: + [pyproject](file:///home/william5lin/Dreamverse/pyproject.toml#L17-L22), + [uv source](file:///home/william5lin/Dreamverse/pyproject.toml#L54-L61), + [uv.lock](file:///home/william5lin/Dreamverse/uv.lock#L716-L722). +- **Drift:** README still tells users that `uv` resolves from + `../FastVideo-internal` and that bootstrap expects `../FastVideo-internal`: + [README](file:///home/william5lin/Dreamverse/README.md#L76-L109). +- **Drift:** bootstrap script still defaults to cloning the private repo and + verifying imports from that clone: + [script defaults](file:///home/william5lin/Dreamverse/.agents/skills/bootstrap-fastvideo-private-fork/scripts/bootstrap_fastvideo_private.sh#L7-L11), + [script clone flow](file:///home/william5lin/Dreamverse/.agents/skills/bootstrap-fastvideo-private-fork/scripts/bootstrap_fastvideo_private.sh#L33-L63), + [script import assertion](file:///home/william5lin/Dreamverse/.agents/skills/bootstrap-fastvideo-private-fork/scripts/bootstrap_fastvideo_private.sh#L66-L88). +- **Action:** Replace private-fork bootstrap with public FastVideo bootstrap + or delete the bootstrap once PyPI publication is the default path. +- **Do not overreach:** no FastVideo code change required. + +#### 2. Dreamverse carries a 1933-line prompt-enhancer fork + +- **Priority:** P1. +- **Effort:** Medium. +- **Owner:** Dreamverse repo, after public prompt enhancer is available. +- **Evidence:** Dreamverse local fork starts at + [server/prompt_enhancer.py](file:///home/william5lin/Dreamverse/server/prompt_enhancer.py#L1-L80). +- **Public replacement:** FastVideo now has provider-agnostic + `PromptEnhancer` with `enhance`, `auto_extend`, `rewrite`, and + `register_provider`: + [public enhancer](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/prompt/enhancer.py#L66-L142). +- **Provider extension point:** custom providers implement `LLMProvider`: + [provider protocol](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/prompt/providers/base.py#L63-L75). +- **Tracking:** DR-1 in open threads already defines the compat-shim shape: + [DR-1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L174-L206). +- **Action:** Replace the fork with a small Dreamverse shim that adapts + public `LLMResponse` to Dreamverse's product response objects and keeps + only product-only extras. +- **Do not overreach:** do not merge Dreamverse's full prompt product layer + into FastVideo unless a second consumer needs the same semantics. + +#### 3. `cerebras_ifm` provider is unresolved + +- **Priority:** P1 if Dreamverse needs IFM in production; P2 otherwise. +- **Effort:** Small decision plus small/medium implementation. +- **Owner:** Team decision; implementation either Dreamverse-side or public. +- **Public state:** `PromptEnhancerConfig.provider` is currently + `Literal["cerebras", "groq"]`: + [schema](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L229-L235). +- **Design note:** public Literal excludes `cerebras_ifm` today: + [streaming-server D-3](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L61-L100). +- **Tracking:** DR-2 already frames the public-vs-Dreamverse decision: + [DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229). +- **Recommended default:** implement IFM as a Dreamverse-side custom provider + registered through `enhancer.register_provider(...)` unless there is a + non-Dreamverse public user. + +#### 4. `/healthz`, `/readyz`, and `/status` are not in public `build_app` + +- **Priority:** P1 for `BE_FLAVOR=fastvideo` frontend compatibility. +- **Effort:** Medium/Large because route shapes need tests. +- **Owner:** FastVideo public. +- **Public current state:** `build_app` exposes `GET /health` and + `WS /v1/stream`: + [server.py](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/server.py#L126-L160). +- **Dreamverse expected state:** Dreamverse exposes `GET /healthz`, + `GET /readyz`, and `GET /status`: + [routes/health.py](file:///home/william5lin/Dreamverse/server/routes/health.py#L34-L79). +- **Tracking:** open item #1 documents route ownership and files likely to + touch: + [open threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99). +- **Design note:** `/curated-presets`, `/prompt-system-config`, and devtools + stay Dreamverse-side, with feature detection: + [streaming route contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L232-L259). + +#### 5. `fastvideo/models/layerwise_offload.py` exists only internally + +- **Priority:** P3 unless memory-tight Dreamverse deployments require it. +- **Effort:** Medium if adopted; low if documented as deferred. +- **Owner:** FastVideo public only if a concrete deployment needs it. +- **Internal evidence:** internal file defines async layerwise CPU offload + manager with pinned CPU memory and prefetch stream: + [layerwise_offload.py](file:///home/william5lin/FastVideo-internal/fastvideo/models/layerwise_offload.py#L1-L20), + [prefetch path](file:///home/william5lin/FastVideo-internal/fastvideo/models/layerwise_offload.py#L127-L180). +- **Public state:** no equivalent public file was identified in this audit. +- **Action:** defer unless Dreamverse or another public deployment hits a + memory ceiling that cannot be handled by existing offload knobs. +- **Decision rule:** if adopted, port as a generic offload utility with + tests; do not make it Dreamverse-specific. + +#### 6. Standalone LTX-2 upsampler CLI exists only internally + +- **Priority:** P2 for reproducibility; P3 for product runtime. +- **Effort:** Small/Medium after scope decision. +- **Owner:** FastVideo public if standalone upsampling is a supported user + workflow. +- **Internal utility:** `upscale_video_file(...)` reads an existing video, + prepares frame count/resolution, loads VAE + upsampler, and writes an mp4: + [upsample.py](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/upsample.py#L120-L180), + [write tail](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/upsample.py#L181-L202). +- **Internal CLI:** `fastvideo upsample` wrapper exists internally: + [cli/upsample.py](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/cli/upsample.py#L15-L35), + [CLI args](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/cli/upsample.py#L48-L130). +- **Public related functionality:** LTX-2 SR refine stage covers the + in-pipeline latent upsample/refine path: + [ltx2_refine.py](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L1-L22), + [upsample stage](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L116-L180). +- **Action:** decide whether standalone file-to-file upsampling is a public + CLI promise or whether the SR refine stage is sufficient. + +#### 7. Reproducible streaming demo config lives only in Dreamverse + +- **Priority:** P2. +- **Effort:** Small. +- **Owner:** FastVideo public. +- **Evidence:** canonical demo config currently lives at + [Dreamverse/serve_configs/streaming_demo.yaml](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L1-L12). +- **Config content:** it documents LTX-2 distilled model, one GPU, + no offload, compile settings, NVFP4, refine overrides, default request, + and streaming settings: + [generator block](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L31-L87), + [streaming block](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L108-L149). +- **Memory pointer:** design.md already treats this as the canonical + example: + [design YAML example](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L235-L239). +- **Action:** copy/adapt it into + `examples/serving/streaming_demo.yaml` with public-safe comments. + +#### 8. LTX-2 stage equivalence is a verification gap, not proven drift + +- **Priority:** P2. +- **Effort:** Medium if parity checks are added; small if only manual audit. +- **Owner:** FastVideo public. +- **Public state:** model-specific LTX-2 stages are colocated under + `fastvideo/pipelines/basic/ltx2/stages/`, consistent with the target + layout in [design.md](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/design.md#L175-L205). +- **Example public stage:** `ltx2_refine.py` explicitly says it is a + public-side port of the internal stage and describes the three-stage SR + flow: + [ltx2_refine.py](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L1-L22). +- **Action:** verify behavior for the six internal `ltx2_*` stage files + against public colocated stages. If a mismatch is found, file it as a + real drift item with a failing parity test. + +### Findings: deferred / accepted residual + +These items should not block the public-dependency transition. + +1. **StepVideo residual.** + - Dreamverse's model registry is LTX-2/LTX-2.3 only: + [Dreamverse config](file:///home/william5lin/Dreamverse/server/config.py#L28-L45). + - Internal local tests even stub StepVideo modules to keep LTX registry + tests focused: + [test_ltx2_registry.py](file:///home/william5lin/FastVideo-internal/tests/local_tests/test_ltx2_registry.py#L38-L61). + - Conclusion: accepted low-priority deferral unless Dreamverse adds a + StepVideo model. + +2. **Internal debug-only `FastVideoArgs` fields.** + - Internal debug fields exist around `FastVideoArgs` and stage/model sums: + [internal grep source](file:///home/william5lin/FastVideo-internal/fastvideo/fastvideo_args.py#L200-L203). + - They are debug-only and not a public user-facing integration surface. + - Conclusion: low-priority; do not add to public schema unless a debug + workflow requires them. + +3. **Private request aliases.** + - Public request schema is nested and strict: + [GenerationRequest](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L193-L204). + - Legacy OpenAI flat fields are compatibility input, not the canonical + public API: + [internal protocol](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/openai/protocol.py#L64-L82). + - Conclusion: no action beyond current compat tests. + +4. **`experimental["pipeline_config"]` escape hatch.** + - Dreamverse currently uses an explicit in-memory quant config because + typed `transformer_quant: "NVFP4"` does not expose `layer_profile`: + [quantization](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/quantization.md#L86-L97). + - Open thread #4 tracks `layer_profile`: + [open threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L244-L260). + - Conclusion: defer broader typed carrier design; add `layer_profile` + first if Dreamverse needs base/refine profile selection. + +5. **Router sticky routing and active-active semantics.** + - Public router intentionally ships active-passive first and defers + sticky/weighted routing: + [D-15](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L118-L155). + - Follow-ups are tracked: + [D-15 action items](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L175-L201). + - Conclusion: not drift; defer until load-balancing needs are real. + +6. **AbsMaxFP8 failure.** + - Pre-existing and not introduced by NVFP4: + [state](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/state.md#L154-L159), + [quantization](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/quantization.md#L202-L214). + - Conclusion: fix separately; not a Dreamverse public-dependency blocker. + +### Drift summary table + +| # | Item | Priority | Effort | Status | Tracked where | Next action | +|---:|---|---|---|---|---|---| +| 1 | Dreamverse README still names `../FastVideo-internal` | P0 | S | Real drift | [README lines](file:///home/william5lin/Dreamverse/README.md#L76-L109) | Update docs to public FastVideo / PyPI path. | +| 2 | Dreamverse private bootstrap clones internal repo | P0 | S | Real drift | [bootstrap script](file:///home/william5lin/Dreamverse/.agents/skills/bootstrap-fastvideo-private-fork/scripts/bootstrap_fastvideo_private.sh#L7-L11) | Replace or delete private bootstrap. | +| 3 | Dreamverse `prompt_enhancer.py` fork | P1 | M | Real drift | [DR-1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L174-L206) | Build compat shim over public enhancer. | +| 4 | `cerebras_ifm` provider path | P1/P2 | S-M | Real drift / decision | [DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229) | Choose public provider vs Dreamverse custom provider. | +| 5 | Health route mismatch | P1 | M-L | Real drift | [open item #1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99) | Add `/healthz`, `/readyz`, `/status` to public build_app. | +| 6 | Missing public streaming demo config | P2 | S | Real drift | [Dreamverse config](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L1-L12) | Add `examples/serving/streaming_demo.yaml`. | +| 7 | Standalone upsampler CLI | P2/P3 | S-M | Real drift if standalone CLI is desired | [internal CLI](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/cli/upsample.py#L15-L35) | Decide CLI promise; port or defer. | +| 8 | Layerwise offload utility | P3 | M | Optional internal-only residual (no Dreamverse deployment requires it today) | [internal manager](file:///home/william5lin/FastVideo-internal/fastvideo/models/layerwise_offload.py#L15-L20) | Defer until memory-tight deployment needs it. | +| 9 | LTX-2 stage equivalence | P2 | S-M | Verification gap | [public refine stage](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L1-L22) | Add targeted parity audit/test if needed. | +| 10 | StepVideo | P3 | M | Accepted residual | [Dreamverse model registry](file:///home/william5lin/Dreamverse/server/config.py#L28-L45) | No action unless Dreamverse adds StepVideo. | +| 11 | Debug-only fields | P3 | S | Accepted residual | [internal args](file:///home/william5lin/FastVideo-internal/fastvideo/fastvideo_args.py#L200-L203) | Do not publicize unless needed. | +| 12 | `layer_profile` typed quant knob | P2 | M | Tracked gap | [open item #4](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L244-L260) | Add typed layer profile if Dreamverse drops escape hatch. | +| 13 | `ltx2_image_crf` per-segment field flow (D-8) | P1 | S | Open verification gap — Dreamverse still passes `ltx2_image_crf=0.0` per [Dreamverse video_generation.py](file:///home/william5lin/Dreamverse/server/video_generation.py#L420-L435); needs trace-through to confirm it lands on `request.stage_overrides.refine.image_crf` rather than being silently dropped | [D-8 in open-threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L52-L68) | 10-min trace + add a Dreamverse-shape contract test pinning the field. | +| 14 | `video_position_offset_sec` semantics (VPO) | P1 | S | Open decision — persistent-vs-per-segment ambiguity unresolved | [VPO in open-threads](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L120-L144) | Confirm semantics with audio team; document + add test. Decision deadline was "before PR 7.6 emits state" — that PR (7.6 / #1257) is now MERGED, so the decision is overdue. | +| 15 | `GpuPool` ABC docstring missing experimental caveat (D-12-A) | P3 | trivial | Tracked gap — `GpuPool` ABC at [gpu_pool.py:74-83](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/gpu_pool.py#L74-L83) lacks the "API may change post-PR-7.10; experimental / server-internal" caveat | [D-12-A](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L301-L313) | Edit docstring; trivial. | +| 16 | `GpuPool.run_async()` migration (D-12-B) | P2 | M | Tracked gap — `GpuPool.run() -> Any` should become `run_async() -> AsyncIterator[VideoEvent]` per D-12 / D-12-B | [D-12-B](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L317-L327) | Land alongside #1288 merge or in immediate follow-up. | +| 17 | `SessionStore` / `BlobStore` lifecycle policy (SBS) | P2 | M | Tracked gap — in-memory defaults have no eviction/TTL/blob-cleanup policy | [SBS](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L278-L294) | Streaming server design pass needed before high-traffic deployment. | +| 13 | Router sticky / active-active | P3 | M | Deferred | [D-15](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L175-L201) | Defer until reconnect/load evidence. | +| 14 | AbsMaxFP8 test failure | P2 | S | Separate tech debt | [state](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/state.md#L154-L159) | Fix outside Dreamverse migration. | +| 15 | Dynamo backend package | P1 | External | Not FastVideo drift | [Dynamo contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L188-L210) | Reopen Dynamo-side PR after public API lands. | + +--- + +## Part 2 — Integration path tradeoffs + +### The four options + +#### Option A — Status quo: Dreamverse stays separate and depends on `fastvideo` + +**Shape** + +- FastVideo remains the Python library and reusable backend runtime. +- Dreamverse remains the product repo with FastAPI product glue and Next.js + frontend. +- Dreamverse `server` extra depends on `fastvideo>=0.1.7`: + [pyproject](file:///home/william5lin/Dreamverse/pyproject.toml#L17-L22). +- Local development can keep using editable `../FastVideo` until PyPI + publication catches up: + [uv.lock](file:///home/william5lin/Dreamverse/uv.lock#L716-L722). + +**What it solves** + +- Directly satisfies "Dreamverse depends on public FastVideo". +- Keeps frontend release cadence independent. +- Keeps product-specific prompts, routes, and UI in the product repo. +- Minimizes FastVideo packaging and CI growth. + +**What it does not solve by itself** + +- Does not remove Dreamverse prompt-enhancer fork unless DR-1 is executed. +- Does not give Dreamverse FE compatibility with public `build_app` until + health routes migrate. +- Does not make Dreamverse server itself reusable as a public entrypoint. + +**Best fit** + +- Default for the next release if the goal is to stop using + FastVideo-internal quickly and safely. + +#### Option B — Dreamverse as a subfolder under FastVideo + +**Shape** + +- One repository: FastVideo contains `dreamverse/server/` and + `dreamverse/apps/web/`. +- Dreamverse can remain a separate package in the same repo, or FastVideo's + build can ignore Dreamverse by default. +- CI must understand Python library tests plus Next.js install/build/test. + +**What it solves** + +- Eliminates sibling-checkout drift. +- Makes cross-repo integration changes atomic. +- Easier for a single reviewer to see library and product changes together. + +**Costs** + +- Adds frontend dependency management to a Python ML library repo. +- Couples clone size, CI setup, issue tracking, and review load. +- Forces maintainers to decide whether product assets are included in source + distributions, wheels, docs, and release notes. + +**Best fit** + +- Only if Dreamverse becomes the primary FastVideo product surface and the + team accepts a product monorepo. + +#### Option C — Full merge into `fastvideo.entrypoints.dreamverse.*` + +**Shape** + +- Dreamverse backend becomes FastVideo code. +- Public import becomes something like + `from fastvideo.entrypoints.dreamverse import build_app`. +- CLI becomes `fastvideo dreamverse-serve --config dreamverse.yaml`. +- Frontend either ships as static assets in the package or as a frontend + extra. + +**What it solves** + +- One namespace and one release train for library plus product backend. +- No dependency boundary between Dreamverse server and FastVideo internals. +- Product route contract can be tested entirely inside FastVideo CI. + +**Costs** + +- Maximally expands FastVideo's public/security surface. +- Locks product experiments to FastVideo release cadence. +- Makes private prompt/provider/product assumptions look like framework API. +- Has weak precedent for a Python ML library plus Next.js product being merged + into the library namespace. + +**Best fit** + +- Only if Dreamverse is no longer a separate product and becomes the + canonical FastVideo UI/serving mode. + +#### Option D — Hybrid: backend merges, frontend stays separate + +**Shape** + +- Reusable backend components merge into public FastVideo. +- Frontend stays in a separate Dreamverse UI repo or Dreamverse product repo. +- The backend should be generic where possible: `fastvideo.entrypoints.streaming`, + not product-only names, unless product-only routes are intentionally + accepted as public API. +- This matches the current trajectory: streaming server, GPU pool, prompt + enhancer, safety/rewrite/session logging, router, NVFP4, and + `generate_async` are public-side work already tracked in the PR roadmap: + [pr-roadmap](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L19-L42). + +**What it solves** + +- Removes FastVideo-internal dependency for reusable backend pieces. +- Keeps product frontend cadence independent. +- Gives non-Dreamverse users a streaming backend and typed API without + carrying the Dreamverse app. +- Gives Dynamo a stable library API while leaving Dynamo package code in + Dynamo: + [Dynamo contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L188-L210). + +**Costs** + +- Requires careful boundary discipline: generic streaming/server code in + FastVideo; product routes/prompts/presets in Dreamverse. +- Requires contract tests to prevent drift. +- Some Dreamverse compatibility routes may become public and need support. + +**Best fit** + +- Best long-term target if the team wants FastVideo to own serving/runtime + infrastructure while keeping Dreamverse as a separately evolving product. + +### Comparison matrix + +| Criterion | A. Separate dep | B. Subfolder monorepo | C. Full namespace merge | D. Hybrid backend merge | +|---|---|---|---|---| +| Alignment with stated goal | High: Dreamverse depends on public package | Medium: no external dep, but product becomes repo-local | Medium: dependency disappears by absorption | High: reusable backend in public, product separate | +| Time to remove `FastVideo-internal` | Fastest | Medium | Slowest | Medium-fast | +| Build complexity | Low | High: Python + Next.js in one repo | High: Python package plus static/frontend extras | Medium: Python backend only in FastVideo | +| Release cadence | Independent | Coupled clone; releases can still be separate but more friction | Fully coupled | Backend coupled to FastVideo, frontend independent | +| Security surface in FastVideo | Low | Medium/High | Highest | Medium | +| Contributor friction | Low for both repos | Higher for library contributors | Highest; product assumptions in library | Medium; clear backend boundary needed | +| Dependency management | Normal package pin | Workspace/monorepo tooling needed | FastVideo extras/static asset decisions needed | FastVideo extras for backend; FE out-of-tree | +| CI cost | Low/medium | High | High | Medium | +| Contract-test value | High; cross-repo contract tests are essential | Medium; same repo but still useful | Medium; less boundary pressure | High; generic backend vs product boundary | +| Precedent strength | Strong: library/server plus external UI patterns exist | Mixed | Weak for Python ML library + Next.js inside namespace | Strongest match: in-tree server/backend, external UI | +| Packaging risk | Low | Medium/high | High | Medium | +| Future Dynamo fit | Strong | Strong if API remains clean | Risky if product API bleeds in | Strong | +| Frontend iteration speed | Highest | Lower | Lowest | Highest | +| Risk of product-specific API leakage | Low | Medium | High | Medium; controllable with naming discipline | +| Reversibility | High | Medium | Low | Medium/high | + +### OSS precedents (with citations) + +| Pattern | Project | What it supports | Citation | +|---|---|---|---| +| Library plus in-tree server | vLLM | A Python ML library can ship an in-tree OpenAI-compatible server while clients remain external. | https://github.com/vllm-project/vllm/blob/bcf5cac9fb956788f649d1f5297b74c886a9d6d3/README.md#L64-L74 | +| Service packaging | BentoML | Packaging model + service + dependencies is supported, but CWD packaging creates discipline needs. | https://github.com/bentoml/BentoML/blob/32230a5276a8da8b23c4a06a9ec6272c1993451a/docs/source/build-with-bentoml/asgi.rst#L5-L18 | +| YAML-driven production serving | Ray Serve | Production updates should avoid in-place mutation; use new deployment/traffic switch. | https://docs.ray.io/en/latest/serve/advanced-guides/inplace-updates.html | +| Library/server plus external UI | TGI + ChatUI | Server can live with backend project while UI is separate. | https://github.com/huggingface/text-generation-inference/blob/b4adbf2f6e2e721280bd0ea5f91d70f7d033f5ed/docs/source/basic_tutorials/consuming_tgi.md#L182-L186 | +| Lean library plus examples elsewhere | Transformers.js | Library stays lean; demos/examples can live outside core. | https://github.com/huggingface/transformers.js/blob/f7487c737aa8cafbc106c9adf69dc9578c8f3fe0/README.md#L26-L34 | +| Product monorepo that later split frontend | ComfyUI | Product UI/server monorepo can hit release-cadence mismatch and split FE later. | https://github.com/comfyanonymous/ComfyUI/blob/fed8d5efa6b70d5b24c4c33cb643bfccc39d45b5/README.md#L131-L149 and https://github.com/Comfy-Org/ComfyUI_frontend/blob/60f789d58070a9d1d789b260f83c36d7293a39f0/README.md#L31-L60 | +| Tightly coupled UI/server product | AUTOMATIC1111 SD WebUI | Product repos can couple UI/server tightly, but security surface becomes product-sized. | https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/82a973c04367123ae98bd9abdf80d9eda9b910e2/webui.py#L48-L104 | + +#### Precedent synthesis + +- Strong precedents exist for a Python ML library shipping a server entrypoint. +- Strong precedents exist for keeping frontend/product UI out of the backend + library repo. +- The cited set does not contain a clean precedent for merging a Next.js + product into a Python ML library namespace. +- The most applicable pattern is **backend/server in the ML project, + product UI outside**. + +### Recommendation + +#### Recommend Option D, constrained: backend merges as generic FastVideo streaming; frontend stays separate + +Recommendation: follow **Option D** as the long-term architecture, but keep +the backend merge generic. In practice, this means continuing the current +public FastVideo path: + +- `fastvideo.entrypoints.streaming.*` owns reusable streaming runtime. +- `fastvideo.entrypoints.streaming.gpu_pool` owns generic GPU worker pools. +- `fastvideo.entrypoints.streaming.prompt.*` owns provider-agnostic prompt + operations. +- `fastvideo.entrypoints.streaming.router.*` owns FastVideo-aware routing. +- `fastvideo.api` owns typed construction, requests, results, events, and + continuation state. +- Dreamverse keeps product-only FE, curated presets, prompt UX, product + routes, and launch scripts. + +This is effectively the path already underway in PRs #1257, #1258, #1284, +#1286, and #1288: +[PR roadmap](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L21-L42). + +#### Why not Option A as the final answer? + +Option A is the fastest near-term release posture and should be used as the +immediate migration posture. However, plain status quo is not enough for +the ultimate goal because reusable backend pieces still need to live in +public FastVideo so Dreamverse can stop reaching into internal code. That +work is already partly complete: + +- GPU pool: [D-12](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L47-L116). +- Prompt enhancer: [PR roadmap 7.7](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L32-L35). +- Streaming auxiliaries: [PR roadmap 7.8](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/pr-roadmap.md#L35-L36). +- Router: [D-15](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L118-L155). +- `generate_async`: [streaming-server unlock PR](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L314-L345). + +So the practical answer is: + +- **Near term:** Option A operationally, after docs/scripts are fixed. +- **Architecture target:** Option D, with generic backend ownership in + FastVideo and product ownership in Dreamverse. + +#### Why not Option B? + +Option B makes cross-repo coordination easier but imports frontend build, +package, and CI complexity into FastVideo. That is unnecessary while a +normal package dependency plus contract tests can guard the integration. +FastVideo's current repo structure is a Python package with examples and +docs, not a product monorepo: +[codebase map](file:///home/william5lin/FastVideo/.agents/memory/codebase-map/README.md#L5-L75). + +#### Why not Option C? + +Option C makes the product backend a public FastVideo namespace. That is +only appropriate if the team wants to support Dreamverse as a first-class +FastVideo product surface. Today the known public obligations are generic: +typed requests, streaming server, GPU pool, prompt provider protocol, +router, NVFP4, and Dynamo event APIs. Product-only Dreamverse behavior does +not need to become framework API. + +#### Conditions that would change the recommendation + +Move from constrained D toward **C** only if all of these become true: + +1. Dreamverse is declared the canonical FastVideo serving product. +2. Product routes such as curated presets and prompt-system config are + accepted as public FastVideo API. +3. FastVideo maintainers accept the security and support surface. +4. Release cadence for product UX and FastVideo core is intentionally + coupled. +5. Frontend packaging/static asset strategy is explicitly owned by + FastVideo. + +Move from constrained D back toward **A** if any of these become true: + +1. Prompt enhancement, router, or GPU pool turn out to be Dreamverse-only. +2. No second user appears for the streaming backend outside Dreamverse. +3. FastVideo maintainers want to minimize serving surface and publish only + Python library APIs. +4. Dreamverse needs product changes faster than FastVideo can release. +5. Security review rejects in-tree serving/router responsibilities. + +### Migration sketch for the recommended path + +#### Phase 0 — Land the public backend stack + +- **Effort:** Large, already in flight. +- **Owner:** FastVideo public. +- **Files:** #1288 scope, especially `fastvideo/api/`, + `fastvideo/entrypoints/video_generator.py`, + `fastvideo/entrypoints/streaming/`, LTX-2 pipeline stages, NVFP4 files, + and contract tests. +- **Exit criteria:** #1288 merges; public `fastvideo.api.VideoEvent` and + `VideoGenerator.generate_async` are available: + [results.py](file:///home/william5lin/FastVideo/fastvideo/api/results.py#L109-L164), + [video_generator.py](file:///home/william5lin/FastVideo/fastvideo/entrypoints/video_generator.py#L264-L332). + +#### Phase 1 — Fix Dreamverse dependency docs and bootstrap + +- **Effort:** Small. +- **Owner:** Dreamverse. +- **Files:** + - [README.md](file:///home/william5lin/Dreamverse/README.md#L76-L109) + - [bootstrap script](file:///home/william5lin/Dreamverse/.agents/skills/bootstrap-fastvideo-private-fork/scripts/bootstrap_fastvideo_private.sh#L7-L11) + - [pyproject.toml](file:///home/william5lin/Dreamverse/pyproject.toml#L17-L22) + - [uv.lock](file:///home/william5lin/Dreamverse/uv.lock#L716-L722) +- **Exit criteria:** no user-facing docs or scripts mention + `FastVideo-internal` as the expected dependency path. + +#### Phase 2 — Add public health/readiness/status route compatibility + +- **Effort:** Medium/Large. +- **Owner:** FastVideo public. +- **Files likely to touch:** + - `fastvideo/entrypoints/streaming/server.py::build_app` + - new `fastvideo/entrypoints/streaming/health.py` + - tests under `fastvideo/tests/entrypoints/streaming/` +- **Source route shapes:** + [Dreamverse health routes](file:///home/william5lin/Dreamverse/server/routes/health.py#L34-L79). +- **Tracking:** [open item #1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99). +- **Exit criteria:** Dreamverse FE can target public `build_app` for + `/healthz`, `/readyz`, `/status`, and `/v1/stream`; product-only routes + remain feature-detected. + +#### Phase 3 — Replace Dreamverse prompt enhancer fork + +- **Effort:** Medium. +- **Owner:** Dreamverse. +- **Files likely to touch:** + - new `Dreamverse/server/prompting/_internal_compat.py` + - `Dreamverse/server/runtime.py` + - `Dreamverse/server/main.py` + - `Dreamverse/server/prompt_enhancer.py` +- **Public API:** + [PromptEnhancer](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/prompt/enhancer.py#L66-L142), + [LLMProvider](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/prompt/providers/base.py#L63-L75). +- **Tracking:** [DR-1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L174-L206). +- **Exit criteria:** Dreamverse no longer carries a full local fork for the + generic prompt operations public FastVideo already owns. + +#### Phase 4 — Decide and implement `cerebras_ifm` + +- **Effort:** Small decision plus small/medium implementation. +- **Owner:** Team decision, then Dreamverse or FastVideo. +- **Default recommendation:** Dreamverse-side custom provider. +- **Public schema source:** + [PromptEnhancerConfig](file:///home/william5lin/FastVideo/fastvideo/api/schema.py#L229-L235). +- **Tracking:** [DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229). +- **Exit criteria:** Dreamverse IFM provider works after prompt fork removal. + +#### Phase 5 — Move streaming demo config into FastVideo examples + +- **Effort:** Small. +- **Owner:** FastVideo public. +- **Source:** + [Dreamverse streaming_demo.yaml](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L1-L149). +- **Target:** `examples/serving/streaming_demo.yaml`. +- **Exit criteria:** users can reproduce the typed streaming path from the + FastVideo repo without checking out Dreamverse. + +#### Phase 6 — Remove `experimental["pipeline_config"]` where practical + +- **Effort:** Medium for `layer_profile`; Large for a full typed + `dit_config.quant_config` carrier. +- **Owner:** FastVideo public, then Dreamverse cleanup. +- **Tracking:** + [open item #4](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L244-L260), + [quantization follow-up](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/quantization.md#L175-L200). +- **Exit criteria:** Dreamverse can express its quant layer profile through + typed config instead of in-memory mutation. + +#### Phase 7 — Decide standalone upsampler CLI + +- **Effort:** Small/Medium. +- **Owner:** FastVideo public. +- **Input:** internal standalone utility + [upsample.py](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/upsample.py#L120-L180) + and internal CLI + [cli/upsample.py](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/cli/upsample.py#L48-L130). +- **Public alternative:** SR refine stage already covers in-pipeline latent + upsampling: + [ltx2_refine.py](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L116-L180). +- **Exit criteria:** explicit decision: port CLI, document refine-stage-only + support, or defer. + +#### Phase 8 — Validate LTX-2 stage parity and offload residuals + +- **Effort:** Small/Medium for stage parity; Medium for layerwise offload. +- **Owner:** FastVideo public. +- **Stage source:** + [public LTX-2 stages](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/). +- **Offload source:** + [internal layerwise offload](file:///home/william5lin/FastVideo-internal/fastvideo/models/layerwise_offload.py#L15-L20). +- **Exit criteria:** no known behavior gap between internal and public LTX-2 + stages; offload is either deliberately deferred or ported with tests. + +### Open questions + +1. **Which provider path for `cerebras_ifm`?** + - Public provider or Dreamverse-side custom provider? + - Default recommendation: Dreamverse-side unless there is another user. + - Source: [DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229). + +2. **Should public FastVideo support standalone LTX-2 file upsampling?** + - If yes, port internal CLI. + - If no, document that SR support is pipeline-refine only. + - Sources: [internal CLI](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/cli/upsample.py#L15-L35), + [public refine stage](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L1-L22). + +3. **Does Dreamverse need layerwise CPU offload?** + - If memory-tight deployments require it, port as generic FastVideo. + - Otherwise defer. + - Source: [internal offload manager](file:///home/william5lin/FastVideo-internal/fastvideo/models/layerwise_offload.py#L15-L20). + +4. **How much Dreamverse route surface should FastVideo own?** + - Health/readiness/status should migrate because they are part of + streaming-server compatibility. + - Curated presets and prompt-system config should stay Dreamverse-side + unless product policy changes. + - Source: [route contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/streaming-server.md#L232-L259). + +5. **Should `layer_profile` be the only near-term quant typed addition?** + - Adding `layer_profile` is bounded. + - A typed carrier for arbitrary mutated `PipelineConfig` is larger design + work. + - Source: [quantization follow-ups](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/quantization.md#L175-L200). + +6. **When does Option D become Option C?** + - Only if Dreamverse backend routes become public FastVideo product API. + - Until then, keep generic streaming code in FastVideo and product code in + Dreamverse. + +--- + +## Part 3 — Action items + +1. **P0 / S — Update Dreamverse README dependency notes.** + - Replace `../FastVideo-internal` with public FastVideo instructions. + - Preserve local editable `../FastVideo` dev flow where useful. + - Source: [README stale lines](file:///home/william5lin/Dreamverse/README.md#L76-L109). + +2. **P0 / S — Replace or remove private FastVideo bootstrap script.** + - Current script clones `FastVideo-internal` and verifies imports from it. + - Source: [script](file:///home/william5lin/Dreamverse/.agents/skills/bootstrap-fastvideo-private-fork/scripts/bootstrap_fastvideo_private.sh#L7-L11). + +3. **P1 / M-L — Add `/healthz`, `/readyz`, and `/status` to public `build_app`.** + - Keep `/curated-presets` and prompt-system config in Dreamverse. + - Source: [open item #1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L69-L99). + +4. **P1 / M — Replace Dreamverse prompt-enhancer fork with compat shim.** + - Wrap public `PromptEnhancer`. + - Keep only Dreamverse-specific metadata and product fallback behavior. + - Source: [DR-1](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L174-L206). + +5. **P1 / S-M — Decide `cerebras_ifm` provider path.** + - Default: Dreamverse custom provider via `register_provider`. + - Source: [DR-2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L211-L229). + +6. **P2 / S — Add `examples/serving/streaming_demo.yaml` to FastVideo.** + - Start from Dreamverse config and remove Dreamverse-private comments. + - Source: [streaming_demo.yaml](file:///home/william5lin/Dreamverse/serve_configs/streaming_demo.yaml#L1-L149). + +7. **P2 / M — Verify each public LTX-2 colocated stage against internal behavior.** + - Start with refine, denoising, latent prep, image conditioning, text + encoding, and audio decoding. + - Source: [public refine stage](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/stages/ltx2_refine.py#L1-L22). + +8. **P2 / M — Add typed `transformer_quant_layer_profile` if Dreamverse needs it.** + - Thread schema → compat → `FastVideoArgs._apply_transformer_quant`. + - Source: [open item #4](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L244-L260). + +9. **P2 / S-M — Decide standalone LTX-2 upsampler CLI support.** + - Port internal CLI only if file-to-file upsampling is a public workflow. + - Source: [internal upsample CLI](file:///home/william5lin/FastVideo-internal/fastvideo/entrypoints/cli/upsample.py#L15-L35). + +10. **P2 / S — Fix pre-existing AbsMaxFP8 test failure separately.** + - Do not block Dreamverse migration on it. + - Source: [open item #2](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/open-threads.md#L100-L117). + +11. **P2 / S-M — Document public streaming install extras and dependencies.** + - Include router `websockets`, prompt enhancer provider SDKs, and optional + safety classifier extras. + - Source: [D-16 dependency note](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L224-L242). + +12. **P2 / S — Keep contract tests in the FastVideo CI path.** + - Guard Dreamverse shape, Dynamo shape, and async events. + - Sources: [Dreamverse test](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dreamverse_shape.py#L1-L26), + [Dynamo test](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_dynamo_shape.py#L1-L19), + [async test](file:///home/william5lin/FastVideo/fastvideo/tests/contract/test_generate_async.py#L1-L7). + +13. **P3 / M — Defer layerwise offload until a deployment needs it.** + - Port only as generic FastVideo utility with tests. + - Source: [internal offload](file:///home/william5lin/FastVideo-internal/fastvideo/models/layerwise_offload.py#L15-L20). + +14. **P3 / M — Defer StepVideo public parity for this integration.** + - Dreamverse model registry is LTX-2/LTX-2.3 only. + - Source: [Dreamverse config](file:///home/william5lin/Dreamverse/server/config.py#L28-L45). + +15. **P3 / S — Do not add debug-only fields to public schema by default.** + - Keep them private unless there is a user-facing debugging workflow. + - Source: [internal debug args](file:///home/william5lin/FastVideo-internal/fastvideo/fastvideo_args.py#L200-L203). + +16. **P3 / M — Keep router active-active and sticky routing deferred.** + - Add only when session-routing evidence justifies it. + - Source: [D-15 action items](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L175-L201). + +17. **P3 / S — Preserve Dynamo as an external backend package.** + - FastVideo should expose typed API; Dynamo code lives in Dynamo. + - Source: [Dynamo contract](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/cross-repo-surfaces.md#L188-L210). + +18. **P3 / S — After #1288 merges, update memory-dir state.** + - Mark item D resolved and update branch tips. + - Source: [runbook post-merge steps](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/runbook.md#L51-L70). + +19. **P3 / S — Remove stale split-PR mental model from follow-up docs.** + - #1288 is the current vehicle; split bookmarks are historical. + - Source: [D-17 implications](file:///home/william5lin/FastVideo/.agents/memory/dreamverse-integration/decisions-log.md#L34-L45). + +20. **P3 / S — Keep product-only Dreamverse frontend out of FastVideo unless explicitly re-scoped.** + - This preserves release cadence and avoids packaging bloat. + - Source: [Dreamverse baseline](file:///home/william5lin/Dreamverse/README.md#L5-L16). diff --git a/.agents/memory/dreamverse-integration/open-threads.md b/.agents/memory/dreamverse-integration/open-threads.md new file mode 100644 index 0000000000..96e4fa1c92 --- /dev/null +++ b/.agents/memory/dreamverse-integration/open-threads.md @@ -0,0 +1,531 @@ +# Open Threads — Active Follow-Ups + +Live work items with priority, effort estimate, dependencies, and +recommended next action. + +For why each item is open see [decisions-log.md](decisions-log.md). For +PR-level context see [pr-roadmap.md](pr-roadmap.md). + +**Last updated:** 2026-05-05 (D-20 broken-pipe root cause + fix landed on +`will/dreamverse-monorepo` @ `5eaf0a13`; added new thread D-20-CP for +cherry-picking the public-API audio routing fix to `will/ltx2_sr_port` +so it lands in PR #1288. Earlier: strategy reversal — PR #1287 CLOSED, +replaced by mega-PR #1288 on `will/ltx2_sr_port` @ `b36bdbc9` covering +the full 6-layer stack at once. See [decisions-log.md D-17](decisions-log.md#d-17). +Item D resolution gate is now #1288 merge instead of #1287; same content, +different vehicle.). + +## Priority overview + +| # | Pri | Item | Effort | Unblocks | +|---|---|---|---|---| +| **D-20-CP** | High | Cherry-pick `[fix] api: route LTX-2 audio kwargs through batch.extra; strict update` (`265ce1a6`) onto `will/ltx2_sr_port` so it lands in PR #1288 | 15 min | Surfaces the public-API audio-conditioning fix in the mega-PR rather than waiting for `will/dreamverse-monorepo` to fold in. Tests already pass (185/185 api). | +| **~~D-22~~** | ~~Med~~ | ~~Per-chunk timing instrumentation in `stream_fmp4` and the controller's AV relay loop~~ | ~~M~~ | ✅ **Resolved 2026-05-06** in `bade2c0a`. `av_streaming.stream_fmp4` now records `av_wav_write_ms`, `av_ffmpeg_spawn_ms`, `av_first_chunk_ms`, `av_chunk_interval_ms_{min,median,p95,max}`, `av_chunk_publish_ms_{median,p95}`, `av_chunk_read_ms_{median,p95}` into the timings dict; `gpu_pool.handle_command` prints a one-line summary per segment so they show up directly in the deploy log. **Controller-side WS-send instrumentation is the remaining unaddressed slice** — see new D-22-CTL. | +| **D-22-CTL** | Low | Controller-side AV relay timing in `apps/dreamverse/server/session/controller.py` (between media event arrival and ws_send_bytes). | S | The worker side is now fully attributed by D-22. The controller-side per-chunk WS-send overhead is the still-unmeasured slice of the 700ms gap between `worker_e2e` and `main_user_step`. Add `t_ws_send_ms` per chunk in the AV relay loop, surface as `controller_ws_send_ms_{median,p95}` in the segment summary log line. | +| **D-23** | Med | On NVENC-capable hosts (RTX 50-series / T4 / A10 / H100 PCIe), benchmark `h264_nvenc` vs `libx264` and decide whether to default `--nvenc=true` for that SKU | S benchmark + S decision | Stutter mitigation depends on hardware; B200 needs different fix path (D-24). | +| **D-24** | Low | For B200-class deploys without NVENC, prototype gen-N+1 // encode-N pipelining or FE buffer pre-fill | L | Architectural change required; benchmark suggests ~700ms can be hidden if we pipeline. Ship-blocker only when realtime stutter becomes user-visible on B200 deploys. | +| **D-8** | High | Verify `ltx2_image_crf` post-`d80c2a8` | 10 min | Confirms typed stage-override path actually flows; closes a latent silent-drop bug | +| **1** | High | Migrate `/healthz`+`/readyz`+`/status` into FastVideo `build_app` | M-L | Closes BE_FLAVOR=fastvideo FE-compatibility; closes streaming-upstream contract debt | +| **2** | High | Fix pre-existing AbsMaxFP8 test failure | S | Self-contained quantization tech debt | +| **VPO** | High | Decide `video_position_offset_sec` semantics (a vs b) | 30 min | Unblocks PR 7.6 state emission | +| **D** | 🟢 in flight | Implement `generate_async` — content shipped in mega-PR **#1288** on `will/ltx2_sr_port` @ `b36bdbc9` (was #1287, CLOSED + re-routed per [D-17](decisions-log.md#d-17)) | L | Closes Q-5/Q-9/PR-7.5 TODOs simultaneously; enables Dynamo backend; unblocks audio re-encode; enables `GpuPool.run_async()` migration (D-12-B). Resolution gate: #1288 merge. | +| **DR-1** | High | Dreamverse: create `prompting/_internal_compat.py` shim + replace local `prompt_enhancer.py` (1933 LOC) — **PR #1258 has merged (`f673423b`); now actionable** | M (~150-200 LOC shim, replace upstream wiring) | Lets Dreamverse stop carrying a 1933-LOC fork | +| **DR-2** | Med | Decide `cerebras_ifm` provider path: (a) public Literal + `CerebrasIFMProvider` shipped, OR (b) Dreamverse-side custom provider via `enhancer.register_provider(...)` | S (decision) + S-M (impl) | Resolves the cerebras_ifm gap left by PR #1258. Same item as legacy #3 below; DR-2 is the Dreamverse-side framing. | +| **3** | Med | Add `cerebras_ifm` to `PromptEnhancerConfig.provider` Literal + provider | S-M | Public-side resolution if DR-2 picks (a) | +| **4** | Med | Expose `layer_profile` on typed `engine.quantization` | M | Removes Dreamverse's `experimental["pipeline_config"]` dodge for stage profiles | +| **5** | Med | Design typed `dit_config.quant_config` carrier | L design + L impl | Removes broader `experimental["pipeline_config"]` escape hatch | +| **SBS** | Med | `SessionStore` / `BlobStore` lifecycle policy | M design | Needed in PR 7.5 design pass | +| **D-12-A** | Med | Update `GpuPool` ABC docstring: mark "API may change post-PR-7.10; experimental / server-internal" | trivial | Prevents accidental promotion of streaming-internal API to framework-level | +| **D-12-B** | Med | Replace `GpuPool.run() -> Any` with `run_async() -> AsyncIterator[VideoEvent]` in PR 7.10 cycle | M | Closes the streaming-server cancellation TODO; converges with `generate_async` | +| **D-13-A** | Med | Document `fastvideo.entrypoints.streaming.prompt.*` in user-facing docs as "streaming-server scoped"; avoid framework-level framing | trivial (docs only) | Keeps future move to `fastvideo.prompt.*` cheap | +| **D-13-B** | Low | Add optional `client_factory` parameter to `LLMProvider` for `httpx.AsyncClient` pooling | S | Only if metrics show connect/TLS overhead is meaningful | +| **D-12-C** | Low | Avoid locking `PoolAssignment.gpu_id: int` as public; rename to `worker_id` (already exists) or add `device_ids: list[int]` for topology-aware pooling | S | Future multi-GPU-per-worker refactor stays cheap | +| **6** | Low | Audio attention quantization profile + test update | S | Future audio quant exploration | +| **7** | Low | Schema parity inventory cleanup (env-driven prompt fields) | S-M | Long-term consistency | +| **8** | Low | Stale `apps/web/test-results/` dir cleanup | trivial | Cosmetic | +| **11** | Low | Promote LTX-2 prompt orchestration (locked segments, segment_prompts JSON shape, rollout id/label) to `fastvideo.entrypoints.streaming.prompt.ltx2_orchestration` | M | Resolves Q-2 from decisions-log when a second LTX-2-style consumer appears | +| **12** | Low | When streaming server starts using `PromptSafetyFilter`, ensure operator-visible logging on `SafetyDecision.UNAVAILABLE` results | trivial | Surfaces degraded-safety state to operators (per D-14 Watch-Out item) | +| **13** | Low | When sticky session routing is needed, add `ReplicaRegistry.select(routing_key: str | None = None)` and document where `session_id` lives (WS URL/header preferred over first JSON frame) | M | Forward-compat from D-15 — keeps the door open without buffering/peeking | +| **14** | Low | At higher load, add `_bridge_session()` max-size + timeout limits OR recommend Envoy/HAProxy in front | S-M | The libraries' basic backpressure suffices for MVP; document the limit per D-15 | +| **15** | Low | If active-active multi-primary becomes a requirement, define behavior (round-robin within healthy primaries, weighted, sticky-by-key) | M | Currently `RouterConfig.__post_init__` rejects multi-primary; D-15 deferred until evidence | +| **~~Source-doc disposition~~** | ~~Med~~ | ~~Disposition of 7 untracked source docs~~ | ~~trivial~~ | ✅ **Resolved 2026-05-03** — moved into [source-archive/](source-archive/) | +| **~~9~~** | ~~Low~~ | ~~Commit-message cleanup: PR 8's 3 commits still have `[8/n] Improve API:` prefix~~ | ~~S~~ | ✅ **Resolved 2026-05-04** — bundled into the will/api_7.8 prep rebase. PR 8's 3 commits now read `[type] streaming: ...` | +| **~~10~~** | ~~Low~~ | ~~Commit-message cleanup: PR 7.8/7.9 commits have `streaming: streaming X` duplication~~ | ~~S~~ | ✅ **Resolved 2026-05-04** — bundled into the will/api_7.8 prep rebase. 3 commits dedup'd. | + +--- + +## High priority + +### D-20-CP: Cherry-pick public-API audio routing fix to `will/ltx2_sr_port` + +**Why:** Commit `265ce1a6` (`[fix] api: route LTX-2 audio kwargs through batch.extra; strict update`) lives only on `will/dreamverse-monorepo` today. It touches public FastVideo surface (`fastvideo/entrypoints/video_generator.py`, `fastvideo/api/sampling_param.py`, plus a new regression test). For PR #1288 to ship a coherent public API — including the strict `SamplingParam.update()` — this commit needs to also land on `will/ltx2_sr_port`. + +**Action:** + +1. `git checkout will/ltx2_sr_port` +2. `git cherry-pick 265ce1a6` (clean; only touches `fastvideo/` paths that exist on both branches) +3. `pre-commit run --files fastvideo/entrypoints/video_generator.py fastvideo/api/sampling_param.py fastvideo/tests/api/test_extra_overrides_routing.py` +4. `pytest fastvideo/tests/api/ -q` (expect 185 passed) +5. `git push origin will/ltx2_sr_port` (fast-forward, no force) +6. `git checkout will/dreamverse-monorepo` (return to default working branch per runbook) + +**Outcome:** PR #1288 picks up the fix automatically (its head IS `will/ltx2_sr_port`). The cherry-pick lives on both branches as separate SHAs; they'll dedupe naturally on any future rebase. + +**Effort:** 15 minutes (cherry-pick + lint + test + push). + +**Dependencies:** None. Tests already pass; no rebase conflicts expected. + +**Files touched (same on both branches):** + +- `fastvideo/entrypoints/video_generator.py` +- `fastvideo/api/sampling_param.py` +- `fastvideo/tests/api/test_extra_overrides_routing.py` (new file) + +### D-8: Verify `ltx2_image_crf` typed flow post-`d80c2a8` + +**Why:** Apr 26 dreamverse_review documented this field getting silently +dropped by the public `SamplingParam`. May 2 `d80c2a8` (Dreamverse) +refactored to typed `GeneratorConfig` + `preset_overrides`. Whether +`image_crf` now flows through `request.stage_overrides.refine.image_crf` +(per [design.md](design.md) mapping) or is still dropped is unverified. + +**Action:** +1. Read [`Dreamverse/server/video_generation.py`](file:///home/william5lin/Dreamverse/server/video_generation.py) + post-`d80c2a8` for `image_crf` handling +2. Trace through to FastVideo's `request.stage_overrides.refine.image_crf` +3. Confirm runtime consumption in [`fastvideo/pipelines/basic/ltx2/`](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/) + +**Effort:** 10 min, no code changes. + +**Outcome:** Either confirms working OR identifies bug → opens fix item. + +### Item #1: Migrate `/healthz`+`/readyz`+`/status` into `build_app` + +**Why:** Today +[`fastvideo.entrypoints.streaming.server.build_app`](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/server.py) +exposes only `/health` + `/v1/stream`. Dreamverse FE expects all of +`/healthz`, `/readyz`, `/status`, `/curated-presets`, +`/prompt-system-config`, devtools. + +The streaming-server-upstream plan (line 84) explicitly lists +`/healthz`+`/readyz`+`/status` as part of the contract that the upstream +of `realtime/` → `streaming/` must preserve. They were deferred from +PR 7.5's MVP. `/curated-presets` and `/prompt-system-config` are +operator-side and stay in Dreamverse (FE feature-detects). + +**Action:** +1. Read PR 7.5 (#1251) `build_app` to scope what's there +2. Read [`Dreamverse/server/routes/health.py`](file:///home/william5lin/Dreamverse/server/routes/health.py) + for the route shapes Dreamverse already consumes +3. Propose route migration as commit on top of `will/api_7.5` or as + part of PR 7.10 cycle +4. Land + +**Effort:** Medium-Large (route shapes need preservation; tests). + +**Dependencies:** None blocking; can land anytime. + +**Files likely to touch:** +- `fastvideo/entrypoints/streaming/server.py::build_app` +- New `fastvideo/entrypoints/streaming/health.py` +- Tests in `fastvideo/tests/entrypoints/streaming/` + +### Item #2: AbsMaxFP8 pre-existing test failure + +**Why:** [`fastvideo/tests/ops/quantization/test_absmax_fp8.py::test_create_weights_rejects_invalid_dtype`](file:///home/william5lin/FastVideo/fastvideo/tests/ops/quantization/test_absmax_fp8.py) +fails with `AssertionError not raised`. Pre-existing on `main`; verified +NOT introduced by NVFP4 work via `git stash`. + +**Action:** +1. `git log --oneline fastvideo/tests/ops/quantization/test_absmax_fp8.py` + to find when it last passed +2. Either: + - Restore the assert in `AbsMaxFP8LinearMethod.create_weights` if + intentional behavior was lost + - Drop the test if assert is no longer correct +3. Verify + +**Effort:** Small. + +**Dependencies:** None. + +### Item VPO: `video_position_offset_sec` semantics + +**Why:** Per +[`fastvideo/pipelines/basic/ltx2/continuation.py`](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/continuation.py), +`LTX2ContinuationState.video_position_offset_sec` exists as a state +field. Two valid interpretations: + +- **(a) Persistent across segments** — accumulating time offset for long + sessions; useful for time-coherent audio chaining. +- **(b) Per-segment hint that rides on the carrier** — runtime + overwrites every time; field is harmless redundancy. + +Dreamverse computes `prefix_sec = float(audio_extra) / 24.0` per segment +in `apply_audio` and currently does NOT persist it on +`ContinuationState`. Field's docstring leans toward (b). + +**Decision deadline:** before PR 7.6 starts emitting/consuming the +field (PR 7.6 branch is ready, not yet PR'd). + +**Action:** +1. Confirm field's intended semantics with audio team +2. If (a): document the accumulation rule explicitly + add tests +3. If (b): leave docstring as-is + add test confirming overwrite + +**Effort:** 30 min discussion + small implementation. + +### Item D: Implement `generate_async` (PR 7.10) + +**Why:** Highest leverage. Closes: + +- D-5 / Q-5: audio re-encode for cross-segment continuity +- Q-9: Dynamo progress passthrough (deferred) +- PR 7.5's mid-segment cancellation TODO +- Unblocks Dynamo native backend integration +- **D-12-B**: enables `GpuPool.run() -> run_async() -> AsyncIterator[VideoEvent]` migration + +**Action:** See [streaming-server.md](streaming-server.md) "PR 7.10 — the +unlock PR" section for scoping. + +**Effort:** Large. + +**Dependencies:** Best after PR 7.6 lands (gpu_pool upstream). + +**Files:** +- `fastvideo/entrypoints/video_generator.py` — add `generate_async`, + refactor `generate_video` as wrapper +- `fastvideo/api/results.py` — add `VideoEvent`/`VideoProgressEvent`/ + `VideoPartialEvent`/`VideoFinalEvent` +- `fastvideo/entrypoints/streaming/server.py` — consume `generate_async`, + remove TODO markers +- `fastvideo/entrypoints/streaming/gpu_pool.py` — add `run_async()` + forwarding events from worker to caller +- New `fastvideo/tests/entrypoints/test_generate_async.py` +- New `fastvideo/tests/contract/test_dynamo_shape.py` (already in PR 8) + +### Item DR-1: Dreamverse — replace local `prompt_enhancer.py` with public + compat shim + +**Why:** Today Dreamverse carries `Dreamverse/server/prompt_enhancer.py` +(1933 LOC) — a local copy/derivative of the FastVideo-internal version. +After PR #1258 merges, Dreamverse should switch to the public +`fastvideo.entrypoints.streaming.prompt.PromptEnhancer` and delete most +of the local module. + +**Migration shape:** + +1. **Create** `Dreamverse/server/prompting/_internal_compat.py` (~150-200 LOC): + - Wraps public `PromptEnhancer.enhance()` → returns `EnhanceResult` shape Dreamverse expects + - Wraps public `PromptEnhancer.auto_extend()` — JSON-parses `LLMResponse.content` into `{"next_prompt": "..."}` + - Wraps public `PromptEnhancer.rewrite()` — JSON-parses into `{"segment_prompts": [...]}` with lenient fallback for malformed JSON + - Layers locked-segment + rollout_id + rollout_label metadata back on top +2. **Update** `Dreamverse/server/runtime.py + main.py` — replace `from prompt_enhancer import PromptEnhancer` with `from prompting._internal_compat import PromptEnhancer` +3. **Delete most of** `Dreamverse/server/prompt_enhancer.py` (1933 LOC). Keep only the bits that don't have a public equivalent: + - Race-based parallel fallback (`_run_provider_race`) — Dreamverse-specific tail-latency optimization + - `cerebras_ifm` provider — pending DR-2 decision + - Multi-classifier prompt safety (NSFW + hate-speech chained) — public ships single classifier +4. **Tests** — verify Dreamverse session controllers still see the expected response shapes through the shim + +**Effort:** Medium (~150-200 LOC shim + replace upstream wiring + delete 1700+ LOC local module + test fixture updates). + +**Dependencies:** +- PR #1258 must merge first (publishes `fastvideo.entrypoints.streaming.prompt.*`) +- DR-2 informs the cerebras_ifm path + +**Files:** +- New: `Dreamverse/server/prompting/_internal_compat.py` +- Modified: `Dreamverse/server/runtime.py`, `Dreamverse/server/main.py` +- Mostly deleted: `Dreamverse/server/prompt_enhancer.py` + +--- + +## Medium priority + +### Item DR-2: Decide `cerebras_ifm` provider path + +**Why:** Public PR #1258's `PromptEnhancerConfig.provider` is +`Literal["cerebras", "groq"]`. Internal supports `"cerebras_ifm"` (the +Cerebras IFM API endpoint with different auth). Dreamverse needs +`cerebras_ifm` working post-migration. + +Two options: + +| Option | Approach | Pros | Cons | +|---|---|---|---| +| **(a) Public** | Add `"cerebras_ifm"` to public Literal + ship `CerebrasIFMProvider` in `fastvideo/entrypoints/streaming/prompt/providers/cerebras_ifm.py` | Discoverable; users with IFM access can use typed config | Adds ~50 LOC + Literal extension to public surface | +| **(b) Dreamverse-side** | Implement `CerebrasIFMProvider` Dreamverse-side as a custom `LLMProvider`, register via `enhancer.register_provider(CerebrasIFMProvider())` | Zero public surface change; private endpoint stays private | Slightly more boilerplate Dreamverse-side; not surfaced to non-Dreamverse users | + +**Recommendation:** Option (b) is more contained. Option (a) is more +discoverable. Default to (b) unless there's a third-party user who needs +IFM access. The Dreamverse-side PR carrying DR-1 is the natural place to +make this decision. + +**Effort:** Small (decision) + Small-Medium (implementation). + +**Dependencies:** DR-1 (compat shim creation). + +### Item #3: `cerebras_ifm` provider in public Literal + +**Why:** Same item as DR-2 from the public-side framing. If DR-2 picks +option (a), this is the implementation. If DR-2 picks option (b), this +item is closed without implementation. + +**Action:** See DR-2. + +**Effort:** S-M. + +### Item #4: Expose `layer_profile` on typed `engine.quantization` + +**Why:** Today `transformer_quant: "NVFP4"` always constructs +`NVFP4Config()` with default `layer_profile="refine"`. Dreamverse +dodges via `experimental["pipeline_config"]`. + +**Action:** +1. Add `transformer_quant_layer_profile: str | None = None` to + `QuantizationConfig` in [`schema.py`](file:///home/william5lin/FastVideo/fastvideo/api/schema.py) +2. Thread through [`compat.py`](file:///home/william5lin/FastVideo/fastvideo/api/compat.py) +3. Update `_apply_transformer_quant` in + [`fastvideo_args.py`](file:///home/william5lin/FastVideo/fastvideo/fastvideo_args.py) + to pass profile +4. Update Dreamverse to drop the `experimental["pipeline_config"]` + dodge in favor of typed knob +5. Tests in [`test_typed_quant_flow.py`](file:///home/william5lin/FastVideo/fastvideo/tests/api/test_typed_quant_flow.py) + +**Effort:** Medium. + +**Files:** schema.py, compat.py, fastvideo_args.py, test_typed_quant_flow.py, ++ Dreamverse/server/video_generation.py. + +### Item #5: Typed `dit_config.quant_config` carrier + +**Why:** The `experimental["pipeline_config"]` escape hatch in +Dreamverse should eventually become a typed field. Design TBD. + +**Action:** Heaviest design work. Should consult Oracle. + +**Effort:** Large design + Large implementation. + +**Dependencies:** #4 should land first; this is the "final form" of #4. + +### Item SBS: `SessionStore` / `BlobStore` lifecycle policy + +**Why:** PR 7's in-memory implementations have no eviction, no TTL, no +automatic blob cleanup on state replacement. Documented as per-deployment +policy decision. + +When PR 7.5/7.6 land the live consumer, who owns: + +- bounded session capacity (LRU? TTL? hard max?) +- blob `drop()` chained when state is replaced +- session expiry on websocket disconnect + +**Recommendation:** streaming server's session manager. Worth stating +explicitly in PR 7.5's design. + +**Effort:** Medium design + small implementation. + +### Item D-12-A: Update `GpuPool` ABC docstring — mark experimental + +**Why:** Per D-12 in [decisions-log.md](decisions-log.md), `GpuPool` +should be documented as "API may change post-PR-7.10; experimental / +server-internal" to prevent accidental promotion of streaming-internal +API to framework-level. PR #1257 merged without this caveat. + +**Action:** Edit +[`fastvideo/entrypoints/streaming/gpu_pool.py`](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/gpu_pool.py) +class docstring on `GpuPool` ABC. Add a note: "API may change post-PR-7.10 +when run_async() lands; treat as server-internal for now." + +**Effort:** Trivial. + +**Dependencies:** None. + +### Item D-12-B: Replace `GpuPool.run() -> Any` with `run_async() -> AsyncIterator[VideoEvent]` + +**Why:** Per D-12, this is the canonical evolution post-PR-7.10. Closes +the streaming server's cancellation TODO and converges the streaming + +OpenAI + Dynamo consumers on a single async API. + +**Action:** As part of PR 7.10 cycle: +1. Add `GpuPool.run_async(session_id, request) -> AsyncIterator[VideoEvent]` +2. Worker forwards events through `result_queue` with type discriminator +3. Streaming server replaces `await pool.run(...)` with `async for event in pool.run_async(...)` +4. Sync `run()` becomes a thin compat wrapper that collects events and returns the final +5. Cancellation propagates: client disconnect → `asyncio.CancelledError` → worker stops mid-step + +**Effort:** Medium. Adds ~50-100 LOC + tests. + +**Dependencies:** Item D (PR 7.10 — `generate_async` on `VideoGenerator`). + +### Item D-13-A: Document `streaming/prompt/*` as streaming-scoped + +**Why:** Per D-13 in [decisions-log.md](decisions-log.md), the prompt +enhancer is currently scoped to streaming-server use even though the +abstraction is general. Phrase user-facing docs as "streaming-server +prompt enhancement" to keep future move to `fastvideo.prompt.*` cheap. + +**Action:** When PR 12 (docs migration) is written, the prompt enhancer +section should: +- Be titled "Streaming Server Prompt Enhancement", not "Prompt API" +- Note the 3 fixed operations (`enhance` / `auto_extend` / `rewrite`) are + shaped by LTX-2 streaming session needs +- Note that consumers wanting custom prompt operations can use + `provider.complete()` directly with their own LLMRequest +- Avoid `from fastvideo import LLMProvider` exports until a second + consumer exists + +**Effort:** Trivial (docs only). + +**Dependencies:** PR 12 (docs migration). + +--- + +## Low priority + +### Item D-13-B: Optional `client_factory` parameter for `httpx.AsyncClient` pooling + +**Why:** Today `_openai_compat.py` instantiates `httpx.AsyncClient` per +call (no connection pooling). Reviewer flagged inefficient. Team chose +simplicity for the expected scale (~6-10 enhancer calls per LTX-2 +session). If real-world metrics show connect/TLS overhead is meaningful, +add an optional `client_factory: Callable[[], httpx.AsyncClient] | None` +parameter to providers so they can share a pool. + +**Action:** Only when metrics justify. Add `client_factory=None` parameter +to `CerebrasProvider` / `GroqProvider` constructors and pass through to +`complete_openai_compatible()`. Default to current per-call behavior. + +**Effort:** Small. + +**Dependencies:** None blocking; only act on real perf data. + +### Item D-12-C: Avoid locking `PoolAssignment.gpu_id: int` as public + +**Why:** Today `PoolAssignment` exposes `gpu_id: int`, assuming +one-GPU-per-worker. Future topology-aware pooling may need +`device_ids: list[int]` (one worker = group of GPUs running internal +`MultiprocExecutor`). Don't freeze the int field as public API. + +**Action:** +- Treat `gpu_id` as a current-impl detail; prefer `worker_id` (already + exists, is stable identifier) +- When a worker actually spans multiple GPUs, add + `PoolAssignment.device_ids: list[int]` and let `gpu_id` be `device_ids[0]` + for backward compat +- Or rename to `gpu_id` → `device_id` with deprecation alias + +**Effort:** Small (1 field rename + alias). + +**Dependencies:** Driven by an actual future "one worker = many GPUs" use case. Don't act preemptively. + +### Item #6: Audio attention quantization profile + +**Why:** Today audio attn and FFN are bf16. If an audio-quant profile +is added to `NVFP4Config.fp4_layers`, update +[`test_basic_av_block_propagates_quant_config_to_all_children`](file:///home/william5lin/FastVideo/fastvideo/tests/ops/quantization/test_nvfp4_ltx2_wiring.py). + +**Effort:** Small (one test + one config field). + +### Item #7: Schema parity inventory cleanup + +**Why:** A few internal-only fields are not exposed publicly: + +- `PROMPT_HTTP_TIMEOUT_MS` +- `PROMPT_INITIAL_STAGE_TIMEOUT_MS` +- `PROMPT_TEMPERATURE` +- `PROMPT_MAX_COMPLETION_TOKENS` +- `PROMPT_AUTO_SLEEP_MS` +- `PROMPT_AUTO_TIMEOUT_MS` +- curated-presets file paths + +These flow via env vars on `dreamverse-server` today. If +`fastvideo serve --config` becomes the canonical entrypoint, they need +typed homes. + +**Effort:** Small-Medium. + +### Item #8: Stale `apps/web/test-results/` directory + +**Why:** Cosmetic. `.gitignore` entry hides it from `git status`, but +the dir has stale `.last-run.json` (45 bytes) from a prior Playwright +run. + +**Action:** `rm -rf apps/web/test-results` whenever convenient. + +**Effort:** Trivial. + +### ~~Item #9~~ + ~~#10~~: Commit-message cleanups — ✅ Resolved 2026-05-04 + +Both items resolved during the `will/api_7.8` prep rebase. A targeted +conditional script (`/tmp/opencode/cleanup_subjects_v2.sh` — only amends +when text actually changes) ran across 33 commits, modified 6: + +- **#9 fix**: extended the regex from `\[\d+\.\d+/n\]` to + `\[\d+(\.\d+)?/n\]` so single-digit prefixes match. PR 8's 3 commits + now read `[type] streaming: ...` instead of `[type] [8/n] Improve API: ...`. +- **#10 fix**: added second substitution `streaming: streaming X` → + `streaming: X`. PR 7.8 / 7.9 commits no longer have the duplication. + +The conditional check skipped pre-commit-hook flakiness on no-op amends +(unlike the earlier first attempt). All affected commits verified clean +post-rebase. + +### Item #11: Promote LTX-2 prompt orchestration to public (when 2nd consumer exists) + +**Why:** Per Q-2 in [decisions-log.md](decisions-log.md) and D-13's +"missing alternative", the LTX-2-specific orchestration (locked +segments, segment_prompts JSON shape, rollout id/label, lenient JSON +parsing) currently stays Dreamverse-side per DR-1. If a second +LTX-2-style consumer appears (e.g. another video model with multi-segment +continuation needing the same prompt orchestration), promote this layer +to `fastvideo.entrypoints.streaming.prompt.ltx2_orchestration`. + +**Action:** Wait for a second consumer to materialize. Until then, the +orchestration stays in Dreamverse's `_internal_compat.py` shim (DR-1). + +**Effort:** Medium when triggered. + +**Dependencies:** A second consumer. + +--- + +## Recommended pull order + +If you have unbounded time and want to maximize forward progress: + +1. **D-8 verify** (10 min) — eliminates uncertainty +2. **D-12-A docstring** (trivial) — caveat the GpuPool API publicly +3. **Item #2 AbsMaxFP8** (S) — clears tech debt +4. **Item VPO video_position_offset_sec** (30 min) — unblocks PR 7.10 (since 7.6 has merged, this is now scoped to whatever consumer first reads the field) +5. **DR-1 + DR-2 Dreamverse migration** (M) — **now unblocked since PR #1258 merged**; replaces 1700+ LOC of local fork +6. **Item #4 layer_profile** (M) — closes Dreamverse quant escape hatch +7. **Item #1 build_app routes** (M-L) — closes FE-compat +8. **Item D generate_async** (L) — unlock PR; brings along D-12-B (run_async) + closes Q-5/Q-9/PR-7.5 TODOs +9. **Item #5 typed quant_config carrier** (L+L) — final form +10. **Items #6/#7/#8 + D-12-C/D-13-A/D-13-B + #11** — cleanup polish (#9, #10 resolved 2026-05-04) + +If you have a specific user goal (e.g. "ship `BE_FLAVOR=fastvideo` +flavor end-to-end"), that goal dictates the order — read this list as a +menu, not a prescription. + +--- + +## Verification gates per item + +When implementing any item above, evidence required: + +| Phase | Check | +|---|---| +| Build | `lsp_diagnostics` clean on changed files | +| Test | new + relevant existing tests pass; output captured | +| Manual QA | actually run the affected feature end-to-end (per AGENTS.md MANUAL_QA_MANDATE) | +| Regression | full `fastvideo/tests/api/` + `contract/` + relevant SSIM (if NVFP4 touch) | + +For NVFP4 touches: re-run `test_nvfp4_ltx2_wiring.py` + +`test_typed_quant_flow.py` (CPU) + ideally a flashinfer-enabled path +test (manual, not in CI). + +For Dreamverse-side items (DR-1, DR-2): re-run +`Dreamverse/apps/web/npx playwright test e2e/preset-prompt-generation.spec.ts` +end-to-end against the live BE+FE — this is the contract test that +exercises the prompt enhancer through a real session. diff --git a/.agents/memory/dreamverse-integration/pr-roadmap.md b/.agents/memory/dreamverse-integration/pr-roadmap.md new file mode 100644 index 0000000000..2fe1f69f53 --- /dev/null +++ b/.agents/memory/dreamverse-integration/pr-roadmap.md @@ -0,0 +1,141 @@ +# PR Roadmap + +Status of all 17 PRs in the FastVideo public API refactor + streaming +server upstream + Dynamo backend contract + post-deprecation cleanup. + +For design rationale see [design.md](design.md). For streaming-specific +PRs (7.5-7.10) see [streaming-server.md](streaming-server.md). For NVFP4 +work that runs parallel to this sequence see [quantization.md](quantization.md). + +**Last updated:** 2026-05-05 (strategy reversal — single mega-PR #1288 replaces planned splits 7.10/8/LTX-2/NVFP4/post-fixes/agents_cleanup; see [decisions-log.md D-17](decisions-log.md#d-17)). + +## Status legend + +- ✅ **Landed on `origin/main`** +- 🟢 **Open / in flight** — branch exists, may have open PR +- 🟡 **Planned** — designed, not started +- 🔵 **Future** — deferred to post-PR-13 cleanup + +## Landed PRs (0 → 7.7) + +| # | PR | Status | Merge commit | Scope | +|---|---|---|---|---| +| 0 | #1218 [1/n] | ✅ | merged | Parity inventory + typed inference schema | +| 1 | #1218 [1/n] | ✅ | merged | Strict parser/validation/overrides + API tests | +| 2 | #1220 [2/n] | ✅ | merged | Typed `VideoGenerator` constructors + request path + compat | +| 3 | #1226 [3/n] | ✅ | merged | CLI/YAML-first typed config loading for `generate` and `serve` | +| 4 | #1234 [4/n] | ✅ | merged | Preset registry + presets for all 13 model families; `SamplingParam` moved to `fastvideo/api/`; `configs/sample/` deleted entirely | +| 5 | #1237 [5/n] | ✅ | merged | `ServeConfig.default_request` wired into stateless OpenAI server | +| 5.5 | (`5d1d71fc`) | ✅ | merged | Streaming server package skeleton, typed `StreamingConfig`/`GpuPoolConfig`/`PromptEnhancerConfig`/`PromptSafetyConfig`/`WarmupConfig`, `streaming-serve` CLI stub | +| 6 | #1239 [6/n] | ✅ | merged | LTX2 public preset + asset wiring + `gpu_pool.py` typed-kwarg translation | +| 7 | #1250 [7/n] | ✅ | merged | Typed LTX2 continuation state + streaming session store + blob store | +| **7.5** | **#1251** | ✅ | `95fd29e0` (merged 2026-04-26) | Streaming server skeleton (WebSocket + fMP4 + single generator). 8 commits. Deferred TODOs (per-step progress, mid-segment cancellation) carried forward to PR 7.10. | +| **7.6** | **#1257** | ✅ | `eb0a4152` (merged 2026-05-04) | GPU pool upstream + worker subprocess + two-segment warmup. 7 commits squashed. APPROVED by Eigensystem. See [decisions-log.md D-12](decisions-log.md#d-12) for the architectural review. | +| **7.7** | **#1258** | ✅ | `f673423b` (merged 2026-05-04) | Prompt enhancer with `LLMProvider` abstraction. Built-in providers: cerebras, groq. 3 commits squashed. **Public Literal does NOT include `cerebras_ifm`** — open-threads.md item DR-2 covers the gap. See [decisions-log.md D-13](decisions-log.md#d-13) for the architectural review. | +| **7.8** | **#1284** | ✅ | `eb3a3942` (merged 2026-05-04) | Streaming auxiliaries — `prompt/safety.py` (optional fasttext, lazy import), `prompt/rewrite.py`, `session_logger.py` (thread-safe JSONL), `mock_server.py` (build_mock_app + MockGenerator for FE dev). 730 LOC, 2 commits. See [decisions-log.md D-14](decisions-log.md#d-14). | +| **7.9** | **#1286** | ✅ | `2aaeee2a` (merged 2026-05-05) | Streaming router (multi-replica load balancer + WS proxy + `fastvideo router-serve` CLI). Squashed `cd76cf51 + 1ac1e732 + b0b7f59c + a152cb77` (router-polish second-pass; cherry-pick of `40e265b8` from `will/ltx2_sr_port`). See [decisions-log.md D-15](decisions-log.md#d-15) (structural review) + [D-16](decisions-log.md#d-16) (second-pass polish). | + +## In flight (mega-PR #1288) + +| # | PR | Status | Branch | Scope | +|---|---|---|---|---| +| **mega** | **#1288** | 🟢 OPEN, MERGEABLE | `will/ltx2_sr_port` (head `b36bdbc9`) | **Single consolidated landing of the full `will/ltx2_sr_port` chain.** Was originally planned as 6 stacked PRs (slices 1-3 / 4-6 / 7-15 / 16-21 / 22-23 / 24-34). Now landing as one PR — see [decisions-log.md D-17](decisions-log.md#d-17) for the strategy decision. **Contents** (commit-ordered): (1) streaming `generate_async` + `VideoEvent` + Dynamo backend contract (3 commits, was PR 7.10/#1287 closed); (2) server contract docs + Dreamverse/Dynamo shape tests (3 commits, was PR 8); (3) LTX-2 SR runtime port + i2v conditioning + alignment harness (9 commits); (4) NVFP4 wire-up + per-component compile + typed `transformer_quant` flow (6 commits); (5) LTX-2 post-handoff parity fixes — Gemma `to()`, list-of-generators (2 commits); (6) `.agents/memory/dreamverse-integration/` knowledge base + agents Phase 1 cleanup (11 commits). 34 commits total, 71 files, +13,074/-583 LOC. | + +## Closed PRs in this scope + +| # | PR | Status | Why closed | +|---|---|---|---| +| **7.10** | **#1287** | ❌ CLOSED 2026-05-05 | Superseded by mega-PR #1288 — strategy reversal to land everything in one go. Same 3 commits now form the head of #1288. | + +## Deprecated split bookmarks (D-17) + +`will/api_7.10` / `will/api_8` / `will/ltx2_sr_runtime` / `will/ltx2_nvfp4` / `will/ltx2_post_fixes` / `will/agents_cleanup` were the split-PR bookmarks under the abandoned 6-PR plan. They remain locally as historical references but are no longer maintained. STACK.md (top-level) is similarly deprecated. + +## Planned (post-#1288 merge) + +| # | Status | Branch | Scope | +|---|---|---|---| +| 9 | 🟡 | — | LongCat preset migration + colocation (9 model-specific stage files) | +| 10 | 🟡 | — | Hunyuan15 SR preset migration + colocation + SR field migration POC | +| 11 | 🟡 | — | SSIM/performance test migration off legacy `generate_video(..., **kwargs)` | +| 12 | 🟡 | — | Docs + examples migration (includes streaming server + Dynamo) | +| 13 | 🟡 | — | Deprecation cleanup (includes flat LTX2 kwargs the internal `gpu_pool.py` used to consume) | + +## Future (compat.py death sequence) + +After PR 13 lands deprecation warnings, `fastvideo/api/compat.py` (~370 +lines) is the last translation shim between typed public API and legacy +internals (`FastVideoArgs`, `SamplingParam`). + +| # | Status | Scope | Lines removed | +|---|---|---|---| +| 14 | 🔵 reachable | Strip forward translation: `legacy_from_pretrained_to_config`, `legacy_generate_call_to_request`, `_sampling_param_to_request_raw`, `_LEGACY_REQUEST_ALIASES`, `_LTX2_REFINE_FLAT_KEYS`. Depends on PRs 11/12/7.6 callers being migrated. | ~100 | +| 15 | 🔵 | `FastVideoArgs` becomes a `@dataclass` view over `GeneratorConfig` with `@property` accessors backing legacy field names. ~600-line god-object refactor. Depends on PR 14. | reverse-translation half (~150) trivial | +| 16 | 🔵 | `ForwardBatch` reads `GenerationRequest` by reference; kills `request_to_sampling_param` and the `ForwardBatch(**shallow_asdict(sampling_param), …)` spread. `SamplingParam` demoted or deleted. Depends on PR 15. | rest | +| 17 | 🔵 | Move `normalize_generator_config`, `normalize_generation_request`, `load_generator_config_from_file` to `parser.py`. Delete `compat.py`. | file gone | + +PRs 15-17 touch training, distributed, and worker code in addition to +inference path; realistically 1-2 quarters beyond the current plan. + +## Dependency chain + +``` +PR 13 (deprecation) + ↓ +PRs 11, 12, 7.6 (migrate callers) + ↓ +PR 14 (forward translation gone) ─── ~100 lines out of compat.py + ↓ +PR 15 (FastVideoArgs as view) ─── reverse-translation trivial + ↓ +PR 16 (ForwardBatch reads request) ─── SamplingParam demoted + ↓ +PR 17 (move normalizers, delete file) +``` + +## NVFP4 work (out-of-band, parallel to PR 7.5+) + +NOT in the canonical PR sequence. Lives on `will/ltx2_sr_port` +(currently @ `156103b9`) — a separate stack alongside the public-API +upstreaming. See [quantization.md](quantization.md) for what each commit +locks in. + +| Commit range | Topic | +|---|---| +| `cfccd292..b6ac7630` | LTX-2 i2v + SR runtime port + alignment harness | +| `a4760bae..c6c14c55` | NVFP4 LTX-2 wire-up + per-component compile + parity fixes (May 2 handoff) | +| `a5fcd19c..156103b9` | Post-handoff parity/perf fixes | + +## Key landed artifacts (reference points) + +- Parity inventory: [`docs/design/inference_schema_parity_inventory.yaml`](file:///home/william5lin/FastVideo/docs/design/inference_schema_parity_inventory.yaml) + guard [`fastvideo/tests/api/test_schema_parity_inventory.py`](file:///home/william5lin/FastVideo/fastvideo/tests/api/test_schema_parity_inventory.py) +- Typed schema: [`fastvideo/api/schema.py`](file:///home/william5lin/FastVideo/fastvideo/api/schema.py) +- Compat layer: [`fastvideo/api/compat.py`](file:///home/william5lin/FastVideo/fastvideo/api/compat.py) +- Preset system: [`fastvideo/api/presets.py`](file:///home/william5lin/FastVideo/fastvideo/api/presets.py) + per-family `pipelines/basic//presets.py` +- Streaming package skeleton (PR 5.5): [`fastvideo/entrypoints/streaming/`](file:///home/william5lin/FastVideo/fastvideo/entrypoints/streaming/) +- LTX2 typed continuation state (PR 7): [`fastvideo/pipelines/basic/ltx2/continuation.py`](file:///home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/continuation.py) + +## Known notable decisions carried forward + +- **Public inference boundary stays plain dataclasses + plain dict/YAML/JSON** + — not OmegaConf, not runtime config wrappers. +- **Every public entrypoint normalizes into typed config objects** before + touching legacy `FastVideoArgs` or `SamplingParam`. +- **Legacy `generate_video(..., **kwargs)` stays on direct legacy execution + path until PR 11**'s SSIM/performance migration. Prevents golden + baselines from drifting during compat period. +- **Typed requests use schema defaults**; legacy `generate_video(...)` + continues to inherit model-specific `SamplingParam` defaults during + compat period. +- **Preset registry uses explicit `_register_presets()` pattern** matching + `_register_configs()`; lookup keyed by `model_family`. +- **Stateless OpenAI server clones `ServeConfig.default_request`** and + merges user overrides; preset validation runs before legacy generation. +- **Streaming server added as sibling `fastvideo/entrypoints/streaming/`** + rather than extending `fastvideo/entrypoints/openai/` (PR 5.5). + +## Per-PR commit-level detail + +For per-PR commit lists, test plans, and merge criteria, the archived +source [`source-archive/PR-plan.md`](source-archive/PR-plan.md) (1145 lines) +remains the deepest reference. This file is the navigable summary. diff --git a/.agents/memory/dreamverse-integration/quantization.md b/.agents/memory/dreamverse-integration/quantization.md new file mode 100644 index 0000000000..e68e7e4df5 --- /dev/null +++ b/.agents/memory/dreamverse-integration/quantization.md @@ -0,0 +1,229 @@ +# Quantization — NVFP4, LinearBase Fallback, Layer Profiles + +What landed in the May 2 NVFP4 stack, why it's load-bearing, and what's +still owed (`layer_profile`, typed quant carrier, AbsMaxFP8 cleanup). + +For overall API design see [design.md](design.md). For the open +follow-ups see [open-threads.md](open-threads.md). + +**Last updated:** 2026-05-03. + +## NVFP4 — what it is + +NVIDIA's specific block-scaled FP4 format: + +- e2m1 mantissa +- fp32 alpha +- `layout_128x4` scale layout +- group size 16 + +Distinct from MX-FP4 / OCP-FP4 / generic e3m0. The May 2 rename +(`94c983a2`) disambiguated the naming throughout FastVideo's public +surface. + +## Files (current) + +| File | Role | +|---|---| +| [`fastvideo/layers/quantization/nvfp4_config.py`](file:///home/william5lin/FastVideo/fastvideo/layers/quantization/nvfp4_config.py) | `NVFP4Config`, `NVFP4QuantizeMethod`, `convert_model_to_nvfp4` | +| [`fastvideo/layers/quantization/__init__.py`](file:///home/william5lin/FastVideo/fastvideo/layers/quantization/__init__.py) | `QuantizationMethods` literal includes `"NVFP4"`; `get_quantization_config` resolves it | +| [`fastvideo/layers/linear.py`](file:///home/william5lin/FastVideo/fastvideo/layers/linear.py) | `LinearBase.__init__` falls back to `UnquantizedLinearMethod` when `quant_config.get_quant_method` returns None — **load-bearing** | +| [`fastvideo/models/loader/fsdp_load.py`](file:///home/william5lin/FastVideo/fastvideo/models/loader/fsdp_load.py) | `_maybe_convert_model_to_nvfp4` helper detects via `isinstance(quant_method, NVFP4QuantizeMethod)`; calls `convert_model_to_nvfp4` to materialize buffers | +| [`fastvideo/models/dits/ltx2.py`](file:///home/william5lin/FastVideo/fastvideo/models/dits/ltx2.py) | `nn.Linear` → `ReplicatedLinear` for FP4-eligible subset; `_supports_prequantized_input` + `_linear_project_with_optional_prequant` helpers; quant_config + prefix= plumbing | +| [`fastvideo/api/compat.py`](file:///home/william5lin/FastVideo/fastvideo/api/compat.py) | Typed `engine.quantization.transformer_quant: "NVFP4"` resolves to `NVFP4Config()` instance | +| [`fastvideo/fastvideo_args.py`](file:///home/william5lin/FastVideo/fastvideo/fastvideo_args.py) | `__post_init__._apply_transformer_quant` pins `pipeline_config.dit_config.quant_config = NVFP4Config()` | + +## Buffer naming (post-rename) + +| Old | New | +|---|---| +| `_fp4_weight` / `_fp4_alpha` | `_nvfp4_weight` / `_nvfp4_alpha` | +| `_weight_global_sf` | unchanged | +| `convert_model_to_fp4` | `convert_model_to_nvfp4` | +| `FP4QuantizeMethod` | `NVFP4QuantizeMethod` | +| `QuantizationMethods` literal `"FP4"` | `"NVFP4"` | + +Internal-scope torch op namespace `fastvideo_fp4::*` and +`_get_ltx2_fp4_stage_profile` deliberately left as-is — purely internal +naming that mirrors FastVideo-internal. + +## Layer set asymmetry — by design + +`NVFP4Config.fp4_layers` (default `layer_profile="refine"`) covers: + +- `attn1.{to_q,to_k,to_v,to_out}` — full self-attention +- `attn2.{to_q,to_out}` — cross-attn Q + out only (text context not quantized) +- `audio_to_video_attn.{to_q,to_out}` — AV cross Q + out +- `video_to_audio_attn.{to_k,to_v}` — VA cross K + V +- `ffn.{fc_in,fc_out}` — video FFN +- `adaln_single.linear` — but this is `nn.Linear` (not `LinearBase`), + so it never actually gets FP4'd. List entry has no effect; matches + internal. + +**NOT in the set:** + +- audio self-attention (`audio_attn1.*`) +- audio cross-attention (`audio_attn2.*`) +- audio FFN (`audio.ffn.*`) + +Audio path is cheap enough that quant overhead isn't worth it. Test +[`test_basic_av_block_propagates_quant_config_to_all_children`](file:///home/william5lin/FastVideo/fastvideo/tests/ops/quantization/test_nvfp4_ltx2_wiring.py) +locks this in — if you add audio quantization later, update the test. + +## `LinearBase` fallback — DO NOT REMOVE + +[`fastvideo/layers/linear.py:191-202`](file:///home/william5lin/FastVideo/fastvideo/layers/linear.py#L191-L202): when `quant_config.get_quant_method` returns +`None` (layer not in the quant config's set), we fall back to +`UnquantizedLinearMethod`. + +**Removing this fallback would break every non-tagged +`ReplicatedLinear` constructed with an `NVFP4Config`** — the previous +`assert quant_method is not None` would crash on unmatched layers (e.g. +text-encoder K/V projections, audio attention, etc.). + +This is one of the load-bearing changes from `42b30bf9`. + +## `transformer_quant` precedence rules + +`FastVideoArgs._apply_transformer_quant` only writes +`dit_config.quant_config` when it's currently `None`. **If a caller has +explicitly set** `pipeline_config.dit_config.quant_config = NVFP4Config(...)`, +the explicit setter wins. + +Dreamverse's `video_generation.py` relies on this precedence — it sets +`NVFP4Config()` directly via `experimental["pipeline_config"]` because +typed `transformer_quant: "NVFP4"` doesn't yet expose `layer_profile`. +See "Open follow-ups" below. + +## Attention forward optimization + +[`models/dits/ltx2.py`](file:///home/william5lin/FastVideo/fastvideo/models/dits/ltx2.py) +ports `_supports_prequantized_input` and +`_linear_project_with_optional_prequant`. Attention forward +pre-quantizes input once (`quantize_input`), reuses the +`(x_fp4, x_scale, x_global_sf)` tuple for k/v projections when +`context is x` — bit-matches internal's fused path. + +## `prepare_for_compile` protocol + +[`composed_pipeline_base._maybe_compile_pipeline_module`](file:///home/william5lin/FastVideo/fastvideo/pipelines/composed_pipeline_base.py) +calls `getattr(module, "prepare_for_compile", None)` before invoking +`torch.compile`. Defined as a duck-type protocol — no base class method. + +Currently only **Gemma3** implements it (to materialize HF weights +outside Dynamo's tracer). Add to other models that have lazy external +state if you observe compile-time graph breaks. + +## Per-component compile flags + +`CompileConfig` (in +[`fastvideo/api/schema.py`](file:///home/william5lin/FastVideo/fastvideo/api/schema.py)) +gained per-component knobs in `221cb20a`: + +```python +@dataclass +class CompileConfig: + enabled: bool = False # master DiT switch + backend: str = "inductor" + fullgraph: bool = False + mode: str | None = None + dynamic: bool | None = None + extras: dict = field(default_factory=dict) + + # Per-component overlays, None = inherit master `enabled` + text_encoder_enabled: bool | None = None + vae_enabled: bool | None = None + audio_vae_enabled: bool | None = None + + # Per-component kwargs override master when non-empty + dit_kwargs: dict = field(default_factory=dict) + text_encoder_kwargs: dict = field(default_factory=dict) + vae_kwargs: dict = field(default_factory=dict) + audio_vae_kwargs: dict = field(default_factory=dict) +``` + +**`transformer_refine` is auto-compiled with the master DiT flag.** No +separate `enable_torch_compile_refine` flag — by design, refine inherits +DiT compile state to keep typed surface small. Decoupling would add a +new flag, not repurpose existing ones. + +## Quantization commit chain (`will/ltx2_sr_port`) + +| Commit | Locks in | +|---|---| +| `365a66c7 feat(quantization): upstream LTX-2 FP4Config with lazy flashinfer` | Public colocation of FP4Config (resolves dreamverse_review Q-6 option 1); flashinfer lazy-imported in loader helper, no public hard-dep | +| `a4760bae fix(api): propagate generic refine_*` | `_resolve_refine_args()` copies generic `refine_*` knobs onto `ltx2_refine_*` runtime carriers; `_randn_ltx2_video_latents` reverts to `torch.randn` to bit-match internal under single-generator inference | +| `221cb20a feat(api): typed per-component CompileConfig` | `CompileConfig` per-component knobs; matching `FastVideoArgs` carriers; compat layer round-trip | +| `6da342ba feat(compile): per-component compile + transformer_refine + prepare hook` | `composed_pipeline_base.post_init` compiles `transformer_refine` alongside `transformer`/`transformer_2`; per-component compile loops; `prepare_for_compile` hook on Gemma3 | +| `42b30bf9 feat(ltx2): wire FP4 inference` (largest) | `nn.Linear` → `ReplicatedLinear` for FP4-eligible LTX2 subset; `quant_config` + `prefix=` plumbing; `_maybe_convert_model_to_nvfp4` helper; `LinearBase` fallback to `UnquantizedLinearMethod`; typed `transformer_quant` resolution | +| `94c983a2 refactor(quant): rename FP4 → NVFP4` | Mechanical rename across config, methods, buffers, tests | +| `c6c14c55 test(nvfp4): lock LTX-2 wiring + typed transformer_quant flow` | 6+4 tests in `test_nvfp4_ltx2_wiring.py` + `test_typed_quant_flow.py` | +| `a5fcd19c [fix]: lazy-import flash_attn 2 fallback in attention backend` | post-handoff: lazy import to avoid hard flash_attn 2 dep | +| `d4ee5be2 [fix]: avoid model.to() round-trip in Gemma encoder forward` | post-handoff: parity / perf fix | +| `156103b9 [fix]: unwrap list-of-generator before torch.randn in LTX-2 latent prep` | post-handoff: parity fix for list-of-generators (was bit-matching only single-generator path) | + +## Tests + +| Test | Asserts | +|---|---| +| [`fastvideo/tests/ops/quantization/test_nvfp4_ltx2_wiring.py`](file:///home/william5lin/FastVideo/fastvideo/tests/ops/quantization/test_nvfp4_ltx2_wiring.py) (6 tests) | `LTXSelfAttention.to_q/to_k/to_v/to_out` are `ReplicatedLinear`; `NVFP4Config()` attaches `NVFP4QuantizeMethod` on the quantized subset with correct `layer_prefix`; non-tagged projections (cross-attn K/V, audio attn, audio FFN) fall back to `UnquantizedLinearMethod`; `BasicAVTransformerBlock` propagates `quant_config`+`prefix` correctly to all 4 attention modules + FFN | +| [`fastvideo/tests/api/test_typed_quant_flow.py`](file:///home/william5lin/FastVideo/fastvideo/tests/api/test_typed_quant_flow.py) (4 tests) | typed `engine.quantization.transformer_quant: "NVFP4"` → `NVFP4Config()` instance flow; default leaves `transformer_quant` None; explicit `dit_config.quant_config = ...` wins over typed carrier | + +CPU-only by design; do NOT exercise actual FP4 kernels (no flashinfer in +CI). Real kernel coverage requires a CI run with flashinfer installed. + +## Open follow-ups (quantization-specific) + +### #4: Expose `layer_profile` on typed `engine.quantization` + +Today `transformer_quant: "NVFP4"` always constructs `NVFP4Config()` +with default `layer_profile="refine"`. To support stage-1 profiles (no +`attn2.to_out`, no cross-modal AV) via typed config, add +`transformer_quant_layer_profile: str | None = None` and thread it +through: + +- `fastvideo/api/schema.py` — `QuantizationConfig` field +- `fastvideo/api/compat.py` — typed → flat translation +- `fastvideo/fastvideo_args.py` — `_apply_transformer_quant` consumes it + +Dreamverse currently dodges this by setting `NVFP4Config()` directly via +`experimental["pipeline_config"]`. Exposing `layer_profile` removes the +dodge. See [open-threads.md](open-threads.md) #4. + +### #5: Typed `dit_config.quant_config` carrier (replace `experimental["pipeline_config"]`) + +Long-term: design a typed home for an in-memory `PipelineConfig` +instance with mutated `dit_config`. Today `compat.py` recognizes the +`pipeline_config` key in `experimental` and threads it through to +`FastVideoArgs.from_kwargs`. This is fine for short-term but not pretty. + +Heaviest design work in the open queue. May need Oracle consult. + +### #2: AbsMaxFP8 pre-existing test failure + +`fastvideo/tests/ops/quantization/test_absmax_fp8.py::test_create_weights_rejects_invalid_dtype` +fails on `main` and on `will/ltx2_sr_port` with the same error +(`AssertionError not raised`). Verified via `git stash` that the +failure pre-dates NVFP4 work. + +Either: +- Fix the test (`AbsMaxFP8LinearMethod.create_weights` no longer + asserts on invalid dtype — restore the assert if intentional, or drop + the test). + +Self-contained tech debt; small fix. + +## Don't / Cautions + +- **Don't change `NVFP4Config` buffer names back to `_fp4_*`.** Rename + is intentional to disambiguate from MX-FP4 / OCP-FP4. +- **Don't remove the `LinearBase` `UnquantizedLinearMethod` fallback.** + Load-bearing for non-tagged layers when a `quant_config` is set. +- **Don't repurpose `enable_torch_compile` to mean DiT-only.** It also + drives `transformer_refine` and `transformer_2` compile. +- **Don't bypass the typed surface for new options.** New compile / + quant / refine knobs should land on the dataclass + compat.py + + parity inventory together. The existing test suite locks this in. +- **Don't merge to main without a CI run that covers FP4.** Current CI + doesn't run flashinfer-dependent paths; the wiring tests are CPU-only + by design. diff --git a/.agents/memory/dreamverse-integration/runbook.md b/.agents/memory/dreamverse-integration/runbook.md new file mode 100644 index 0000000000..4dcf134c3b --- /dev/null +++ b/.agents/memory/dreamverse-integration/runbook.md @@ -0,0 +1,385 @@ +# Runbook — How to Do Work in This Scope + +Operational how-to for the dreamverse-integration scope. Read after +[state.md](state.md) and [open-threads.md](open-threads.md). + +For design rationale see [design.md](design.md). For who to credit see +[authors.md](authors.md). For PR status see [pr-roadmap.md](pr-roadmap.md). + +**Last updated:** 2026-05-05 (strategy reversed to single mega-PR #1288 on `will/ltx2_sr_port`; #1287 closed; STACK.md split model deprecated per [decisions-log.md D-17](decisions-log.md#d-17)). + +## Worktree contract + +``` +Repo: /home/william5lin/FastVideo +Branch: will/ltx2_sr_port +``` + +Other agents and the user share this worktree concurrently. If `git status` +shows changes you don't recognize, they belong to **someone else's work** — +don't revert, don't `git stash drop`, don't `git checkout -- `. +Switch to `will/ltx2_sr_port` cleanly with `git checkout will/ltx2_sr_port` +(safe if your own working tree is clean) and proceed. + +If your task requires a different branch (e.g. cherry-pick to +`will/api_7.9` for PR #1286 propagation), return to `will/ltx2_sr_port` +when done — that is the assumed default. + +## Branch topology (single mega-PR model) + +The dreamverse-integration work now ships as one PR (#1288) off +`will/ltx2_sr_port`. The split-PR model documented in earlier revisions +of this runbook (and in top-level `STACK.md`) is **abandoned** — +see [decisions-log.md D-17](decisions-log.md#d-17). + +``` +origin/main + ↓ [public-API refactor: PRs 0..7.9 merged on main, latest #1286 = 2aaeee2a] +will/ltx2_sr_port (**PR #1288 head** — single mega-PR, 34 commits, 71 files, +13,074/-583) +``` + +| Branch | Role | Status | +|---|---|---| +| `will/ltx2_sr_port` | **PR #1288 head**, default working branch | OPEN, MERGEABLE | +| `will/api_7.10` / `will/api_8` / `will/ltx2_sr_runtime` / `will/ltx2_nvfp4` / `will/ltx2_post_fixes` / `will/agents_cleanup` | deprecated split-PR bookmarks | local-only historical references; safe to delete | +| `will/ltx2_sr_port-pre-1286-rebase` | safety backup | local-only; preserves the 4 commits dropped during the post-#1286 rebase | + +**Sanity check:** `git merge-base --is-ancestor origin/main will/ltx2_sr_port` +should exit 0. If it doesn't, the branch is in an unexpected state — read +[state.md](state.md) before continuing. + +## After PR #1288 merges + +When the mega-PR squash-merges into `main`: + +1. `git fetch origin main` to pull the merge commit. +2. The entire `will/ltx2_sr_port` content is now on main; the branch can + be deleted (locally + on origin) once all consumers are notified. +3. Delete deprecated split bookmarks: `git branch -D will/api_7.10 + will/api_8 will/ltx2_sr_runtime will/ltx2_nvfp4 will/ltx2_post_fixes + will/agents_cleanup` (local-only, no remote). +4. Optionally remove top-level `STACK.md` (now a historical artifact). + Keep `CO-AUTHORS.md` — still the canonical roster reference. +5. Decide whether to keep `will/ltx2_sr_port-pre-1286-rebase` (safety + backup of the pre-rebase chain) — recommend deleting once #1288 is + merged and verified on main. +6. Update memory dir to reflect the post-merge state — bump + `Last reconciled` headers, mark Item D resolved in + [open-threads.md](open-threads.md), record the merge commit in + [decisions-log.md](decisions-log.md). + +## Historical: split-PR re-slice protocol (deprecated) + +Prior revisions of this runbook documented a 10-step re-slice protocol +for the abandoned 6-PR split model. That protocol is now obsolete. +The post-#1286 rebase (2026-05-05) was the last execution of it; details +are preserved in [state.md](state.md) "Post-#1286 rebase summary" and +git history at commit `b34d9704`. + +## Verification + +### Lint (pre-commit) + +```bash +pre-commit run --files +``` + +- Binary: `/home/william5lin/miniconda3/envs/fv-main/bin/pre-commit`. + NOT `.venv/bin/pre-commit` — that doesn't exist in this worktree. +- Auto-applies yapf reformatting; re-stage modified files after. +- Hook chain: yapf → ruff → codespell → mypy → spaces-check. +- Memory dir (`.agents/memory/`) is yapf/ruff/mypy excluded — only + "spaces" runs. Memory edits don't need lint, but DO use UTF-8 and + consistent line endings. + +### Tests + +Router tests (PR #1286 scope): +```bash +.venv/bin/python -m pytest fastvideo/tests/entrypoints/streaming/test_router.py -v --no-header +``` + +Stack baseline (May 2 handoff suite — re-run when you change anything in +api/, contract/, or LTX-2 paths): +```bash +.venv/bin/python -m pytest \ + fastvideo/tests/api/ \ + fastvideo/tests/contract/ \ + fastvideo/tests/ops/quantization/test_nvfp4_*.py \ + tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py \ + -q --no-header +``` + +Expected baselines: +- May 2 handoff (`156103b9`): 222 passed, 1 skipped. +- Post-D-16 (`a152cb77` / `09647a30`): +7 router tests pass on top. + +### LSP + +Use `lsp_diagnostics` on changed files BEFORE running build. Pre-existing +warnings to ignore (predate this work): + +- `fastvideo/entrypoints/streaming/router/main.py:37` — `Task` generic. +- `fastvideo/entrypoints/cli/router_serve.py:55` — `_SubParsersAction` generic. + +### gh CLI for PR status + +```bash +# PR #1286 quick status +gh pr view 1286 --json headRefOid,mergeable,statusCheckRollup \ + --jq '{headRefOid, mergeable, checks: [.statusCheckRollup[] | {name, status, conclusion}]}' + +# All commits in a PR + co-author check +gh pr view 1286 --json commits \ + --jq '.commits[] | {oid: .oid[0:8], msg: .messageHeadline, author: .authors[0].login}' +``` + +## Commit workflow + +### Subject convention + +`[type] : ` — keep ≤ 72 chars. + +Types observed in this scope: `feat`, `fix`, `test`, `docs`, `chore`, +`refactor`. Scopes observed: `streaming`, `dreamverse-integration`, +`api`, `quant`, `ltx2`, `nvfp4`, etc. + +Examples: +- `[fix] streaming: router polish — bridge cancel + state machine + deps` +- `[docs] dreamverse-integration: add authors.md + track D-16 router polish` + +### Body convention + +Bullet list, one bullet per file or concern. Why-before-what. Wrap at +~80 chars (yapf doesn't reformat commit messages; readability is on you). + +### Co-author trailers (REQUIRED on every commit) + +The 4 trailers in [authors.md](authors.md) MUST appear on every commit +in this scope. Use `--trailer` flags or write the body to a file with +`-F` — DO NOT use multiple `-m` blocks for the trailers (each `-m` is +its own paragraph and git's trailer parser only reads the LAST paragraph, +yielding 1 trailer parsed instead of 4). + +**Inline `--trailer` form (preferred for short commits):** + +```bash +git commit -m "subject" -m "body..." \ + --trailer "Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com>" \ + --trailer "Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>" \ + --trailer "Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>" \ + --trailer "Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>" +``` + +**File form (preferred for multi-paragraph bodies):** + +```bash +cat > /tmp/opencode/msg.txt <<'EOF' +[type] scope: subject + +* Bullet one with rationale. +* Bullet two with rationale. + +Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com> +Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com> +Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com> +Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com> +EOF +git commit -F /tmp/opencode/msg.txt +``` + +The trailers MUST be a single block at the end of the message with no +blank lines between them. + +**Verify trailers parsed:** + +```bash +git log -1 --format='%(trailers:key=Co-authored-by,valueonly)' +``` + +Should print 4 lines (one per author). If only 1 line, you have the +multi-`-m` bug — amend with `-F` to fix (allowed if commit is unpushed +and you authored it in this session per AGENTS.md amend rules). + +### NEVER add to commits + +Per [`AGENTS.md`](../../../AGENTS.md): + +- AI co-authors (Claude, GPT, Codex, Cursor, etc.) — explicitly forbidden +- "Generated with Claude Code" footer — explicitly forbidden +- `--no-verify` to skip pre-commit — explicitly forbidden + +## Push + PR propagation + +### Pushing `will/ltx2_sr_port` (top of stack) + +```bash +git push origin will/ltx2_sr_port # fast-forward, no force needed +``` + +If git wants to force-push, you've rewritten history. STOP and verify: + +```bash +git log origin/will/ltx2_sr_port..will/ltx2_sr_port # local-only commits +git log will/ltx2_sr_port..origin/will/ltx2_sr_port # remote-only commits +``` + +Force-push requires explicit user confirmation per `AGENTS.md`. + +### Propagating fixes to PR #1286 (`will/api_7.9`) + +When a fix is in router code (`fastvideo/entrypoints/streaming/router/`, +`cli/router_serve.py`, `tests/entrypoints/streaming/test_router.py`, +or `pyproject.toml` router-related), it must land on BOTH branches. +Cherry-pick avoids any force-push: + +```bash +# 1. Commit on will/ltx2_sr_port first (working branch) +git add +git commit -F /tmp/opencode/msg.txt # with trailers per above + +# 2. Cherry-pick onto will/api_7.9 (creates a separate SHA, identical diff) +git checkout will/api_7.9 +git cherry-pick +git push origin will/api_7.9 # fast-forward, no force + +# 3. Return to working branch +git checkout will/ltx2_sr_port + +# 4. Verify PR #1286 picked it up +gh pr view 1286 --json headRefOid --jq '.headRefOid' +``` + +Two SHAs for the same diff — they'll dedupe naturally on the next +bulk-rebase via the trailer-injection rebase command in +[authors.md](authors.md). + +### When a fix is memory-dir-only + +`.agents/memory/dreamverse-integration/` lives in the `agents_cleanup` +layer of the stack — it does NOT belong on `will/api_7.9`. Memory updates +stay on `will/ltx2_sr_port` only. + +### When a fix is non-router code in the integration scope + +Land on `will/ltx2_sr_port`. If that fix needs to ship as a separate PR +(e.g. extending PR 7.10 or starting PR 9), open a new branch off the +right base per [pr-roadmap.md](pr-roadmap.md). + +## Memory dir maintenance + +When state changes, update the memory dir BEFORE moving on. Every file +has a "Last updated" header — bump when you edit. + +| Change | File to update | +|---|---| +| Branch tip moves | [state.md](state.md) "Branch tips" + "Last reconciled" | +| PR opens / merges | [pr-roadmap.md](pr-roadmap.md) status table | +| New decision made | [decisions-log.md](decisions-log.md) — add D-N entry, bump header | +| Open thread resolved | [open-threads.md](open-threads.md) — strikethrough + "Resolved" note | +| New open thread | [open-threads.md](open-threads.md) — priority overview + section | +| New collaborator credited | [authors.md](authors.md) roster + trailer block + bulk-rebase | +| Source doc archived | [source-archive/README.md](source-archive/README.md) + [README.md](README.md) sources table | +| Process / runbook detail changes | [runbook.md](runbook.md) (this file) | + +Cross-link siblings via relative paths. Never duplicate content — link. + +## Common pitfalls + +### `pre-commit` not in `.venv/bin` + +`pre-commit` lives at `/home/william5lin/miniconda3/envs/fv-main/bin/pre-commit`. +The `.venv` here is for the FastVideo package itself, not pre-commit. + +### Trailers split across paragraphs + +`git commit -m A -m B -m C` makes A, B, C separate paragraphs. Git's +trailer parser only reads the LAST paragraph — multiple `-m +"Co-authored-by: ..."` produces 1 trailer parsed, not 4. Use `--trailer` +flags or `-F` with the trailers in a single block at the end. + +### Stash 0 on FastVideo IS NOT yours + +`stash@{0}: WIP on main: 71bfc13d HunyuanVideo plugin` predates this work. +**DO NOT POP.** See [state.md](state.md) "Stashes — DO NOT POP". + +### `AbsMaxFP8` test "failure" is pre-existing + +`fastvideo/tests/ops/quantization/test_absmax_fp8.py::test_create_weights_rejects_invalid_dtype` +fails on `main` and on every branch in this scope. NOT introduced by +integration work. See [open-threads.md](open-threads.md) item #2. + +### Untracked nested clones at repo root + +`dynamo/`, `ray/`, `vllm-omni/` are untracked nested git clones at the +FastVideo repo root. Reference repos for cross-repo work. **Do not +`rm -rf`** — they're someone else's working state. + +### Live services on 8009 / 5274 + +`dreamverse-server` runs on 8009 (warmed GPU worker), Next.js dev server +on 5274. Don't start new instances on those ports without checking +[state.md](state.md) "Live services" first. + +### Branch may have been switched by another agent + +Other agents share this worktree. If `git branch --show-current` returns +something other than `will/ltx2_sr_port`, switch back cleanly with +`git checkout will/ltx2_sr_port` — don't disturb their work, don't +discard their uncommitted changes. + +### Force-push policy + +Per `AGENTS.md`: never force-push without explicit user confirmation. +For trailer fixes on already-pushed commits, prefer the bulk-rebase +command in [authors.md](authors.md) — safe to re-run. + +### Two trailerless commits in PR #1286 + +`a152cb77` (on `will/api_7.9`) and `40e265b8` (now-superseded ancestor +on `will/ltx2_sr_port`) lack the 4 co-author trailers. **Accepted gap** +per user decision — see [authors.md](authors.md) "Known gaps". + +## Self-test (verify your context is loaded) + +After reading the memory dir, you should be able to answer: + +1. What branch should I be on? → `will/ltx2_sr_port` +2. What's the active open PR in this scope? → #1286 on `will/api_7.9` +3. Where does PR #1286 land in the stack? → Bottom; ancestor of `will/ltx2_sr_port` +4. Who do I credit on every commit? → 4 authors per [authors.md](authors.md) +5. Where do memory updates land? → `will/ltx2_sr_port` only (NOT api_7.9) +6. What's the next-priority open thread? → See [open-threads.md](open-threads.md) "Recommended pull order" — D-8 verify is current top +7. What pre-existing failure can I ignore? → AbsMaxFP8 test (item #2) +8. What's the bulk-rebase command for adding trailers across the stack? → See [authors.md](authors.md) "How the trailers were applied" + +If you can't answer one of these from the memory dir alone, the dir has +a gap — file it as a new entry in [open-threads.md](open-threads.md) +before continuing. + +## First 60 seconds — copy-paste orientation + +```bash +# 1. Confirm branch +cd /home/william5lin/FastVideo +git branch --show-current # should print: will/ltx2_sr_port +# If not, recover: git checkout will/ltx2_sr_port + +# 2. Confirm worktree clean (untracked nested clones expected) +git status --short + +# 3. Confirm PR #1286 head matches expected api_7.9 tip +gh pr view 1286 --json headRefOid --jq '.headRefOid' +git rev-parse will/api_7.9 # should match PR head + +# 4. Confirm your context vs the memory dir +git log -1 --oneline +cat .agents/memory/dreamverse-integration/state.md | head -30 + +# 5. Confirm live services still running +curl -s http://localhost:8009/readyz | head -c 200 +curl -s http://localhost:5274/ -o /dev/null -w "%{http_code}\n" +``` + +If any of those produce unexpected output, read [state.md](state.md) +before changing anything. diff --git a/.agents/memory/dreamverse-integration/source-archive/PR-plan.md b/.agents/memory/dreamverse-integration/source-archive/PR-plan.md new file mode 100644 index 0000000000..8b4d9a0f81 --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/PR-plan.md @@ -0,0 +1,1145 @@ +# FastVideo API Refactor PR Plan + +## Related Documents +- [apirefactor.md](apirefactor.md) — design spec this plan implements +- [.agents/exploration/streaming-server-upstream-plan.md](.agents/exploration/streaming-server-upstream-plan.md) — plan for upstreaming the internal LTX2 streaming server and Dynamo backend contract (shapes PRs 5.5-7.10) +- `../FastVideo-internal/.agents/exploration/rebase-upstream-fastvideo.md` — rebasing FastVideo-internal onto upstream (in progress; enables PRs 6-8) +- `../FastVideo-internal/ui/ltx2-streaming/` — upstream source for streaming server (server/, gpu_pool.py, prompt_enhancer.py, router/) +- `../dynamo/` — local clone of ai-dynamo/dynamo; backend shape at `components/src/dynamo/sglang/` is the template for the FastVideo native backend consumed in PR 7.10 +- https://github.com/ai-dynamo/dynamo/pull/7544 — draft PR (CLOSED) that promotes FastVideo to a native Dynamo backend; establishes the contract this plan must satisfy + +## Status + +Working plan for implementing the design in [apirefactor.md](apirefactor.md). + +### PR Landing History + +Landed: +- PR 0 + PR 1 as `[1/n]` (#1218) on `main` — parity inventory + typed inference schema + strict parser/validation/overrides + API test suite +- PR 2 as `[2/n]` (#1220) on `main` — typed `VideoGenerator` constructors and request path + compatibility translation +- PR 3 as `[3/n]` (#1226) on `main` — CLI/YAML-first typed config loading for `generate` and `serve` +- PR 4 as `[4/n]` (#1234) on `main` — preset registry + presets for all 13 model families; `SamplingParam` moved to `fastvideo/api/`; `fastvideo/configs/sample/` deleted entirely +- PR 5 as `[5/n]` (#1237) on `main` — `ServeConfig.default_request` wired into the stateless OpenAI server +- PR 5.5 as `[5.5/n]` (`5d1d71fc`) on `will/api_5.5` — streaming server package skeleton, typed `StreamingConfig`/`GpuPoolConfig`/`PromptEnhancerConfig`/`PromptSafetyConfig`/`WarmupConfig`, `streaming-serve` CLI stub +- PR 6 as `[6/n]` (#1239) on `main` — LTX2 public preset + asset wiring + gpu_pool translation +- PR 7 as `[7/n]` (#1250) on `main` — typed LTX2 continuation state + streaming session store + blob store + +In progress: +- PR 7.5 (#1251) on `will/api_7.5` — streaming server skeleton (WebSocket + fMP4 + single generator). Open for review. +- PR 7.6 on `will/api_7.6` — GPU pool upstream. Branch ready, not yet PR'd; rebased on `will/api_7.5`. + +Remaining (PR 7.7 onward): +- PR 7.7: prompt enhancer upstream with `LLMProvider` abstraction +- PR 7.8: streaming auxiliaries (prompt safety, session logger, mock server, rewrite) +- PR 7.9: router upstream +- PR 7.10: Dynamo backend contract — `VideoGenerator.generate_async` + health-check helper; the Dynamo backend package itself lives in the Dynamo repo +- PR 8: internal-UI ↔ public-server contract docs + Dynamo integration reference +- PR 9: LongCat preset migration + colocation +- PR 10: Hunyuan15 SR preset migration + colocation + SR field migration POC +- PR 11: SSIM/performance test migration +- PR 12: docs/examples migration (includes streaming server + Dynamo) +- PR 13: deprecation cleanup (includes flat LTX2 kwargs the internal gpu_pool used to consume) + +PRs 5.5-7.10 are driven by the decision to upstream `FastVideo-internal/ui/ltx2-streaming/server/` into the public repo; see [streaming-server-upstream-plan.md](.agents/exploration/streaming-server-upstream-plan.md). + +### Landed Artifacts + +Reference points for later PRs: + +- parity inventory: [inference_schema_parity_inventory.yaml](/home/william5lin/FastVideo/docs/design/inference_schema_parity_inventory.yaml) + guard [test_schema_parity_inventory.py](/home/william5lin/FastVideo/fastvideo/tests/api/test_schema_parity_inventory.py) +- typed public schema: [schema.py](/home/william5lin/FastVideo/fastvideo/api/schema.py) +- parser / validation / overrides: [parser.py](/home/william5lin/FastVideo/fastvideo/api/parser.py), [errors.py](/home/william5lin/FastVideo/fastvideo/api/errors.py), [overrides.py](/home/william5lin/FastVideo/fastvideo/api/overrides.py) +- compatibility translation + typed result: [compat.py](/home/william5lin/FastVideo/fastvideo/api/compat.py), [results.py](/home/william5lin/FastVideo/fastvideo/api/results.py) +- typed `VideoGenerator`: [video_generator.py](/home/william5lin/FastVideo/fastvideo/entrypoints/video_generator.py) +- CLI typed config loading: [inference_config.py](/home/william5lin/FastVideo/fastvideo/entrypoints/cli/inference_config.py), [generate.py](/home/william5lin/FastVideo/fastvideo/entrypoints/cli/generate.py), [serve.py](/home/william5lin/FastVideo/fastvideo/entrypoints/cli/serve.py), [main.py](/home/william5lin/FastVideo/fastvideo/entrypoints/cli/main.py) +- preset system: [presets.py](/home/william5lin/FastVideo/fastvideo/api/presets.py) plus `fastvideo/pipelines/basic//presets.py` for all 13 families (Wan, LTX2, Hunyuan, Hunyuan15, HYWorld, GameCraft, LingBotWorld, MatrixGame, GEN3C, Cosmos, Cosmos25, SD35, TurboDiffusion, LongCat); `model_family` + `default_preset` on `ConfigInfo` in [registry.py](/home/william5lin/FastVideo/fastvideo/registry.py) +- `SamplingParam` canonical home: [sampling_param.py](/home/william5lin/FastVideo/fastvideo/api/sampling_param.py) — all 12 subclass files removed; defaults flow through `SamplingParam.from_pretrained()` → `_from_preset()` +- API test suite: [fastvideo/tests/api](/home/william5lin/FastVideo/fastvideo/tests/api) (98+ tests) +- streaming package + config types (PR 5.5): `fastvideo/entrypoints/streaming/` +- LTX2 preset + asset wiring + typed pipeline / refine config (PR 6): `fastvideo/pipelines/basic/ltx2/`, `fastvideo/api/schema.py` typed `pipeline.preset_overrides`/`pipeline.components`, `compile.extras`, `gpu_pool.py` translation +- LTX2 typed continuation state (PR 7): [`fastvideo/pipelines/basic/ltx2/continuation.py`](/home/william5lin/FastVideo/fastvideo/pipelines/basic/ltx2/continuation.py) with `LTX2ContinuationState`, `_pack_frame_blobs`/`_unpack_frame_blobs` (bounds-checked), public `ContinuationState` envelope on `GenerationRequest`, `register_continuation_kind` registry validation in [`compat.py`](/home/william5lin/FastVideo/fastvideo/api/compat.py) +- streaming session + blob stores (PR 7): [`fastvideo/entrypoints/streaming/session_store.py`](/home/william5lin/FastVideo/fastvideo/entrypoints/streaming/session_store.py) + +### Notable Decisions Carried Forward From Landed PRs + +- public inference boundary remains plain dataclasses + plain dict/YAML/JSON (not OmegaConf or runtime config wrappers) +- every public entrypoint normalizes into typed config objects before touching legacy `FastVideoArgs` or `SamplingParam` +- legacy `generate_video(..., **kwargs)` stays on its direct legacy execution path until PR 11's SSIM/performance migration — prevents golden baselines from drifting during the compat period +- typed requests use schema defaults while legacy `generate_video(...)` continues to inherit model-specific `SamplingParam` defaults during the compat period +- preset registry uses an explicit `_register_presets()` pattern matching `_register_configs()`; lookup keyed by `model_family` +- stateless OpenAI server clones `ServeConfig.default_request` and merges user overrides; preset validation runs before legacy generation +- streaming server added as sibling `fastvideo/entrypoints/streaming/` rather than extending `fastvideo/entrypoints/openai/` (PR 5.5 added only the package stub + typed config; live server lands in PRs 7.5-7.9) + +## Goals + +- Break the API refactor into reviewable PRs. +- Keep each PR testable and bisectable. +- Add explicit CI coverage for the new inference schema. +- Include the LTX2/Dreamverse upstream work as part of the rollout. +- Avoid one giant mixed refactor that is hard to land or debug. + +## Ground Rules + +- Every PR must leave the repo in a shippable state. +- Every PR must add or update tests with the code change. +- The new schema cannot freeze before a public-field parity inventory is reviewed. +- Compatibility shims stay in place until all downstream entrypoints are migrated. +- Every public inference entrypoint must normalize into typed config objects before touching legacy internals. +- LTX2 upstreaming should be staged so private Dreamverse behavior is not partially depended on before the public FastVideo API can represent it. +- Private Dreamverse-only fields should be translated in a private adapter layer, not silently turned into public FastVideo compatibility promises. +- GPU-heavy validation should remain in Buildkite or nightly jobs, but core API and config semantics must be covered by regular CI. +- Public inference terminology should use `config`, not `document`. +- The canonical public inference boundary should remain plain dataclasses plus plain dict/YAML/JSON, not OmegaConf objects or runtime-specific config wrappers. + +## Known Technical Debt + +Identified during PR 0-5.5 review; deferred to later PRs. + +### Model-specific fields in shared schema + +Most model-specific SamplingParam fields were promoted to base `SamplingParam` in PR 4 (LTX2 multi-modal CFG/STG, HYWorld attention masks, GameCraft camera/action control). These are classified as `preset_owned` in the parity inventory. + +`SamplingConfig` still contains fields used by only one or two model families: +- `height_sr`, `width_sr`, `num_inference_steps_sr` (Hunyuan15 SR) — target PR 10 +- `guidance_scale_2`, `boundary_ratio` (Wan2.2, LingBotWorld) + +`InputConfig` still contains model-specific conditioning fields: +- `mouse_cond`, `keyboard_cond`, `grid_sizes` (MatrixGame) +- `c2ws_plucker_emb` (LingBotWorld) +- `refine_from`, `stage1_video` (LongCat) — target PR 9 + +Target: migrate to preset-owned typed override classes or request extensions. + +### LTX2-specific defaults leaked onto shared `SamplingParam` (CFG-force fixed in PR 6; remaining fields still leak) + +**Fixed in PR 6**: `SamplingParam.ltx2_cfg_scale_video` / +`ltx2_cfg_scale_audio` class defaults were `3.0` / `7.0`, which tripped +`ForwardBatch.__post_init__`'s +`ltx2_text_cfg_enabled = (ltx2_cfg_scale_video != 1.0 or ltx2_cfg_scale_audio != 1.0)` +check → force-enabled `do_classifier_free_guidance` for every model +family whose preset didn't explicitly reset them. Surfaced as the +TurboDiffusion I2V SSIM crash +(`text_encoding.py:81` asserting `isinstance(batch.negative_prompt, str)`). +Defaults are now `1.0` (CFG off); `ltx2_base` preset already sets +`3.0`/`7.0` explicitly so its behavior is unchanged. Regression guard +at `test_presets.py::TestPresetDefaultTypes::test_ltx2_cfg_defaults_are_off`. + +**Still leaking (not fixed)**: `SamplingParam` still carries the other +LTX-2 multi-modal knobs — `ltx2_modality_scale_video`/`_audio`, +`ltx2_rescale_scale`, `ltx2_stg_scale_video`/`_audio`, +`ltx2_stg_blocks_video`/`_audio` — with LTX-2-on class defaults +(`3.0`/`3.0`/`0.7`/`1.0`/`1.0`/`[29]`/`[29]`). These only affect LTX-2 +denoising stages today (no cross-family code path reads them), so +there's no active bug, but they are still "model-specific fields in +shared schema" tech debt. + +**Consequence for existing LTX-2 reference videos**: `ltx2_distilled` +and `ltx2_two_stage` presets don't explicitly set +`ltx2_cfg_scale_video`/`_audio`, so they previously inherited +`3.0`/`7.0` (CFG accidentally on). After the default-change they +inherit `1.0`/`1.0` (CFG off). This matches the design intent for +few-step distilled variants but may shift SSIM output. Re-seed +`ltx2_distilled` + `ltx2_two_stage` references if the existing refs +fail threshold; alternatively, explicitly set the CFG fields on those +presets to preserve old behavior. + +**Remaining cleanup**: subsumed by the "Model-specific fields in +shared schema" entry above — migrate the still-leaking LTX-2 knobs +(`ltx2_modality_scale_*`, `ltx2_rescale_scale`, `ltx2_stg_*`) to a +typed `LTX2SamplingOverride` during the per-model migration sweep. + +### Escape hatch usage + +The compat layer (`compat.py:134`) routes unrecognized legacy kwargs into `generator.pipeline.experimental`, which `generator_config_to_fastvideo_args` dumps back into flat kwargs. This untyped pass-through should shrink as presets absorb model-specific fields; each model migration PR should review and reduce it. + +### Quantization code paths need a careful audit + +Before any public-API freeze, quantization routing needs end-to-end verification — this was not finished in PR 6 and several overlapping surfaces coexist: + +- `QuantizationConfig.text_encoder_quant` / `.transformer_quant` are string fields on `EngineConfig.quantization` (`api/schema.py`). +- `FastVideoArgs.override_text_encoder_quant` is a legacy flat kwarg that compat.py routes to the typed field above. +- Internal's `ui/ltx2-streaming/server/gpu_pool.py:220,231` skips those typed paths entirely and does `pipeline_config.dit_config.quant_config = FP4Config()` — a direct object mutation on the PipelineConfig instance before calling `VideoGenerator.from_pretrained`. +- `fastvideo/layers/quantization/fp4_config.py` is where the actual `FP4Config()` lives; the wiring from the string (`"fp4"`) to that class is not currently covered by any API-layer test. + +Open questions to resolve before freeze: +- Does `transformer_quant="fp4"` construct an `FP4Config()` end-to-end, or is the string path an unused vestige? +- Does `text_encoder_quant` have the same string→class wiring, or different semantics? +- When the realtime runtime (PR 7.6) drops the in-place `dit_config.quant_config = FP4Config()` mutation, which typed path replaces it? +- Should DiT, text-encoder, VAE, and image-encoder quant each be a separate typed field (current `QuantizationConfig` only has DiT + text-encoder)? + +Action: audit `fastvideo/layers/quantization/` ↔ `api/schema.QuantizationConfig` ↔ `FastVideoArgs` ↔ `PipelineConfig.*.quant_config` wiring in one sweep. Add an equivalence test (`"fp4"` string → same runtime behavior as `FP4Config()` instance) before PR 7.6 lands, or document a deliberate decision to keep the object-mutation path. + +### Scattered model-family config layout + +Each family's config is still spread across: +- `fastvideo/configs/pipelines/.py` — PipelineConfig subclasses (DiT, VAE, precision, flow_shift) +- `fastvideo/api/sampling_param.py` — base SamplingParam with promoted model-specific fields +- `fastvideo/pipelines/basic//` — pipeline + presets +- `fastvideo/pipelines/stages/` — model-specific stages mixed with shared stages + +PR 4 deleted `fastvideo/configs/sample/` entirely and moved defaults to `pipelines/basic//presets.py`. Remaining colocation targets: `configs/pipelines/.py` and model-specific stages in `pipelines/stages/`. See "Pipeline Package Structure" below. + +### Minor duplication + +`_serialize_config` in `request_metadata.py` duplicates `config_to_dict` in `parser.py`. Consolidate when convenient. + +## Config Layer Plan + +The repo should end up with fewer inference-facing config abstractions. Layers with distinct jobs: + +1. **Public Inference Config** — `fastvideo.api`: `GeneratorConfig`, `GenerationRequest`, `RunConfig`, `ServeConfig`, `ContinuationState`, preset selection, stage overrides. Stable API for Python, CLI, YAML/JSON, and OpenAI/server request normalization. Strict, typed, validated. + +2. **Legacy Inference Compatibility Config** — transitional internal layer: `FastVideoArgs`, `SamplingParam` (now at `fastvideo/api/sampling_param.py` with preset-based defaults), legacy inference-facing `PipelineConfig` usage. Shrinks as the runtime migrates to use the typed API directly. + +3. **Pipeline Preset / Default Config** — model-owned presets at `fastvideo/pipelines/basic//presets.py`. Engine/architecture config from `configs/pipelines/.py` should be **colocated** with pipeline implementations (`pipelines/basic//pipeline_configs.py`) but not absorbed by presets — those classes carry internal component wiring (DiT, VAE, precision) that is not user-facing. + +4. **Model Architecture / Component Config** — `fastvideo/configs/models/` stays separate (architecture and component loading, not user request normalization). + +5. **Runtime Execution State** — `ForwardBatch` in `pipeline_batch_info.py` is internal execution state, not a public config object. + +6. **Training Config** — stays separate from inference config. At most shares dotted override parsing/merging, YAML/JSON loading helpers, and error-path formatting patterns. + +### Naming Guidance + +- keep public inference schema names namespaced under `fastvideo.api` +- avoid exporting them from top-level `fastvideo/__init__.py` until migration is farther along +- envelope types `RunConfig` / `ServeConfig` get sufficient disambiguation from training config via the namespace +- consider renaming the public engine quantization type to `EngineQuantizationConfig` if a collision arises (deferred) + +### Inference Package Structure + +Current single-file [schema.py](/home/william5lin/FastVideo/fastvideo/api/schema.py) is acceptable but not the final shape. Later cleanup should split by concern (config/model classes, envelope types, parser/loading, compatibility translation). + +## Pipeline Package Structure + +The `fastvideo/pipelines/` package should converge on a layout where each model family directory is self-contained: pipeline implementation, presets, engine/arch config, and model-specific stages all in one place. + +### Current layout + +``` +fastvideo/ +├── configs/ +│ ├── pipelines/ # flat: wan.py, ltx2.py, hunyuan15.py, ... +│ │ # (PipelineConfig subclasses — engine/arch config) +│ └── models/ # hierarchical: dits/, vaes/, encoders/ +│ # (architecture definitions — stays here) +│ # note: configs/sample/ was removed entirely in PR 4 +├── pipelines/ +│ ├── basic/ +│ │ ├── wan/ # pipeline classes + presets.py +│ │ ├── ltx2/ # pipeline classes + presets.py +│ │ ├── hunyuan15/ # pipeline classes + presets.py +│ │ └── ... # all families have presets.py (PR 4) +│ └── stages/ # flat: 14 shared + 19 model-prefixed files +└── api/ # public inference schema + preset registry + + # sampling_param.py (canonical SamplingParam home) +``` + +### Target layout + +``` +fastvideo/ +├── configs/ +│ ├── models/ # stays: architecture definitions (dits/, vaes/, ...) +│ └── pipelines/ +│ └── base.py # stays: PipelineConfig base class +├── pipelines/ +│ ├── basic/ +│ │ ├── wan/ +│ │ │ ├── wan_pipeline.py +│ │ │ ├── ... +│ │ │ ├── presets.py # user-facing presets (done in PR 4) +│ │ │ └── pipeline_configs.py # from configs/pipelines/wan.py +│ │ ├── ltx2/ +│ │ │ ├── ltx2_pipeline.py +│ │ │ ├── presets.py +│ │ │ ├── pipeline_configs.py # from configs/pipelines/ltx2.py +│ │ │ └── stages/ # from pipelines/stages/ltx2_*.py +│ │ ├── longcat/ +│ │ │ ├── longcat_pipeline.py +│ │ │ ├── presets.py +│ │ │ ├── pipeline_configs.py +│ │ │ └── stages/ # from pipelines/stages/longcat_*.py (9 files) +│ │ └── ... +│ └── stages/ # shared-only stages remain here +│ ├── base.py, denoising.py, encoding.py, decoding.py, text_encoding.py, ... +└── api/ # unchanged +``` + +### What moves and when + +| Source | Destination | Status | +|--------|------------|--------| +| `configs/sample/.py` | Absorbed by `presets.py` defaults | **Done** (PR 4) — directory deleted entirely | +| `configs/pipelines/.py` | `pipelines/basic//pipeline_configs.py` | Pending — per-model colocation PR | +| `pipelines/stages/_*.py` | `pipelines/basic//stages/` | Pending — optional, per-model PR | + +### What stays + +- **`configs/pipelines/base.py`** — `PipelineConfig` base class, shared CLI arg generation, JSON serialization. +- **`configs/models/`** — Model architecture definitions (DiT, VAE, encoder). +- **`pipelines/stages/` shared stages** — `denoising.py`, `encoding.py`, `decoding.py`, `text_encoding.py`, `timestep_preparation.py`, etc. +- **`pipelines/stages/` model-specific stages** — moving into family directories is optional and should only happen when the family has enough model-specific stages to justify it (LongCat: yes; Wan: no). + +### Cross-family inheritance + +Some families extend another family's PipelineConfig (e.g., `LingBotWorld` extends `Wan2_2_I2V_A14B_Config`; `TurboDiffusion` reuses `t5_postprocess_text` from Wan). After relocation these become cross-family imports; shared utilities like `t5_postprocess_text` should be extracted to a common location to avoid coupling unrelated families. + +### Migration approach + +Config colocation is not a separate PR. Each per-model colocation PR (PRs 6, 9, 10) includes the colocation step for that family: +1. `presets.py` already exists (done in PR 4) +2. Move `configs/pipelines/.py` → `pipeline_configs.py` in family dir +3. `configs/sample/` already removed entirely (done in PR 4) +4. Update `registry.py` imports +5. Update any cross-family imports + +## CI / Testing Baseline + +- GitHub Actions runs pre-commit only in [.github/workflows/ci-precommit.yml](/home/william5lin/FastVideo/.github/workflows/ci-precommit.yml) +- Aggregate/full-suite orchestration via Buildkite status flows ([ci-aggregate-status.yml](/home/william5lin/FastVideo/.github/workflows/ci-aggregate-status.yml), [ci-trigger-full-suite.yml](/home/william5lin/FastVideo/.github/workflows/ci-trigger-full-suite.yml)) +- Useful test anchors: + - [test_video_generator.py](/home/william5lin/FastVideo/fastvideo/tests/entrypoints/test_video_generator.py) + - [test_openai_api.py](/home/william5lin/FastVideo/fastvideo/tests/entrypoints/test_openai_api.py) / [test_openai_api_integration.py](/home/william5lin/FastVideo/fastvideo/tests/entrypoints/test_openai_api_integration.py) (real-server GPU integration belongs in Buildkite/nightly) + - [test_sp_ltx2.py](/home/william5lin/FastVideo/fastvideo/tests/distributed/test_sp_ltx2.py) + - [test_ltx2_pipeline_smoke.py](/home/william5lin/FastVideo/tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py) + - [test_ltx2_registry.py](/home/william5lin/FastVideo/tests/local_tests/test_ltx2_registry.py) + +## New CI Coverage To Add + +### GitHub Actions + +A workflow such as `.github/workflows/ci-inference-api-schema.yml` should run on every PR: + +```bash +pytest -q \ + fastvideo/tests/api \ + fastvideo/tests/entrypoints/test_video_generator.py \ + fastvideo/tests/entrypoints/test_openai_api.py +``` + +`fastvideo/tests/api/` already covers schema parsing, YAML/JSON loading, dotted overrides, strict unknown-key validation, legacy flat-flag translation, and preset selection / stage override validation. + +### Buildkite Fastcheck + +GPU fastcheck jobs for LTX2 preset smoke, preset-to-pipeline dispatch, and one old-vs-new compatibility equivalence check: + +```bash +pytest -q fastvideo/tests/distributed/test_sp_ltx2.py +pytest -q tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py +pytest -q tests/local_tests/test_ltx2_registry.py +``` + +### Buildkite Full Suite + +- multi-stage preset smoke on LongCat and Hunyuan15 +- server default-request merge behavior +- backward compatibility path for legacy CLI and Python kwargs +- real-server integration via [test_openai_api_integration.py](/home/william5lin/FastVideo/fastvideo/tests/entrypoints/test_openai_api_integration.py) + +### Nightly + +- LTX2 continuation/state roundtrip +- Hunyuan15 SR preset smoke +- LongCat distill/refine preset smoke + +## PR Sequence + +### PRs 0-7 (landed) + +See "PR Landing History", "Landed Artifacts", and "Notable Decisions Carried Forward" above. Scope, tests, and merge criteria for the landed PRs are reflected in the linked files; the test suites at `fastvideo/tests/api/` + `fastvideo/tests/entrypoints/` cover the merge criteria. The detailed per-PR sections for PR 6 and PR 7 below are kept as historical context. + +## PR 6: LTX2 Public Preset and Asset Wiring + +### Scope + +Upstream the public LTX2 two-stage preset shape from Dreamverse, but only for repo-visible/public FastVideo surfaces. Also colocate LTX2 config alongside its pipeline implementation. + +Note: `ltx2_base` and `ltx2_distilled` presets already exist (`fastvideo/pipelines/basic/ltx2/presets.py`) and the old `configs/sample/ltx2.py` is gone along with the rest of `configs/sample/` (PR 4). This PR adds the two-stage preset and colocates PipelineConfig. + +### Main Changes + +- add `ltx2_two_stage` preset (two-stage refine) +- add LTX2 stage override types +- map repo-visible LTX2 init-time knobs into: preset selection, component config, preset overrides +- **typed replacements for internal `gpu_pool.py` flat kwargs** (load_kwargs at [gpu_pool.py:233-260](/home/william5lin/FastVideo-internal/ui/ltx2-streaming/server/gpu_pool.py)): + - `ltx2_refine_enabled`, `ltx2_refine_upsampler_path`, `ltx2_refine_lora_path`, `ltx2_refine_add_noise` → `generator.pipeline.preset_overrides.refine.*` (init-time stage config) + - `ltx2_refine_num_inference_steps`, `ltx2_refine_guidance_scale` → `request.stage_overrides.refine.*` (per-request) + - `ltx2_vae_tiling` → typed field on `PipelineSelection` or similar + - `torch_compile_kwargs` (`backend`, `fullgraph`, `mode`, `dynamic`) → first-class fields on `CompileConfig`, with `extras: dict[str, Any]` for uncommon kwargs + - `FP4Config()` integration → typed quantization field path +- do not promise public compatibility for private Dreamverse-only aliases; those belong in the private adapter layer +- **config colocation**: move `configs/pipelines/ltx2.py` → `pipelines/basic/ltx2/pipeline_configs.py`; update `registry.py` imports +- optionally move `pipelines/stages/ltx2_*.py` (4 files) into `pipelines/basic/ltx2/stages/` + +### Why the gpu_pool.py typed-replacement scope matters + +PR 7.6 upstreams `gpu_pool.py` into the public repo. Without this PR 6 expansion, the public server perpetuates the flat-kwarg surface the refactor is eliminating. Every flat kwarg used by `gpu_pool.py` must have a typed home in `GeneratorConfig` / preset overrides / request stage overrides before PR 7.6 can land cleanly. + +Same scoping rule applies to Dynamo. Draft PR ai-dynamo/dynamo#7544 had to reason about FastVideo-internal flat kwarg names to construct `VideoGenerator`; after this PR, Dynamo's `FastVideoArgGroup` can build a typed `GeneratorConfig` directly from its CLI with no awareness of legacy LTX2 names. Hard gate for PR 7.10. + +### Commits + +1. `feat(ltx2): add public ltx2 pipeline presets` +2. `feat(ltx2): add typed refine stage overrides for public ltx2 surfaces` +3. `feat(api): promote common torch.compile kwargs and ltx2 vae tiling to typed fields` +4. `refactor(ltx2): colocate pipeline config and stages with implementation` +5. `test(ltx2): cover preset config normalization, gpu_pool-style kwarg translation, compatibility` + +### Required Tests + +- new `fastvideo/tests/api/test_ltx2_presets.py` +- expand [test_ltx2_registry.py](/home/william5lin/FastVideo/tests/local_tests/test_ltx2_registry.py) +- expand [test_ltx2_pipeline_smoke.py](/home/william5lin/FastVideo/tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py) + +### Merge Criteria + +- public/repo-visible LTX2 setup can be represented by public config +- no new public compatibility promise is made for private-only Dreamverse field aliases +- LTX2 preset smoke passes on GPU CI +- LTX2 config no longer split across `configs/` and `pipelines/` + +## PR 7: LTX2 Continuation State (Public Payload + Server Session) + +### Scope + +Upstream the continuation contract as a **hybrid** public API that supports both opaque client-round-trip payloads AND server-held session state. Both surfaces share one serialization format. + +Decision rationale: [streaming-server-upstream-plan.md § Design Decision 1](.agents/exploration/streaming-server-upstream-plan.md). + +### Main Changes + +- add typed `LTX2ContinuationState` internal dataclass with: + - trailing conditioning frames (or tensor blob ID) + - audio latents (or tensor blob ID) + - segment index / rollout position + - audio sample rate, `video_position_offset_sec`, other metadata +- map request/response state into public `ContinuationState` with `kind="ltx2.v1"` and `payload` serialized from the typed state +- stop requiring app-level raw latent/audio condition fields in the public API +- keep `extensions` only for any remaining experimental fields +- add `SessionStore` interface in `fastvideo/entrypoints/streaming/session_store.py`: + - `snapshot(session_id) -> ContinuationState` + - `hydrate(state: ContinuationState) -> session_id` + - default in-memory implementation; pluggable for redis/etc. later +- specify payload serialization: tensor fields use a blob store indirection when large (bandwidth optimization for the opaque form) + +### Commits + +1. `feat(ltx2): add typed ltx2 continuation state and public payload schema` +2. `feat(streaming): add SessionStore with snapshot and hydrate` +3. `refactor(ltx2): replace raw continuation kwargs with state roundtrip` +4. `test(ltx2): add continuation roundtrip, session snapshot, and compatibility coverage` + +### Required Tests + +- new `fastvideo/tests/api/test_ltx2_state.py` +- new `fastvideo/tests/entrypoints/streaming/test_session_store.py` +- integration tests: generate returns state when requested; next request accepts returned state; invalid state/preset combinations error cleanly; server-held session auto-chains between segments; snapshot + hydrate round-trip across session boundaries +- GPU smoke on Buildkite for state roundtrip + +### Merge Criteria + +- continuation state is a first-class public concept +- both opaque payload and server-held session flows work end to end +- server request translation is able to carry typed state without reverting to ad hoc kwargs +- `gpu_pool.py` style per-GPU implicit chaining is expressible via SessionStore +- `ContinuationState.payload` is JSON-serializable (or uses an opaque blob-ID indirection for large tensors) so it can round-trip through a Dynamo-style RPC for future disaggregated prefill/decode — aggregated Dynamo in PR 7.10 ignores continuation, but the contract must not drift + +## PR 7.5: Streaming Server Skeleton + +Status: open as #1251 on `will/api_7.5`. + +### Scope + +Upstream the minimum viable WebSocket streaming server from `FastVideo-internal/ui/ltx2-streaming/server/main.py`. Single generator (no GPU pool yet), typed `GenerationRequest` as JSON messages, fMP4 output. Prompt enhancer and pool come in later PRs. + +### Main Changes + +- `fastvideo/entrypoints/streaming/server.py`: FastAPI + WebSocket entry +- `fastvideo/entrypoints/streaming/session.py`: session lifecycle, state machine +- `fastvideo/entrypoints/streaming/protocol.py`: JSON WebSocket message schemas (`session_init_v2`, `segment_prompt_source`, `ltx2_segment_start`, `step_complete`, `media_init`, `media_segment_complete`, etc.) +- `fastvideo/entrypoints/streaming/stream.py`: fMP4 encoding via ffmpeg +- `fastvideo/entrypoints/streaming/session_init_image.py`: i2v init image +- Replace the `NotImplementedError` stub from PR 5.5 with the live server +- Wire into `fastvideo streaming-serve` CLI from PR 5.5 +- `docs/design/server_contracts/streaming.md`: WebSocket protocol contract + +### Commits (as shipped) + +1. `feat(streaming): protocol schemas + session state machine` +2. `feat(streaming): fMP4 encoder + session init-image persistence` +3. `feat(streaming): single-generator WebSocket server entry` +4. `test(streaming): server lifecycle + protocol + fMP4 coverage` +5. `docs(streaming): server contract spec` +6. `fix(streaming): restore missing-streaming-block guard + retire stub-era test` +7. `simplify(streaming): review follow-ups (idle timeout via asyncio.wait_for, _send_error helper, _cleanup_session, Protocol-typed generator, cleanup-on-disconnect)` +8. `fix(streaming): enforce idle timeout on receive_json + flag generator-cancellation gap (TODO → PR 7.10)` + +### Required Tests + +- `fastvideo/tests/entrypoints/streaming/test_protocol.py` +- `fastvideo/tests/entrypoints/streaming/test_session.py` +- `fastvideo/tests/entrypoints/streaming/test_session_init_image.py` +- `fastvideo/tests/entrypoints/streaming/test_stream_encoder.py` (fMP4 encode) +- `fastvideo/tests/entrypoints/streaming/test_server.py` (in-process WebSocket lifecycle via `starlette.testclient`) + +### Merge Criteria + +- Single-user WebSocket session end-to-end: connect, init, submit prompts, receive fMP4 chunks, clean shutdown ✓ +- All JSON messages validated against typed protocol schemas ✓ + +### Deferred to later PRs + +- Per-step progress events (only a terminal `step_complete` is emitted today; per-step events need `generate_async` from PR 7.10). +- Mid-segment cancellation on client disconnect (TODO marker in `server.py` near `pool.run`; needs PR 7.10). + +## PR 7.6: GPU Pool Upstream + +Status: branch `will/api_7.6` rebased on `will/api_7.5`, not yet PR'd. + +### Scope + +Upstream `FastVideo-internal/ui/ltx2-streaming/server/gpu_pool.py` with a typed configuration boundary. One subprocess per GPU, one `VideoGenerator` per subprocess, job queue, session-to-GPU binding. + +### Main Changes (as shipped on `will/api_7.6`) + +- `fastvideo/entrypoints/streaming/gpu_pool.py`: `GpuPool` (ABC) + `InProcessGpuPool` + `SubprocessGpuPool` + `PoolAssignment` / `PoolHealth` / `PoolAcquireTimeout` +- `fastvideo/entrypoints/streaming/worker.py`: per-GPU worker entry (`worker_main`) and two-segment warmup helper +- subprocess startup uses typed `GeneratorConfig`, not flat kwargs +- session-to-GPU binding with timeout + queue for contention +- two-segment startup warmup per worker (segment 1 fresh + segment 2 with returned `ContinuationState` so both compile branches are primed) +- `SessionStore` (from PR 7) wired into the pool for per-GPU continuation cache +- `server.py` rerouted to `pool.acquire` / `pool.run` / `pool.release`; `build_app` accepts either a generator (wrapped in `InProcessGpuPool`) or a pre-built `pool=` + +### Commits (as shipped) + +1. `feat [7.6/n]: GPU pool manager with typed worker boundary` +2. `refactor [7.6/n]: route streaming server through GpuPool` +3. `test [7.6/n]: GPU pool coverage (in-process + subprocess)` +4. `fix(streaming): restore missing asyncio import in server` (rebase fixup) +5. `feat(streaming): extract worker.py and add two-segment warmup` + +### Required Tests + +- `fastvideo/tests/entrypoints/streaming/test_gpu_pool.py` (in-process + subprocess via thread-backed factory) ✓ +- `fastvideo/tests/entrypoints/streaming/test_worker.py` (two-segment warmup dispatch + result-shape extractor) ✓ +- GPU smoke on Buildkite for real multi-GPU pool — deferred (separate infra ask) + +### Merge Criteria + +- Single-user multi-segment WebSocket session via the pool path ✓ (covered by `test_server.py`'s `_MockGenerator` on `InProcessGpuPool`) +- No flat kwargs in the pool/worker construction path ✓ (worker_main takes `GeneratorConfig`) +- Session handoff across GPUs (snapshot + hydrate elsewhere) — `SessionStore` wired but no explicit handoff test yet +- Multi-user, multi-GPU WebSocket server end-to-end on real GPUs — deferred (Buildkite GPU smoke) + +### Deferred to later PRs (with rationale) + +- **Audio re-encode (`LTX2AudioEncoder`, `AudioProcessor`)** — internal `_re_encode_audio` runs *inside* the per-step streaming loop (`_stream_av_fmp4_events` / `do_step_ltx2`) to convert waveform → latents for continuation conditioning. The public LTX2 pipeline already populates `LTX2ContinuationState.audio_latents` via `ltx2_audio_decoding.py`, so the whole-segment `pool.run()` path doesn't need a re-encode shim. Re-encode integration is therefore a **per-step streaming concern that belongs with PR 7.10**'s `generate_async`. +- **Deprecate `VideoGenerator.from_pretrained(**flat_kwargs)`** — worker uses typed config, but no public deprecation warning yet. Belongs with PR 13 cleanup. +- **Multi-GPU Buildkite smoke** — separate infra ask; not blocking the upstream PR. + +## PR 7.7: Prompt Enhancer Upstream + +### Scope + +Upstream `FastVideo-internal/ui/ltx2-streaming/server/prompt_enhancer.py` behind an `LLMProvider` abstraction. Ship built-in providers for cerebras and groq; leave the door open for users to add their own. + +Decision rationale: [streaming-server-upstream-plan.md § Design Decision 3](.agents/exploration/streaming-server-upstream-plan.md). + +### Main Changes + +- `fastvideo/entrypoints/streaming/prompt/enhancer.py`: provider-agnostic prompt operations (enhance, auto-extend, rewrite) +- `fastvideo/entrypoints/streaming/prompt/providers/base.py`: `LLMProvider` protocol + `LLMRequest` / `LLMResponse` dataclasses +- `fastvideo/entrypoints/streaming/prompt/providers/cerebras.py`, `groq.py`: built-in implementations +- hot-reloadable system prompts via management endpoint +- fallback retry across providers in priority order +- wire `PromptEnhancerConfig` from PR 5.5 into server startup + +### Commits + +1. `feat(streaming): add LLMProvider protocol and cerebras providers` +2. `feat(streaming): add groq provider` +3. `feat(streaming): add provider-agnostic prompt enhancer orchestration` +4. `feat(streaming): add hot-reloadable system prompt management endpoint` +5. `test(streaming): cover enhancer operations and provider fallback` + +### Required Tests + +- `fastvideo/tests/entrypoints/streaming/prompt/test_providers.py` +- `fastvideo/tests/entrypoints/streaming/prompt/test_enhancer.py` +- mocked provider tests for fallback behavior + +### Merge Criteria + +- Enhancer works end-to-end with the WebSocket server from PR 7.5 +- Third-party users can register their own `LLMProvider` +- Hot-reload of system prompts survives config changes + +## PR 7.8: Streaming Auxiliaries + +### Scope + +Upstream the small auxiliary modules: prompt safety, rewrite payload builder, session logger, mock server. + +### Main Changes + +- `fastvideo/entrypoints/streaming/prompt/safety.py`: optional fasttext classifier; shipped as optional extra `pip install fastvideo[prompt-safety]` +- `fastvideo/entrypoints/streaming/prompt/rewrite.py`: rewrite payload builder (from internal `rewrite_prompt_payload.py`) +- `fastvideo/entrypoints/streaming/session_logger.py`: session JSONL logs +- `fastvideo/entrypoints/streaming/mock_server.py`: mock backend for dev/tests +- `pyproject.toml` optional-dependency entry for `prompt-safety` + +### Commits + +1. `feat(streaming): add optional fasttext prompt safety` +2. `feat(streaming): add rewrite payload builder and session logger` +3. `feat(streaming): add mock server for frontend development` +4. `test(streaming): cover safety, rewrite, mock server` + +### Required Tests + +- `fastvideo/tests/entrypoints/streaming/test_safety.py` (with and without fasttext installed) +- `fastvideo/tests/entrypoints/streaming/test_rewrite.py` +- `fastvideo/tests/entrypoints/streaming/test_mock_server.py` + +### Merge Criteria + +- Optional dependency gating works: safety silently disables when fasttext is not installed +- Mock server can replace the real server for frontend dev + +## PR 7.9: Router Upstream + +### Scope + +Upstream `FastVideo-internal/ui/ltx2-streaming/router/main.py` — a multi-replica load balancer with health checks and WebSocket proxying. + +**Open decision**: in-repo subpackage vs. separate package. Default assumption: in-repo at `fastvideo/entrypoints/streaming/router/`. If review concludes router is too orthogonal to inference, split into `fastvideo-router/` or `fastvideo/contrib/router/`. + +### Main Changes + +- `fastvideo/entrypoints/streaming/router/main.py`: router entry +- `fastvideo/entrypoints/streaming/router/registry.py`: replica registry (from internal `ReplicaRegistry` class) +- health check loop, failure threshold, primary/secondary semantics +- `fastvideo router-serve --config router.yaml` CLI subcommand +- typed `RouterConfig` in `fastvideo/api/schema.py` + +### Commits + +1. `feat(router): add typed RouterConfig and replica registry` +2. `feat(router): add health check loop and failure handling` +3. `feat(router): add websocket proxy and http passthrough` +4. `feat(cli): add router-serve subcommand` +5. `test(router): cover registry, health, and proxy behavior` + +### Required Tests + +- `fastvideo/tests/entrypoints/streaming/router/test_registry.py` +- `fastvideo/tests/entrypoints/streaming/router/test_main.py` +- integration test with two mock backends + +### Merge Criteria + +- Multi-backend deployment works end-to-end +- Health checks flip replicas in and out cleanly +- WebSocket sessions routed to healthy primary, fall back on failure + +## PR 7.10: Dynamo Backend Contract + +### Scope + +Make FastVideo consumable as a native Dynamo backend (same tier as vllm, sglang, trtllm). **FastVideo does not host any Dynamo code** — the backend package (args.py, main.py, backend.py, register.py, health_check.py, adapter) lives entirely in the Dynamo repo at `components/src/dynamo/fastvideo/`, modeled on draft PR ai-dynamo/dynamo#7544. + +This PR lands only the FastVideo-side **contract**: the public Python API that Dynamo's backend package imports. Zero new dependencies, zero `contrib/dynamo/`, zero Dynamo-specific CLI inside FastVideo. + +Decision rationale: [streaming-server-upstream-plan.md § Design Decision 4](.agents/exploration/streaming-server-upstream-plan.md). + +### Main Changes + +- add `VideoGenerator.generate_async(request: GenerationRequest) -> AsyncGenerator[VideoEvent, None]`: + - `VideoProgressEvent(step, total_steps, stage)` + - `VideoPartialEvent(frames_ndarray, index)` (optional; emitted in the streaming path only) + - `VideoFinalEvent(video_bytes_or_tensor, metadata, continuation_state: ContinuationState | None)` +- refactor `VideoGenerator.generate_video(request=...)` to run through `asyncio.run(generate_async(...))` internally, collecting events and returning the final result; preserve the public sync signature +- add `VideoGenerator.default_health_check_request() -> GenerationRequest` (256x256, 8 frames, 1 step) so a Dynamo-side health payload can be derived purely from the public API +- add typed `VideoEvent` hierarchy and `VideoResult` exports under `fastvideo.api` +- stabilize public re-exports: `from fastvideo import VideoGenerator`; `from fastvideo.api import GenerationRequest, SamplingConfig, ContinuationState, VideoResult, VideoEvent, VideoProgressEvent, VideoPartialEvent, VideoFinalEvent` +- PR 7.5's streaming server is rewired to consume `generate_async` directly (was a TODO in PR 7.5) +- audio re-encode integration (`LTX2AudioEncoder`, `AudioProcessor`) for per-step audio continuity inside the streaming fMP4 pipeline (deferred from PR 7.6 — the whole-segment `pool.run()` path doesn't need it; the per-step path does) +- mid-segment cancellation: client disconnect propagates an `asyncio.CancelledError` through `generate_async` so the GPU work stops instead of running to completion (the gap PR 7.5/7.6 flagged with TODO markers around `pool.run`) +- docs PR (PR 8) carries a reference mapping sketch the Dynamo repo's backend package can use as a template — but no code in FastVideo knows about Dynamo + +### Why the async API matters for more than Dynamo + +The same `generate_async` surface that Dynamo consumes also powers: + +- **Streaming server fMP4 pipeline** (PR 7.5) — decoder pulls `VideoPartialEvent` frames directly, no intermediate disk spill +- **OpenAI stateless server** (PR 5) — can opt into progress events for future Server-Sent-Events extension without a second code path +- **Future disaggregation in Dynamo** — prefill/decode split can carry `ContinuationState` across workers using the same event shape + +Building `generate_async` once prevents three near-duplicate progress loops from growing in the three adapters. + +### Commits + +1. `feat(api): add VideoEvent hierarchy and VideoResult public exports` +2. `feat(entrypoints): add VideoGenerator.generate_async event stream` +3. `refactor(entrypoints): reroute generate_video through generate_async internally` +4. `feat(entrypoints): add default_health_check_request helper` +5. `test(entrypoints): cover generate_async event stream and sync wrapper parity` +6. `test(entrypoints): contract test that mock dynamo-style handler wraps public api` + +### Required Tests + +- `fastvideo/tests/entrypoints/test_generate_async.py`: + - event ordering (Progress* → Final) + - sync `generate_video` returns the same result as collecting events from `generate_async` + - cancellation mid-generation cleans up GPU state +- contract test that imports only the public surface (`from fastvideo import VideoGenerator`, `from fastvideo.api import ...`) and wraps it in a mock Dynamo-style handler signature (`async def generate(req: dict, ctx) -> AsyncGenerator[dict, None]`), verifying the wrap is possible without any FastVideo-internal imports +- health-check helper returns a valid `GenerationRequest` that passes `parse_config` + +### Merge Criteria + +- `generate_async` is the canonical execution API; `generate_video` remains for backward-compat but is a thin sync wrapper +- FastVideo's public surface alone is sufficient to construct a Dynamo backend — no flat LTX2 legacy kwargs, no FastVideo-internal imports required +- migration docs (PR 8) include the Dynamo integration skeleton so ai-dynamo/dynamo#7544 or its successor can be reopened and landed with a straightforward handler/adapter written in the Dynamo repo +- streaming server (PR 7.5) consumes `generate_async` directly, no wrapper duplication +- no Dynamo-related file, dep, or import path lives in FastVideo + +## PR 8: Internal-UI ↔ Public-Server Contract + Dynamo Integration Reference + +### Scope + +Document and test the contract between the public FastVideo server stack (OpenAI HTTP, streaming WebSocket) and both of its major external consumers: + +1. The private Dreamverse / internal-UI server stack that previously lived in `FastVideo-internal/ui/ltx2-streaming/`. +2. The Dynamo backend package at `ai-dynamo/dynamo/components/src/dynamo/fastvideo/`. + +This PR does not ship runtime code beyond tests and docs — it locks down the surface so drift between FastVideo, the internal UI, and Dynamo can be caught at review time. + +### Main Changes + +- add WebSocket protocol reference doc (message schemas, state machine, error codes) for the streaming server from PR 7.5-7.9 +- add OpenAI HTTP contract reference for the stateless server from PR 5 +- add Dynamo integration reference: + - end-to-end example showing how `components/src/dynamo/fastvideo/` should be laid out (args.py, main.py, backend.py, register.py, health_check.py) using the public FastVideo API from PR 7.10 + - contract tests that mimic Dynamo's `serve_endpoint(handler.generate, ...)` wrapping and verify FastVideo's `VideoGenerator` works inside it +- migration examples for Dreamverse-style inputs normalized through the typed public API without private-only compat kwargs +- keep translation from private-only fields in the private adapter rather than broadening the public FastVideo compatibility surface + +### Commits + +1. `docs(server): add websocket streaming protocol reference` +2. `docs(server): add openai stateless contract reference` +3. `docs(dynamo): add integration reference for native backend package` +4. `test(contract): add dreamverse-style and dynamo-style contract fixtures` + +### Required Tests + +- public-side tests only +- verify Dreamverse-style inputs can be normalized without private-only APIs +- mock Dynamo handler wraps `VideoGenerator.generate_async` without pulling internal modules — regression guard for PR 7.10 + +### Merge Criteria + +- clear migration path exists for private server consumers +- clear reference path exists for Dynamo integrators (the next iteration of ai-dynamo/dynamo#7544 can reopen against this doc set) +- no new ad hoc public kwargs are added + +## PR 9: LongCat Preset Migration + +> May be combined with PR 10 if preset infrastructure from PR 4 is mature. + +### Scope + +Move LongCat's two-stage user flow behind a named preset and typed refine override surface. Colocate LongCat config — this family has the strongest case for colocation given its 9 model-specific stage files scattered in the shared `pipelines/stages/` directory. + +Note: LongCat presets (`longcat_t2v`, `longcat_i2v`, `longcat_vc`) already exist and are wired via `default_preset` in registry (PR 4). LongCat never had SamplingParam subclasses. This PR adds the stage override types and colocates PipelineConfig + stages. + +### Main Changes + +- add `longcat_distill_refine` multi-stage preset +- add `LongCatRefineStageOverride` +- map current `refine_from`, `stage1_video`, `t_thresh`, `spatial_refine_only`, `num_cond_frames` +- update examples to use the preset path +- **config colocation**: move `configs/pipelines/longcat.py` → `pipelines/basic/longcat/pipeline_configs.py`; update `registry.py` imports +- **stage colocation**: move `pipelines/stages/longcat_*.py` (9 files) → `pipelines/basic/longcat/stages/`; update imports from pipeline classes + +### Commits + +1. `feat(longcat): add distill refine preset and override types` +2. `refactor(longcat): colocate pipeline config and stages with implementation` +3. `refactor(longcat): migrate examples to typed preset based api` +4. `test(longcat): add preset and compatibility coverage` + +### Required Tests + +- API/unit tests for LongCat preset parsing +- smoke coverage for preset dispatch +- if feasible, SSIM/nightly preset regression for LongCat refine path + +### Merge Criteria + +- LongCat public flow no longer requires hand-wired two-generator orchestration in examples +- LongCat config and model-specific stages colocated under `pipelines/basic/longcat/` + +## PR 10: Hunyuan15 SR Preset Migration + +> May be combined with PR 9 if preset infrastructure from PR 4 is mature. + +### Scope + +Represent Hunyuan15 SR as first-class presets rather than pipeline-class knowledge in examples. Colocate Hunyuan15 config alongside its pipeline implementation. This PR also serves as the proof-of-concept for migrating model-specific fields out of the shared schema (`height_sr`, `width_sr`, `num_inference_steps_sr` → SR stage overrides). + +Note: Hunyuan15 presets (`hunyuan15_t2v_480p`, `hunyuan15_i2v_480p_distilled`, `hunyuan15_t2v_720p`, `hunyuan15_i2v_720p_distilled`, `hunyuan15_sr_1080p`) already exist and the old `configs/sample/hunyuan15.py` is gone along with the rest of `configs/sample/` (PR 4). This PR adds the SR stage override types and colocates PipelineConfig. + +### Main Changes + +- add `HunyuanSRStageOverride` typed override class +- add small stage override types where needed +- update examples and request defaults +- **config colocation**: move `configs/pipelines/hunyuan15.py` → `pipelines/basic/hunyuan15/pipeline_configs.py`; update `registry.py` imports +- **SR field migration POC**: declare `height_sr`, `width_sr`, `num_inference_steps_sr` as SR-stage-owned overrides in the preset rather than shared `SamplingConfig` fields + +### Commits + +1. `feat(hunyuan15): add sr presets with typed stage overrides` +2. `refactor(hunyuan15): colocate pipeline config with implementation` +3. `refactor(hunyuan15): route examples through preset selection` +4. `test(hunyuan15): add sr preset config and smoke coverage` + +### Required Tests + +- API parsing tests +- entrypoint tests for preset defaults +- GPU smoke or nightly SR preset checks + +### Merge Criteria + +- Hunyuan15 SR is discoverable and stable through named presets +- Hunyuan15 config colocated under `pipelines/basic/hunyuan15/` +- SR-specific fields validated through preset stage overrides + +## PR 11: SSIM and Performance Test Migration + +### Scope + +Move golden-quality inference tests onto the typed/public API only after preset defaults and request-vs-pipeline override semantics have stabilized. + +This PR should migrate regression suites that currently depend on legacy generation helpers such as `generate_video(..., **kwargs)` and test-only parameter bags. + +### Main Changes + +- migrate SSIM tests to the typed API or typed config path +- migrate performance tests to the typed API or typed config path +- make any remaining request-vs-pipeline compatibility mappings explicit before the migration +- keep output-quality baselines unchanged while swapping the invocation path + +### Commits + +1. `refactor(ssim): migrate golden inference tests to typed api` +2. `refactor(perf): migrate performance tests to typed api` +3. `test(regression): add old-vs-new invocation parity coverage where needed` + +### Required Tests + +- existing SSIM/nightly suites +- existing performance suites +- at least one parity check that runs the same canonical request through both the legacy and typed paths before the legacy path is removed + +### Merge Criteria + +- SSIM/performance tests no longer depend on legacy public inference helpers +- migrated tests do not need ad hoc compatibility exceptions beyond the typed schema/preset surface +- quality baselines remain stable after the invocation-path switch + +## PR 12: Docs and Example Migration + +> May be combined with PR 13 if scope is small. + +### Scope + +Move docs and examples onto the new API so the canonical surface is actually visible to users. + +### Main Changes + +- update inference docs +- add YAML config examples +- update basic examples to `GeneratorConfig` + `GenerationRequest` +- add migration guide from flat kwargs and flags + +### Commits + +1. `docs(api): add typed inference schema and yaml examples` +2. `docs(migration): map legacy kwargs and flags to new config` +3. `refactor(examples): migrate basic inference examples` + +### Required Tests + +- doc example smoke tests if feasible +- at minimum, keep example snippets synchronized with tested APIs + +### Merge Criteria + +- docs no longer present legacy kwargs as the preferred surface + +## PR 13: Deprecation Warnings and Final Cleanup + +> May be combined with PR 12 if scope is small. + +### Scope + +Make the old surface clearly transitional without removing it immediately. + +### Main Changes + +- add deprecation warnings for: + - direct public `FastVideoArgs` + - direct public `SamplingParam` + - `generate_video(..., **kwargs)` legacy paths + - `VideoGenerator.from_pretrained(**flat_kwargs)` (deferred from PR 7.6 — the typed `config=GeneratorConfig` path is already preferred) + - legacy flat CLI flags where appropriate +- remove duplicated now-dead translation paths if any + +### Commits + +1. `chore(deprecation): warn on legacy inference api entrypoints` +2. `test(deprecation): cover warning paths and compat guarantees` + +### Required Tests + +- warning assertion tests +- ensure compatibility still works while warnings are emitted + +### Merge Criteria + +- users have a clear migration runway +- public docs point to the new surface + +## Post-PR 13: Retire `fastvideo/api/compat.py` + +After PR 13 puts deprecation warnings on the flat-kwarg public surface, the compat layer at `fastvideo/api/compat.py` (~370 lines) is the last remaining translation shim between the typed public API and the legacy internal types (`FastVideoArgs`, `SamplingParam`). It bridges five distinct jobs: + +| Function / block | Job | Removable when | +|---|---|---| +| `legacy_from_pretrained_to_config` | forward: flat `from_pretrained(..., **kwargs)` → `GeneratorConfig` | all callers pass `config=GeneratorConfig` | +| `legacy_generate_call_to_request`, `_sampling_param_to_request_raw`, `_LEGACY_REQUEST_ALIASES` | forward: `generate_video(prompt, **kwargs)` → `GenerationRequest` | all callers pass `request=GenerationRequest` | +| `generator_config_to_fastvideo_args`, `_compile_config_to_torch_kwargs` | reverse: typed `GeneratorConfig` → legacy `FastVideoArgs` | runtime consumes `GeneratorConfig` directly | +| `request_to_sampling_param`, `explicit_request_updates` | reverse: typed `GenerationRequest` → legacy `SamplingParam`; supports the `ForwardBatch(**shallow_asdict(sampling_param), …)` spread in `entrypoints/video_generator.py` | `ForwardBatch`/stages read from `GenerationRequest` directly | +| `normalize_generator_config`, `normalize_generation_request`, `load_generator_config_from_file` | input normalization (dict / YAML / JSON → typed) | never — these move to `parser.py`, not deleted | + +Can't be done in a single PR; each job has a different unlock condition. Kill order: + +### PR 14: Strip forward translation + +Prerequisite: flat-kwarg callers already migrated via PRs 11 (SSIM/performance tests), 12 (docs/examples), and 7.6 (realtime runtime upstream — `fastvideo/entrypoints/realtime/local_runtime.py:GPUSlot` currently calls `VideoGenerator.from_pretrained(**load_kwargs)` with the flat LTX-2 kwarg surface PR 6 has now typed). + +- `VideoGenerator.from_pretrained` accepts `(model_path, config: GeneratorConfig | None = None)`; raises if called with extra `**kwargs`. +- `generate_video` accepts `(request: GenerationRequest, …)`; no legacy flat-kwarg path. +- Delete `legacy_from_pretrained_to_config`, `legacy_generate_call_to_request`, `_sampling_param_to_request_raw`, `_LEGACY_REQUEST_ALIASES`, `_LTX2_REFINE_FLAT_KEYS`, `_apply_request_field`. +- Verify with `grep -rn 'from_pretrained.*,\s*\*\*' fastvideo/ examples/ tests/` — expect zero hits outside the implementation itself. + +Net: ~100 lines gone from compat.py; the file still exists for the reverse half. + +### PR 15: `FastVideoArgs` becomes a view over `GeneratorConfig` + +Today `FastVideoArgs` is a ~600-line god-object threaded through every stage, model loader, distributed setup, and worker. `compat.py:generator_config_to_fastvideo_args` exists solely to produce it from a typed config. + +The minimum-disruption path: turn `FastVideoArgs` into a `@dataclass` that holds a `GeneratorConfig` and exposes the legacy field names as `@property` accessors backed by the nested typed fields. No more two-way conversion — one object, one source of truth. + +- `generator_config_to_fastvideo_args` becomes identity / trivial wrapper; inlined at call sites where obvious. +- `FastVideoArgs.from_kwargs` (which silently filters unknown kwargs — see the text-encoder compile case in this session) either disappears or becomes equivalent to passing the GeneratorConfig through. +- All existing `fastvideo_args.dit_cpu_offload` etc. call sites keep working via the property accessors. + +Alternative: sweep `grep -rn 'fastvideo_args\.' fastvideo/` (200+ hits) and rewrite every usage to read from `GeneratorConfig` directly. Cleaner end state, 3-5 PRs of grunt work; defer to opportunistic clean-as-you-go after PR 15. + +Net: reverse-translation half of compat.py (~150 lines) becomes trivial or inlined. + +### PR 16: `ForwardBatch` reads `GenerationRequest` by reference + +Kills `request_to_sampling_param` and the `ForwardBatch(**shallow_asdict(sampling_param), …)` spread currently at `entrypoints/video_generator.py:576`. + +- `ForwardBatch` gains `request: GenerationRequest` field. +- Stages that read `batch.num_inference_steps`, `batch.guidance_scale`, etc. rewrite to `batch.request.sampling.num_inference_steps` / `.guidance_scale`. Mostly mechanical; easily scripted. +- `explicit_request_updates` (used by the OpenAI server for operator-default merge) moves to `parser.py` — it's genuinely useful logic, just mis-filed in compat. +- `SamplingParam` either deleted or demoted to an external-facing convenience dataclass with no internal consumers. + +Depends on PR 15 (or at least on having a typed config reachable from every stage that currently reaches `fastvideo_args`). + +### PR 17: Dissolve `compat.py` + +At this point the file has only the parsing helpers: + +- `normalize_generator_config`, `normalize_generation_request` → move to `fastvideo/api/parser.py` next to `parse_config`. +- `load_generator_config_from_file` → move to `parser.py`. + +Delete `fastvideo/api/compat.py`. + +### Dependency chain + +``` +PR 13 (deprecation) + ↓ +PRs 11, 12, 7.6 (migrate callers) + ↓ +PR 14 (forward translation gone) ───── ~100 lines out of compat.py + ↓ +PR 15 (FastVideoArgs as view) ───── reverse-translation trivial + ↓ +PR 16 (ForwardBatch reads request) ───── SamplingParam demoted + ↓ +PR 17 (move normalizers, delete file) +``` + +PR 14 is reachable within the currently-planned sequence. PRs 15-17 touch training, distributed, and worker code in addition to the inference path; realistically 1-2 quarters of work beyond the current plan. + +## Checkpoint: Post-PR 5.5 Review + +Before starting PR 6, review: + +- Whether PR 6 expansion (typed replacements for the flat LTX2 kwargs used by internal `gpu_pool.py`) is in reach +- Which model-specific fields remain in the shared `SamplingConfig`/`InputConfig` +- Whether the escape hatch pass-through in the compat layer has shrunk +- Whether config colocation (moving `configs/pipelines/.py` into `pipelines/basic//`) can begin with PR 6, or needs a separate prep PR +- Whether PRs 9 and 10 can be combined +- Whether PRs 12 and 13 can be combined +- Whether model-specific stages in `pipelines/stages/` should move into family directories (especially LongCat with 9 model-specific stage files) +- Router: in-repo subpackage vs. separate package decision (defer to PR 7.9) +- Overall timeline and priority for the remaining PR tail + +## Commit Hygiene Rules + +Within each PR: + +- keep structural moves and behavioral changes in separate commits +- keep test additions in separate commits where practical +- do not mix LTX2 private-upstream logic with unrelated CLI refactors +- every compatibility mapping change must have an equivalence test in the same PR + +## Recommended Test Matrix By Layer + +### Schema / Parser Layer + +- public-field parity assertions +- YAML load +- JSON load +- dict load +- serialization roundtrip +- dotted override merge +- strict unknown-key rejection +- type error path reporting + +### Python API Layer + +- `from_config` +- `from_file` +- typed `generate` +- legacy kwargs translation +- batch prompt file behavior + +### CLI Layer + +- `generate --config` +- `serve --config` +- dotted nested overrides +- flat legacy flag compatibility +- precedence tests + +### Preset Layer + +- model/preset resolution +- preset version validation +- stage name validation +- stage override type validation + +### Server Layer + +- `ServeConfig` load +- default request merge +- per-request overrides +- stage override validation +- continuation state request/response path +- real-server health/generation integration in GPU/nightly CI + +### GPU / Pipeline Layer + +- LTX2 preset smoke +- LTX2 continuation roundtrip +- LongCat preset smoke +- Hunyuan15 SR preset smoke +- old-vs-new config equivalence for one or two canonical requests +- after preset/default stabilization, migrate SSIM and performance regression suites off legacy `generate_video(...)` + +## Risk Management Notes + +### Highest Risk Areas + +- legacy kwargs translation subtly changing behavior +- server request translation bypassing validation +- LTX2 continuation semantics drifting from Dreamverse expectations +- preset defaults accidentally changing output quality + +### Mitigations + +- require normalized-config equivalence tests for legacy mapping +- gate server translation through typed parser only +- add LTX2 continuation roundtrip tests before private-server migration +- add smoke and nightly regression coverage for preset defaults + +## Recommended Landing Order Summary + +1. PR 0: schema parity inventory — **done** +2. PR 1: shared schema — **done** +3. PR 2: VideoGenerator typed path + compat — **done** +4. PR 3: CLI/YAML refactor — **done** +5. PR 4: preset registry + preset migration for all families — **done** +6. PR 5: stateless OpenAI server `default_request` merge (narrow) — **done** +7. PR 5.5: streaming server subpackage split (skeleton only, no behavior change) — **done** +8. PR 6: LTX2 public preset + config colocation + typed replacements for every flat LTX2 kwarg used by internal `gpu_pool.py` and consumed by the Dynamo backend +9. PR 7: LTX2 continuation state (hybrid: opaque payload + server session), JSON-serializable payloads +10. PR 7.5: streaming server skeleton (WebSocket + fMP4 + single generator) +11. PR 7.6: GPU pool upstream +12. PR 7.7: prompt enhancer upstream with `LLMProvider` abstraction +13. PR 7.8: streaming auxiliaries (prompt safety, session logger, mock server, rewrite) +14. PR 7.9: router upstream +15. PR 7.10: Dynamo backend **contract** — FastVideo adds `VideoGenerator.generate_async` + health-check helper; the Dynamo backend package itself lives entirely in the Dynamo repo +16. PR 8: internal-UI ↔ public-server contract docs + Dynamo integration reference +17. PR 9: LongCat preset migration + colocation +18. PR 10: Hunyuan15 SR preset migration + colocation + SR field migration POC +19. PR 11: SSIM/performance test migration +20. PR 12: docs/examples (includes streaming server + Dynamo) +21. PR 13: deprecation cleanup (includes flat LTX2 kwargs the internal gpu_pool used to consume) + +This order keeps the API foundation first, lands server normalization before any private-server or Dynamo migration claim, builds one async execution substrate (PR 7.10) that serves both the streaming server and the Dynamo backend, and keeps broad multi-model cleanup only after the shared abstractions have settled. + +## Post-PR 13 Cleanup Goal + +The successful end state is: + +- one public inference config surface under `fastvideo.api` +- one internal preset/default layer near each pipeline +- one internal execution-state layer + +### Target directory structure per model family + +Each family directory under `fastvideo/pipelines/basic//` should be self-contained: + +``` +fastvideo/pipelines/basic// +├── _pipeline.py # pipeline implementation(s) +├── presets.py # user-facing presets (defaults + stage topology) +├── pipeline_configs.py # engine/arch config (from configs/pipelines/) +└── stages/ # model-specific stages (optional, if >2 files) +``` + +### What should be gone + +- `configs/pipelines/.py` — moved to `pipelines/basic//` +- `configs/sample/` — directory removed entirely (**done** in PR 4); defaults absorbed by `presets.py` +- model-prefixed files in `pipelines/stages/` — moved to family `stages/` dirs + +### What remains shared + +- `configs/pipelines/base.py` — `PipelineConfig` base class +- `configs/models/` — architecture definitions (dits/, vaes/, encoders/) +- `pipelines/stages/` — shared stages only (denoising.py, encoding.py, etc.) + +The refactor is not complete if the repo permanently keeps two equal, first-class public inference config systems, or if understanding a single model family requires visiting 4+ scattered directories. diff --git a/.agents/memory/dreamverse-integration/source-archive/README.md b/.agents/memory/dreamverse-integration/source-archive/README.md new file mode 100644 index 0000000000..6cc5a52f65 --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/README.md @@ -0,0 +1,44 @@ +# Source Archive + +These are the original unsynthesized design and integration docs that +predate the consolidation in +[`../`](../). They are **NOT** the source of truth — the synthesized +sibling files in the parent directory are. + +Archived 2026-05-03. All previously untracked. + +## Contents + +| File | Original location | Date | Synthesized into | +|---|---|---|---| +| `apirefactor.md` | `FastVideo/` (repo root) | 2026-04-21 | [`../design.md`](../design.md) | +| `PR-plan.md` (was `PR plan.md` at repo root) | `FastVideo/` (repo root) | 2026-04-25 | [`../pr-roadmap.md`](../pr-roadmap.md) | +| `dreamverse_review.md` | `FastVideo/` (repo root) | 2026-04-26 | [`../decisions-log.md`](../decisions-log.md) + [`../state.md`](../state.md) | +| `handoff-nvfp4-launch-demo.md` | `.agents/exploration/` | 2026-05-02 | [`../state.md`](../state.md) + [`../quantization.md`](../quantization.md) + [`../open-threads.md`](../open-threads.md) | +| `streaming-server-upstream-plan.md` | `.agents/exploration/` | 2026-04-17 | [`../streaming-server.md`](../streaming-server.md) + [`../decisions-log.md`](../decisions-log.md) | +| `dreamverse_integration.md` | `.agents/exploration/` | 2026-04-23 | [`../cross-repo-surfaces.md`](../cross-repo-surfaces.md) | +| `video-generator-config-api-design.md` | `.agents/exploration/` | 2026-04-02 | [`../design.md`](../design.md) (early-draft material) | + +## Why archived (not deleted) + +- Future agents may want the **full unsynthesized rationale** for a + decision the synthesis abbreviated. +- The originals remain useful as a **time machine** for understanding + how the design evolved. +- These docs were never committed to git, so leaving them on disk costs + nothing. + +## When to read the archive vs. the synthesis + +- **Read the synthesis (`../*.md`)** for: current state, decision + status, action items, design rationale at the conceptual level. +- **Read the archive (here)** for: deep historical context, exact wording + of design decisions, full PR plan with all sub-PR commit details, + the original Q-1..Q-9 / D-1..D-11 prose. + +## Maintenance rule + +Do NOT edit files in this archive. They are point-in-time snapshots. +If new design material appears that supersedes an entry here, update the +synthesis (the parent dir) and append a note to that synthesis file — +do not mutate this archive. diff --git a/.agents/memory/dreamverse-integration/source-archive/apirefactor.md b/.agents/memory/dreamverse-integration/source-archive/apirefactor.md new file mode 100644 index 0000000000..482c406d1a --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/apirefactor.md @@ -0,0 +1,838 @@ +# FastVideo API Refactor Design + +## Related Documents +- [PR plan.md](PR%20plan.md) — PR-by-PR implementation plan for this design +- [.agents/exploration/streaming-server-upstream-plan.md](.agents/exploration/streaming-server-upstream-plan.md) — streaming-server upstream + Dynamo backend contract (shapes PRs 5.5-7.10) +- `../FastVideo-internal/.agents/exploration/rebase-upstream-fastvideo.md` — rebasing FastVideo-internal onto upstream (enables PRs 6-8) +- `../FastVideo-internal/ui/ltx2-streaming/` — source for the streaming server being upstreamed (PRs 7.5-7.9) +- `../dynamo/` — local clone of ai-dynamo/dynamo; `components/src/dynamo/sglang/` is the template for FastVideo's native backend landed in PR 7.10 +- https://github.com/ai-dynamo/dynamo/pull/7544 — closed draft PR that establishes the Dynamo backend shape this design must satisfy + +## Status + +Design spec for the public inference API refactor. PRs 0-5.5 are landed; see [PR plan.md](PR%20plan.md) for rollout status and the PR 6+ roadmap. The typed schema, strict parser, preset system, typed VideoGenerator, typed CLI, and stateless OpenAI server default-request merge are all implemented. Streaming package skeleton + typed streaming config types are in place; live streaming server + Dynamo contract are the next milestones. + +## Executive Summary + +FastVideo should move to a single typed nested inference schema that is shared across: + +- Python API +- CLI +- YAML/JSON config files +- OpenAI/server request translation + +The core split is: + +- `GeneratorConfig`: generator-instance lifetime settings +- `GenerationRequest`: per-call inputs, sampling, outputs, and continuation +- `InferencePreset`: model-owned named multi-stage defaults + +The canonical user experience should be: + +1. Choose a model. +2. Choose a pipeline preset. +3. Override a few typed fields. +4. Generate. + +FastVideo should not make a raw free-form string dict the primary API. Dicts and YAML/JSON should be supported as serialization/interchange layers, but they must be parsed immediately into typed config objects with strict unknown-key validation. + +The repo should also shift model-specific preset/default definitions closer to their pipeline implementations, while keeping the shared public schema and parsers centralized. + +## Why This Refactor Is Needed + +Today the public inference boundary is too flat and too forgiving. + +- `VideoGenerator.from_pretrained(..., **kwargs)` mixes: + - engine/runtime settings + - pipeline init settings + - component overrides +- `VideoGenerator.generate_video(..., **kwargs)` mixes: + - prompt and inputs + - sampling parameters + - output settings + - model-specific workflow knobs +- unknown or drifting keys can be silently filtered or merely logged instead of failing fast +- model-specific multi-stage behavior is exposed through ad hoc top-level flags instead of a stable preset/stage abstraction + +This is already painful in LTX2/Dreamverse, and it will get worse as more multi-stage pipelines are upstreamed. + +## Design Goals + +- Keep the Python API typed and editor-friendly. +- Make YAML/JSON a first-class serialization of the same schema. +- Support CLI overrides cleanly without flattening the schema into hundreds of canonical flags. +- Separate init-time config from request-time config. +- Provide a stable public abstraction for multi-stage pipelines. +- Support LTX2 two-stage and continuation behavior cleanly. +- Keep the simple case simple. +- Co-locate model-owned defaults and stage topology with the relevant pipeline. +- Protect current public/server behavior with an explicit schema parity audit before freezing the new surface. +- Preserve backward compatibility long enough to migrate examples, internal users, and servers safely. + +## Non-Goals + +- Do not make Ray a structural dependency or copy its package layout. +- Do not make a raw free-form dict the primary Python API. +- Do not force all models into one universal `RefineConfig`. +- Do not expose stage indices as the primary user interface. +- Do not move every shared config class into per-model directories. + +## External Inspiration + +### Ray + +Borrow only the ergonomic idea that user-facing config can be expressed as a string-keyed dict or YAML/JSON config. Do not copy Ray's structure into FastVideo. + +### SGL Multimodal Gen + +Useful ideas: split instance config from request config; allow dict input at the boundary; parse dicts immediately into typed request objects; merge request overrides onto model defaults; validate request params against pipeline/task type. Do not copy: request objects depending on server/engine config; broad weakly typed request bags as the canonical API. + +### vLLM-Omni + +Useful ideas: model-owned pipeline presets; explicit stage topology; per-stage default sampling params; clean separation between stage topology, engine defaults, and runtime overrides. Do not copy: positional `sampling_params_list` as the primary public API; serving-engine-oriented stage index semantics in the main Python interface. + +## Core Decision + +FastVideo should have: + +1. A shared typed public schema. +2. Model-owned named pipeline presets. +3. Semantic stage overrides by stage name. +4. Optional advanced explicit plans for power users. +5. YAML-first config loading with dotted CLI overrides. + +The public API should be stable at the schema level, while model-specific behavior should be contained in preset definitions and model-specific typed override classes. + +## Schema Parity Requirement + +Before the new schema is declared canonical, FastVideo should build a parity inventory across all current public inference surfaces (Python `VideoGenerator` kwargs, CLI flags, YAML/JSON config inputs, OpenAI/server request models, model-specific sampling/runtime fields). Each field must be marked: kept as-is, renamed, moved to a nested path, preset-owned, private-only adapter field, or intentionally dropped. No field should disappear implicitly. + +For any public field that remains supported, there should be either a normalized-config equivalence test, or an explicit parser/translation test. Fields that exist only in private Dreamverse integration code should be handled by a private adapter layer, not quietly converted into public FastVideo compatibility guarantees. + +Landed artifact: [inference_schema_parity_inventory.yaml](docs/design/inference_schema_parity_inventory.yaml) + guard [test_schema_parity_inventory.py](fastvideo/tests/api/test_schema_parity_inventory.py). + +## Canonical Public Schema + +The typed schema is implemented in [fastvideo/api/schema.py](fastvideo/api/schema.py). Envelope types: + +- `RunConfig` — offline: `generator` (GeneratorConfig) + `request` (GenerationRequest) +- `ServeConfig` — serving: `generator` + `server` (ServerConfig) + `default_request` (GenerationRequest) + optional `streaming` (StreamingConfig) + +Key nested types (summary; full fields in `schema.py`): + +- `GeneratorConfig` → `model_path`, `revision`, `trust_remote_code`, `engine` (EngineConfig: parallelism/offload/compile/quantization/flags), `pipeline` (PipelineSelection: workload_type, preset, preset_version, components, preset_overrides, experimental) +- `GenerationRequest` → `prompt`, `negative_prompt`, `inputs` (InputConfig), `sampling` (SamplingConfig), `runtime` (RequestRuntimeConfig), `output` (OutputConfig), `stage_overrides`, `state` (ContinuationState), `plan` (GenerationPlan), `extensions` +- `ContinuationState` → opaque `{kind: str, payload: dict[str, Any]}` +- `GenerationPlan` → `{stages: list[PlannedStage], final_stage: str | None}`; advanced/escape-hatch only + +### Important Semantics + +- Dataclasses are canonical for Python users. +- Dict and YAML/JSON are parsed into these dataclasses immediately. +- Unknown keys must raise validation errors. +- Typed `GenerationRequest` defaults come from the public schema, not from model-specific `SamplingParam.from_pretrained(...)` defaults. +- Legacy `generate_video(...)` continues to inherit model-specific sampling defaults until the SSIM/performance migration lands (PR 11). +- The only open-ended escape hatches are: + - `generator.pipeline.experimental` + - `request.extensions` + +That keeps the public contract strict without blocking experimental work. + +### Request Mutation Tracking + +When a `GenerationRequest` is parsed from a raw dict (YAML, JSON, or Python mapping), FastVideo records which fields the user explicitly provided versus which received schema defaults. This matters because `request_to_sampling_param()` must distinguish user-provided values (which should override model defaults) from schema defaults (which should NOT override model defaults). + +The tracking contract: + +- At parse time, the original raw dict and a baseline snapshot of the parsed object are stored on the request. +- Dataclass field mutations after parsing (e.g., `request.sampling.seed = 7`) are captured via lightweight `__setattr__` dirty-path recording. +- Dict-typed field mutations (e.g., `del request.stage_overrides["refine"]`) are detected at access time by diffing the current dict against the baseline snapshot. +- Setting a field to the schema default value IS captured as explicit, so it will override model defaults. +- The raw dict is reconciled lazily when `normalize_generation_request()` is called, not on every individual mutation. + +### Schema Purity and Model-Specific Fields + +The shared schema currently contains fields that are specific to one or two model families. These remain for backward compatibility during the initial migration (PRs 0-3) but should migrate to preset-owned typed override classes as the preset system lands (PRs 4-10). + +**SamplingConfig fields to migrate:** + +- `height_sr`, `width_sr`, `num_inference_steps_sr`: Hunyuan15 SR only. Target: `HunyuanSRStageOverride` in PR 10. +- `guidance_scale_2`, `boundary_ratio`: Wan2.2 and LingBotWorld only. Target: preset-owned overrides in the relevant model migration PR. + +**InputConfig fields to migrate:** + +- `mouse_cond`, `keyboard_cond`, `grid_sizes`: MatrixGame action control only. Target: `request.extensions` or a typed MatrixGame input config. +- `c2ws_plucker_emb`: LingBotWorld camera control only. Target: `request.extensions` or a typed LingBotWorld input config. +- `refine_from`, `stage1_video`: LongCat refinement only. Target: `LongCatRefineStageOverride` inputs or keep in `InputConfig` if they remain a public contract. + +**Universal fields that stay in the shared schema:** + +- `guidance_rescale`: used by multiple denoising stages across models, default 0.0. Universally applicable. +- `true_cfg_scale`: OpenAI adapter surface. Keep for protocol compatibility. + +### Escape Hatch Sunset + +`generator.pipeline.experimental` and `request.extensions` are intentional escape hatches for experimental and private work. They bypass strict validation by design. + +Rules for escape hatch usage: + +- New fields should not be added to `experimental` or `extensions` without a plan to either promote them to typed fields or remove them within two PR cycles. +- Each model migration PR (PRs 6-10) should review and shrink escape hatch usage for that model family. +- The compatibility layer currently routes unrecognized legacy kwargs into `experimental`. This pass-through should shrink as presets absorb model-specific fields. + +## Public Python API + +### New Canonical API + +```python +from fastvideo import VideoGenerator +from fastvideo.api import ( + GeneratorConfig, GenerationRequest, + EngineConfig, OutputConfig, + PipelineSelection, SamplingConfig, +) + +generator = VideoGenerator.from_pretrained( + config=GeneratorConfig( + model_path="/models/ltx2", + engine=EngineConfig(num_gpus=1), + pipeline=PipelineSelection( + workload_type="t2v", + preset="ltx2_two_stage", + ), + ) +) + +result = generator.generate( + GenerationRequest( + prompt="a fox running through snow", + sampling=SamplingConfig( + num_frames=121, height=1024, width=1536, + num_inference_steps=8, seed=42, + ), + output=OutputConfig(save_video=True, return_state=True), + ) +) +``` + +### Accepted Construction Forms + +Canonical: + +```python +VideoGenerator.from_pretrained(config=GeneratorConfig(...)) +VideoGenerator.from_config(GeneratorConfig(...)) +VideoGenerator.from_file("run.yaml") +``` + +Stable convenience constructor: + +```python +VideoGenerator.from_pretrained("model-id") +VideoGenerator.from_pretrained("model-id", num_gpus=2, use_fsdp_inference=False, ...) +``` + +Legacy compatibility: + +```python +VideoGenerator.from_pretrained(model_path, **legacy_kwargs) +``` + +All constructor forms normalize through the same typed path. Stable convenience kwargs remain supported with no deprecation warning. Advanced model/pipeline-specific kwargs are accepted during migration but only as compatibility inputs that normalize into `GeneratorConfig`. The thing being deprecated over time is the unbounded legacy kwarg surface, not the `from_pretrained(...)` entrypoint itself. + +### Generation Entry Point + +Canonical: `generator.generate(request: GenerationRequest) -> GenerationResult`. + +Compatibility alias: `generator.generate_video(prompt=..., **legacy_kwargs)` — converts legacy calls into a `GenerationRequest` and emits a deprecation warning. + +During the compat period, `generate(request=...)` uses schema defaults while `generate_video(...)` preserves legacy model-default behavior. These paths intentionally differ until preset-owned defaults replace the remaining `SamplingParam` default logic (migrated in PR 11). + +### Boundary Normalization Rule + +Every public inference entrypoint normalizes into typed config objects before touching legacy internals. That includes Python constructors, generation calls, CLI `generate`, CLI `serve`, and OpenAI/server request translation. Legacy internals (`FastVideoArgs`, `SamplingParam`) may remain temporarily, but only behind a typed normalization boundary. + +## Pipeline Presets + +### Definition + +An `InferencePreset` is a named model-owned preset that defines: + +- workload selection +- stage topology +- per-stage defaults +- stage names +- allowed stage override types +- init-time feature requirements + +The preset is not user-authored by default. It is supplied by the model integration. + +### Why Presets Are The Right Abstraction + +Users usually do not want to assemble a stage graph by hand. They want to say: + +- use LongCat distill + refine +- use Hunyuan 1080p SR +- use LTX2 two-stage continuation mode + +Presets provide a stable public noun for that behavior. + +### Preset Naming Rules + +- Use semantic names, not stage indices. +- Keep names stable across releases. +- If semantics change incompatibly, change `preset_version` or create a new preset name. + +Examples: `ltx2_base`, `ltx2_two_stage`, `longcat_distill_refine`, `hunyuan15_sr_720p`, `hunyuan15_sr_1080p`. + +### Preset-Owned Stage Names + +Stage names are public and stable within a preset. + +- LTX2: `base`, `refine` +- LongCat: `distill`, `refine` +- Hunyuan15: `base`, `sr_720p`, `sr_1080p` + +Public overrides should reference these stage names, never stage indices. + +## Stage Overrides + +The main user override surface for multi-stage pipelines is: + +```python +request.stage_overrides["refine"] = ... +``` + +Each model family should expose typed override classes for its stage names. Examples for the model families that land in PRs 6/9/10: + +```python +@dataclass +class LTX2RefineStageOverride: + enabled: bool | None = None + num_inference_steps: int | None = None + guidance_scale: float | None = None + add_noise: bool | None = None + image_crf: int | None = None + video_position_offset_sec: float | None = None + +@dataclass +class LongCatRefineStageOverride: + t_thresh: float | None = None + spatial_refine_only: bool | None = None + num_cond_frames: int | None = None + +@dataclass +class HunyuanSRStageOverride: + num_inference_steps: int | None = None + guidance_scale: float | None = None +``` + +### Strictness Rules + +- Stage names must exist in the selected preset. +- Override fields must be valid for that stage type. +- Unknown stage names and unknown fields must error. + +## Advanced Explicit Plans + +Presets should be the default API. `GenerationPlan` exists only for advanced composition or experimentation: + +- building a custom workflow that is not yet standardized as a preset +- debugging or benchmarking stage combinations +- prototyping a future preset + +Do not require `GenerationPlan` for normal users. + +## Continuation State + +Continuation must be a first-class part of the API. + +### Public Contract + +- `GenerationResult.state` may return a `ContinuationState`. +- `GenerationRequest.state` may accept a previously returned state. +- Most users should treat `state` as opaque and round-trip it back into the next request. + +### Why This Matters + +Dreamverse/LTX2 currently leaks continuation internals into app-level request fields like video conditions, audio clean latent, audio denoise mask, and segment offsets. Those should not remain top-level app-owned public API. + +### State Design + +Public surface: + +```python +@dataclass +class ContinuationState: + kind: str + payload: dict[str, Any] +``` + +Internally, FastVideo should also define typed model-specific state subclasses, e.g. `LTX2ContinuationState` (PR 7) and `LongCatIntermediateState` if ever needed. Minimal stable surface: return state, pass state back in, validate that the state is compatible with the active preset. + +Payload serialization: fields must be JSON-serializable or use an opaque blob-ID indirection for large tensors. This supports both the stateless OpenAI client round-trip AND future Dynamo prefill/decode disaggregation where prefill yields a state that decode hydrates across workers. + +## YAML / JSON Design + +YAML and JSON should be exact serializations of the typed schema, not a second unrelated config system. YAML is the primary documented format. JSON is accepted with the same schema. + +### Run Config Example + +```yaml +generator: + model_path: /models/ltx2 + engine: + num_gpus: 1 + parallelism: {tp_size: -1, sp_size: -1} + offload: {dit: false, text_encoder: false, vae: false, pin_cpu_memory: true} + pipeline: + workload_type: t2v + preset: ltx2_two_stage + components: + config_root: /models/ltx2-config + upsampler_weights: /models/ltx2-refine + lora_path: /models/ltx2-refine-lora + preset_overrides: + refine: {enabled: true, add_noise: true} + +request: + prompt: "a fox running through snow" + sampling: + num_frames: 121 + height: 1024 + width: 1536 + num_inference_steps: 8 + seed: 42 + output: {save_video: true, return_state: true} + stage_overrides: + refine: {num_inference_steps: 2, guidance_scale: 1.0} +``` + +### Serve Config Example + +```yaml +generator: + model_path: /models/ltx2 + engine: {num_gpus: 1} + pipeline: {workload_type: t2v, preset: ltx2_two_stage} + +server: {host: 0.0.0.0, port: 8000, output_dir: outputs/} + +default_request: + sampling: {num_frames: 121, height: 1024, width: 1536, num_inference_steps: 8} + output: {save_video: false, return_frames: false} +``` + +### Validation Rules + +- top-level schema must match `RunConfig` or `ServeConfig` +- unknown keys must fail +- dotted CLI overrides are applied to the nested config before typed parsing +- parse errors must include the exact nested path that failed + +## CLI Design + +Inference CLI reuses the best parts of the current training authoring flow (YAML-first authoring, dotted nested overrides, typed parsing after merge) but stays stricter than training at the public boundary because it is a user-facing API surface for Python, CLI, YAML/JSON, and serving. + +### Canonical CLI Forms + +```bash +fastvideo generate --config run.yaml +fastvideo generate --config run.yaml --request.sampling.seed 42 +fastvideo generate --config run.yaml --generator.engine.num_gpus 2 + +fastvideo serve --config serve.yaml +fastvideo serve --config serve.yaml --server.port 8090 +``` + +The CLI is config-only. Beyond `--config`, CLI input uses dotted override paths into the nested schema rather than maintaining a second flat flag surface. + +Implementation: YAML/JSON is loaded into a nested dict, dotted CLI overrides are applied to the nested dict, then the result is parsed into typed config objects. Flat CLI flags are rejected so the nested schema stays canonical. + +## OpenAI / Server Mapping + +`fastvideo serve` loads `ServeConfig`. Incoming HTTP requests are translated into `GenerationRequest` by: + +1. cloning `default_request` +2. applying API request fields onto that request +3. validating against the selected preset + +This is similar in spirit to the SGL pattern of merging user overrides onto model defaults. + +Rules: + +- HTTP request translation must not bypass typed validation. +- multi-stage defaults should come from the preset and `default_request`, not from ad hoc server-local logic. +- stateful continuation requests should accept and return typed `ContinuationState` payloads. + +Landed in PR 5 for the stateless OpenAI server at `fastvideo/entrypoints/openai/`. The streaming/session server (PRs 7.5-7.9) uses the same preset/default_request merge through `ServeConfig.streaming`. + +## Streaming Server + Dynamo Backend + +The typed public API is consumed by three server-class integrations. They must share one execution substrate so we don't grow three near-duplicate progress loops. + +### The three consumers + +| Consumer | Transport | Request shape | State | +|---|---|---|---| +| Stateless OpenAI (`fastvideo/entrypoints/openai/`) | HTTP POST | `GenerationRequest` merged onto `ServeConfig.default_request` | Stateless; continuation via opaque payload if needed | +| Streaming WebSocket (`fastvideo/entrypoints/streaming/`) | WebSocket JSON + binary fMP4 | `GenerationRequest` per segment, session-scoped | Server-held session (per-GPU continuation cache); snapshot on demand | +| Dynamo native backend (`ai-dynamo/dynamo/components/src/dynamo/fastvideo/`) | Dynamo RPC endpoint | `NvCreateVideoRequest` ↔ adapter ↔ `GenerationRequest` | Aggregated today; disaggregated prefill/decode later via `ContinuationState` | + +### Shared execution substrate: `VideoGenerator.generate_async` + +The OpenAI server, streaming server, and Dynamo backend all want the same thing: a typed async API that yields progress events and a typed final result. FastVideo exposes exactly one canonical entry point: + +```python +async def generate_async( + self, + request: GenerationRequest, +) -> AsyncGenerator[VideoEvent, None]: ... +``` + +Events: + +```python +@dataclass +class VideoProgressEvent: + step: int + total_steps: int + stage: str # "denoise" | "refine" | "decode" | ... + +@dataclass +class VideoPartialEvent: + frames: np.ndarray # shape: (num_frames, H, W, 3) + index: int # monotonic chunk index + +@dataclass +class VideoFinalEvent: + video_bytes: bytes | None # mp4-encoded if requested + tensor: torch.Tensor | None # raw if requested + metadata: dict[str, Any] + continuation_state: ContinuationState | None + +VideoEvent = VideoProgressEvent | VideoPartialEvent | VideoFinalEvent +``` + +The sync `generate_video(request=...) -> VideoResult` becomes a thin `asyncio.run` wrapper over `generate_async` that collects events and returns the final. + +### Streaming server mapping + +`fastvideo/entrypoints/streaming/` owns per-session state: + +- `SessionStore.hydrate(state: ContinuationState) -> session_id` +- `SessionStore.snapshot(session_id) -> ContinuationState` +- Per-GPU implicit continuation cache (today's internal behavior) is wrapped as a `SessionStore` implementation. + +Per-segment, the session writes a `GenerationRequest`, pipes the event stream to the WebSocket (progress → JSON messages, partial → fMP4 frames), and persists the final's `ContinuationState` into the session. + +### Dynamo backend mapping + +Dynamo's backend pattern (from `components/src/dynamo/sglang/`) is a pure Python import. FastVideo does not host a `fastvideo/entrypoints/dynamo/` subpackage; the integration lives in the Dynamo repo. FastVideo exposes a stable contract: + +| Surface | Exposed as | +|---|---| +| Construction | `VideoGenerator.from_pretrained(model_path, **typed_kwargs)` | +| Execution (async) | `VideoGenerator.generate_async(request) -> AsyncGenerator[VideoEvent, None]` | +| Execution (sync) | `VideoGenerator.generate_video(request=...) -> VideoResult` | +| Typed request | `fastvideo.api.GenerationRequest`, `SamplingConfig`, `InputConfig` | +| Typed result | `fastvideo.api.VideoResult`, `VideoEvent`, `ContinuationState` | +| Health-check input | `VideoGenerator.default_health_check_request() -> GenerationRequest` | +| Config dump | `config_to_dict(cfg)` (already exists) | + +Request/response mapping the Dynamo adapter must perform: + +``` +NvCreateVideoRequest -> fastvideo.api.GenerationRequest + prompt -> sampling.prompt + size="WxH" -> sampling.width, sampling.height + seconds -> seconds * nvext.fps -> sampling.num_frames + input_reference -> input.image_path | input.video_path + nvext.fps -> sampling.fps + nvext.num_frames -> sampling.num_frames (overrides seconds*fps) + nvext.num_inference_steps -> sampling.num_inference_steps + nvext.guidance_scale -> sampling.guidance_scale + nvext.seed -> sampling.seed + nvext.negative_prompt -> sampling.negative_prompt + response_format -> (handled at the adapter's output stage) + +VideoFinalEvent -> NvVideosResponse + video_bytes -> data[0].b64_json (if response_format=b64_json) + uploaded URL -> data[0].url (if response_format=url) + metadata.inference_time_s -> inference_time_s + continuation_state -> (reserved for future disaggregation) +``` + +All fields already exist (or will exist after PR 6's typed-kwarg expansion) on FastVideo's typed schema. **The adapter lives entirely in the Dynamo repo** at `components/src/dynamo/fastvideo/` — FastVideo does not host any Dynamo subpackage, dep, or CLI. The only FastVideo obligation is the stable public Python API listed above. + +### Constraints this places on other sections + +- **Continuation State** (see earlier section): `ContinuationState.payload` must be JSON-serializable or use an opaque blob-ID indirection for large tensors. This supports both the stateless OpenAI client round-trip *and* future Dynamo prefill/decode disaggregation, where prefill yields a state that decode hydrates across workers. +- **Typed GeneratorConfig** (see Public Python API): every flat legacy LTX2 kwarg currently used by the internal `gpu_pool.py` must have a typed home reachable from `GeneratorConfig`. Dynamo's `FastVideoArgGroup` builds the config from its CLI and must not have to know any legacy LTX2 name. +- **Public exports**: `from fastvideo import VideoGenerator`; `from fastvideo.api import GenerationRequest, SamplingConfig, ContinuationState, VideoResult, VideoEvent, VideoProgressEvent, VideoPartialEvent, VideoFinalEvent`. + +## Repo Layout + +### Shared Public API + +`fastvideo/api/` contains the shared public API package. Current files: + +- `schema.py` — `RunConfig`, `ServeConfig`, `ServerConfig`, `GeneratorConfig`, and all nested typed config dataclasses +- `sampling_param.py` — `SamplingParam` + `CacheParams` (canonical home since PR 4; former `configs/sample/base.py` location removed) +- `presets.py` — `InferencePreset`, `PresetStageSpec`, registry APIs +- `results.py` — `GenerationResult` / `VideoResult` +- `parser.py` — `from_dict`, `to_dict`, `load_yaml`, `load_json`, validation +- `overrides.py` — dotted override application +- `compat.py` — legacy Python kwargs translation +- `errors.py` — path-aware validation errors + +May split further by concern in a future cleanup. + +### Pipeline-Local Model-Owned Config + +Model-owned presets and override types live next to the model pipeline: + +```text +fastvideo/pipelines/basic/ltx2/ + ltx2_pipeline.py, presets.py, stage_overrides.py, continuation.py + +fastvideo/pipelines/basic/longcat/ + longcat_pipeline.py, presets.py, stage_overrides.py + +fastvideo/pipelines/basic/hunyuan15/ + hunyuan15_pipeline.py, hunyuan15_sr_pipeline.py, hunyuan15_2sr_pipeline.py, + presets.py, stage_overrides.py +``` + +PR 4 landed `presets.py` for all 13 model families. Remaining colocation targets are `pipeline_configs.py` (moving `configs/pipelines/.py`) and model-specific stages (moving `pipelines/stages/_*.py`); see [PR plan.md](PR%20plan.md) "Pipeline Package Structure". + +### Registry + +Central registry (`fastvideo/registry.py`) registers preset providers rather than owning all model-specific defaults directly. It answers: + +- which pipeline class corresponds to a model path +- which presets are available for that model family +- which override/state classes are valid for a selected preset + +## Relationship To Current Internal Classes + +This refactor does not require deleting current internals immediately. + +- `FastVideoArgs` is an internal compatibility/input adapter, no longer the primary public inference type. +- `SamplingParam` now lives in `fastvideo/api/sampling_param.py` and gets model-specific defaults from presets via `_from_preset()`. All 12 `SamplingParam` subclasses have been removed and the former `fastvideo/configs/sample/` directory has been deleted entirely (PR 4). It remains an internal adapter between the preset system and the runtime. +- current `PipelineConfig` classes can remain temporarily as internal component config carriers +- the new public schema is the stable boundary above them + +`VideoGenerator` accepts the new schema and translates down into current execution internals. Legacy `generate_video(..., **kwargs)` stays on the direct execution path during the compat period until SSIM/performance tests migrate in PR 11. + +## Model-Specific Design + +### LTX2 / Dreamverse + +LTX2 needs both: + +- init-time two-stage feature wiring +- request-time continuation/refine behavior + +Expressed as: + +- preset: `ltx2_two_stage` +- init-time fields: refine assets, optional config root, stage enablement +- request-time fields: stage override for refine behavior, optional returned continuation state + +#### LTX2 Preset Example + +```yaml +generator: + pipeline: + preset: ltx2_two_stage + components: + config_root: /models/ltx2-config + upsampler_weights: /models/ltx2-refine + lora_path: /models/ltx2-refine-lora + preset_overrides: + refine: {enabled: true, add_noise: true} +``` + +#### LTX2 Request Example + +```yaml +request: + prompt: "continue the previous sequence" + state: ${previous_result.state} + stage_overrides: + refine: + num_inference_steps: 2 + guidance_scale: 1.0 + image_crf: 18 + output: + return_state: true +``` + +#### LTX2 Explicit Decisions + +- `config_model_path` becomes `generator.pipeline.components.config_root` +- `ltx2_refine_*` stops being a pile of top-level kwargs +- continuation internals move into `ContinuationState` +- app-level code should pass `state`, not raw latent/audio condition payloads + +### LongCat + +LongCat should expose a named preset like `longcat_distill_refine` with stage topology `distill` and `refine`. + +User-facing override knobs remain model-specific (`t_thresh`, `spatial_refine_only`, `num_cond_frames`) but live under: + +```yaml +request: + stage_overrides: + refine: + t_thresh: 0.5 + spatial_refine_only: false + num_cond_frames: 8 +``` + +### Hunyuan 1.5 SR + +Hunyuan already behaves like an integrated multi-stage pipeline. Expose it via presets: `hunyuan15_sr_720p`, `hunyuan15_sr_1080p`. Users should not need to know the exact internal pipeline class split between base and SR stages. Per-stage override surface should stay small and mostly sampling-focused. + +Hunyuan15 presets (`hunyuan15_t2v_480p`, `hunyuan15_i2v_480p_distilled`, `hunyuan15_t2v_720p`, `hunyuan15_i2v_720p_distilled`, `hunyuan15_sr_1080p`) are implemented (PR 4). The `Hunyuan15_*_SamplingParam` subclasses have been removed; defaults (including precomputed sigmas) come from preset `defaults` dicts. Remaining work: adding typed `HunyuanSRStageOverride` classes and colocating PipelineConfig (PR 10). + +## Exact Compatibility Mapping + +Intended translation layer for common current fields. + +| Legacy Field | New Path | +| --- | --- | +| `model_path` | `generator.model_path` | +| `revision` | `generator.revision` | +| `trust_remote_code` | `generator.trust_remote_code` | +| `workload_type` | `generator.pipeline.workload_type` | +| `num_gpus` | `generator.engine.num_gpus` | +| `tp_size` | `generator.engine.parallelism.tp_size` | +| `sp_size` | `generator.engine.parallelism.sp_size` | +| `dit_cpu_offload` | `generator.engine.offload.dit` | +| `dit_layerwise_offload` | `generator.engine.offload.dit_layerwise` | +| `text_encoder_cpu_offload` | `generator.engine.offload.text_encoder` | +| `image_encoder_cpu_offload` | `generator.engine.offload.image_encoder` | +| `vae_cpu_offload` | `generator.engine.offload.vae` | +| `pin_cpu_memory` | `generator.engine.offload.pin_cpu_memory` | +| `enable_torch_compile` | `generator.engine.compile.enabled` | +| `torch_compile_kwargs` | split across `generator.engine.compile.backend`, `.fullgraph`, `.mode`, `.dynamic`; uncommon keys land in `.extras` | +| `enable_torch_compile_text_encoder` | `generator.engine.compile.text_encoder_enabled` | +| `enable_stage_verification` | `generator.engine.enable_stage_verification` | +| `prompt_txt` | `request.inputs.prompt_path` | +| `prompt` | `request.prompt` | +| `negative_prompt` | `request.negative_prompt` | +| `image_path` | `request.inputs.image_path` | +| `video_path` | `request.inputs.video_path` | +| `output_path` | `request.output.output_path` | +| `output_video_name` | `request.output.output_video_name` | +| `save_video` | `request.output.save_video` | +| `return_frames` | `request.output.return_frames` | +| `num_videos_per_prompt` | `request.sampling.num_videos_per_prompt` | +| `seed` | `request.sampling.seed` | +| `num_frames` | `request.sampling.num_frames` | +| `height` | `request.sampling.height` | +| `width` | `request.sampling.width` | +| `fps` | `request.sampling.fps` | +| `num_inference_steps` | `request.sampling.num_inference_steps` | +| `guidance_scale` | `request.sampling.guidance_scale` | +| `guidance_scale_2` | `request.sampling.guidance_scale_2` | +| `guidance_rescale` | `request.sampling.guidance_rescale` | +| `true_cfg_scale` | `request.sampling.true_cfg_scale` | +| `boundary_ratio` | `request.sampling.boundary_ratio` | +| `sigmas` | `request.sampling.sigmas` | +| `enable_teacache` | `request.runtime.enable_teacache` | +| `return_trajectory_latents` | `request.runtime.return_trajectory_latents` | +| `return_trajectory_decoded` | `request.runtime.return_trajectory_decoded` | + +### Private Dreamverse Adapter Mapping + +The mappings below are useful for private Dreamverse migration, but they should not be treated as a public FastVideo backward-compatibility promise unless and until those fields actually exist in the public repo surfaces. + +| Private Adapter Field | New Path | +| --- | --- | +| `config_model_path` | `generator.pipeline.components.config_root` | +| `ltx2_refine_enabled` | `generator.pipeline.preset_overrides.refine.enabled` | +| `ltx2_refine_upsampler_path` | `generator.pipeline.components.upsampler_weights` | +| `ltx2_refine_lora_path` | `generator.pipeline.components.lora_path` | +| `ltx2_refine_num_inference_steps` | `request.stage_overrides.refine.num_inference_steps` | +| `ltx2_refine_guidance_scale` | `request.stage_overrides.refine.guidance_scale` | +| `ltx2_refine_add_noise` | `generator.pipeline.preset_overrides.refine.add_noise` | +| `ltx2_image_crf` | `request.stage_overrides.refine.image_crf` | +| `return_continuation_state` | `request.output.return_state` | + +### LongCat Legacy Mapping + +| Legacy Field | New Path | +| --- | --- | +| `refine_from` | `request.inputs.refine_from` | +| `stage1_video` | `request.inputs.stage1_video` | +| `t_thresh` | `request.stage_overrides.refine.t_thresh` | +| `spatial_refine_only` | `request.stage_overrides.refine.spatial_refine_only` | +| `num_cond_frames` | `request.stage_overrides.refine.num_cond_frames` | + +## Validation and Error Handling + +### Strict by Default + +All structured inputs should be strict by default: unknown keys error, wrong types error, invalid stage names error, incompatible state/preset combinations error. + +### Exceptions + +The only intentionally open-ended fields are `generator.pipeline.experimental` and `request.extensions`. These must be clearly documented as unstable and unsupported for long-term API compatibility. + +### Error Quality + +Validation errors should include the full nested path, expected type or valid choices, and preset/stage context when relevant: + +```text +Invalid field: request.stage_overrides.refine.num_inference_steps +Expected int, got "two" +Preset: ltx2_two_stage +Stage: refine +``` + +## Implementation Plan + +### Phases 0-5: Landed + +- Phase 0 — Schema Parity Inventory: inventory complete; field classifications live in `docs/design/inference_schema_parity_inventory.yaml`; parity test guard in `fastvideo/tests/api/test_schema_parity_inventory.py`. +- Phase 1 — Shared Schema: `fastvideo/api/` with typed dataclasses, parser, validation, dotted overrides, `RunConfig`/`ServeConfig`. +- Phase 2 — VideoGenerator Compat: `from_config`, `from_file`, `generate(request=...)`, legacy `from_pretrained(..., **kwargs)` and `generate_video(..., **kwargs)` as compat shims routed through typed normalization. +- Phase 3 — CLI Refactor: `fastvideo generate` and `fastvideo serve` parse nested YAML/JSON with training-style dotted overrides; flat flag expansion removed as the canonical path. +- Phase 4 — Preset System: shared registry + pipeline-local `presets.py` for all 13 families; all 12 `SamplingParam` subclasses removed; `SamplingParam` moved to `fastvideo/api/sampling_param.py`. +- Phase 5 — Server Request Translation: `fastvideo serve` loads `ServeConfig`; stateless OpenAI endpoint clones `default_request` and merges validated user overrides. + +### Remaining Phases + +- **Phase 6 — LTX2 Public Upstream Path** (PR 6): upstream `ltx2_two_stage` preset; upstream continuation-state contract; upstream only repo-visible/public LTX2 surfaces into FastVideo. +- **Phase 7 — Dreamverse Adapter Migration** (PR 7 + private repo work): translate private Dreamverse-only request/config fields in a private adapter; replace raw app-owned continuation kwargs with `state` in the private server; do not expand the public FastVideo compatibility promise just to match private adapter fields. +- **Phase 7.5-7.10 — Streaming Server and Dynamo Contract** (PRs 7.5-7.10): upstream the streaming server (skeleton, GPU pool, prompt enhancer, auxiliaries, router) consuming `generate_async`; land the Dynamo backend contract (`VideoGenerator.generate_async`, health-check helper) with the Dynamo backend package itself living in the Dynamo repo. +- **Phase 8 — Model Migration and Docs** (PRs 9-10, 12): colocate `configs/pipelines/.py` with pipeline implementations; add typed stage override classes for multi-stage models; update basic examples to the new API; document YAML-first inference config and migration guidance. +- **Phase 8.5 — Golden-Test Migration** (PR 11): keep SSIM/performance regression tests on legacy Python generation while preset defaults are still settling; one dedicated migration pass after the preset system and model-default behavior are stable; complete this migration before removing legacy Python inference entrypoints or kwargs. +- **Phase 9 — Deprecation and Cleanup** (PR 13): deprecate direct public use of `FastVideoArgs`; deprecate direct public use of `SamplingParam`; gradually reduce public documentation for flat flags; eventually remove legacy kwargs after downstream migration is complete. + +## Final Recommendation + +The public FastVideo inference API is being rebuilt around: + +- typed nested configs +- model-owned named presets +- semantic stage overrides +- first-class continuation state +- YAML-first CLI with dotted overrides + +The primary abstraction is `InferencePreset`, not raw kwargs and not a fully manual stage graph. + +The repo is moving model-specific defaults closer to each pipeline, while keeping the public schema and parsing logic centralized. + +Regression and quality tests follow the rollout. Unit/entrypoint tests migrated to the typed API early, but SSIM/performance suites only move once the typed path can express all current knobs without compatibility exceptions and produces stable defaults through presets (PR 11). + +End state: + +- stable Python typing +- clean YAML/JSON support +- a much better CLI story +- a sane path for Dreamverse/LTX2 +- a unified abstraction for LongCat, Hunyuan, and future multi-stage models diff --git a/.agents/memory/dreamverse-integration/source-archive/dreamverse_integration.md b/.agents/memory/dreamverse-integration/source-archive/dreamverse_integration.md new file mode 100644 index 0000000000..1e137e277e --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/dreamverse_integration.md @@ -0,0 +1,285 @@ +# Dreamverse ↔ FastVideo Integration + +## Status + +Working integration record. Captures how Dreamverse consumes the +FastVideo public API today, what's already shared, what's still ad +hoc, and what migrations land alongside each PR in the API refactor +sequence. + +Pinned versions (last reconciled this session): + +| Repo | Branch | Commit | Note | +|---|---|---|---| +| FastVideo (public) | `origin/main` | `70ee5d23` | PR 6 merged | +| FastVideo (public) | `will/api_7` | `3de5f833` | PR 7 in flight (typed continuation state) | +| FastVideo-internal | `will/rebase-nbv` | `1adc513e` | pre-PR-1 on the API refactor; has live realtime runtime | +| Dreamverse | `master` | `dc500330` | uses local + remote FastVideo runtimes via `server/runtime/` | + +## Related Documents + +- [PR plan.md](../../PR%20plan.md) — PR-by-PR sequence for the API refactor +- [apirefactor.md](../../apirefactor.md) — design spec +- [streaming-server-upstream-plan.md](streaming-server-upstream-plan.md) — upstream plan for `ui/ltx2-streaming/server/` +- `../../../Dreamverse/server/video_generation.py` — Dreamverse's worker + local `ContinuationState` +- `../../../Dreamverse/server/runtime/{factory,backend,gpu_pool,interfaces}.py` — runtime abstraction +- `../../../FastVideo-internal/fastvideo/entrypoints/realtime/{api_server,local_runtime}.py` — internal's realtime runtime (PR 7.5/7.6 upstream source) + +## Surface Area + +Dreamverse depends on FastVideo across three surfaces. Listed in order +of how stable each is. + +### 1. Pipeline construction (stable) + +`Dreamverse/server/video_generation.py:VideoGenerationWorker` calls +`VideoGenerator.from_pretrained(...)` with flat LTX-2 kwargs today. +After PR 6 the typed `GeneratorConfig` path exists; Dreamverse can +migrate at its own pace. + +| Dreamverse usage | FastVideo public surface (post-PR 6) | +|---|---| +| `VideoGenerator.from_pretrained(model_path, ltx2_refine_enabled=…, …)` | `VideoGenerator.from_pretrained(config=GeneratorConfig(...))` | +| Flat `torch_compile_kwargs={…}` dict | `engine.compile.{backend,fullgraph,mode,dynamic,extras}` | +| `ltx2_vae_tiling=True` | `pipeline.vae_tiling=True` | +| `ltx2_refine_*` family | `pipeline.preset_overrides.refine.*` + `pipeline.components.upsampler_weights` | +| `enable_torch_compile_text_encoder` | `engine.compile.text_encoder_enabled` | + +The legacy flat-kwarg path stays supported via `compat.py`; migration +is opt-in. PR 13's deprecation warnings are the eventual nudge. + +### 2. Realtime runtime (in flight: PRs 7.5–7.6) + +`Dreamverse/server/runtime/factory.py` selects a runtime backend at +process start: + +```python +def create_runtime_pool() -> RuntimePool: + if os.getenv("FASTVIDEO_REALTIME_BASE_URL"): + return FastVideoRealtimePool(base_url=..., ws_url=..., default_model_id=...) + return GPUPool(get_available_gpus()) # in-process, wraps fastvideo.entrypoints.realtime.local_runtime +``` + +Both backends speak the same `RuntimePool` / `RuntimeSlot` Protocol +(`server/runtime/interfaces.py`): + +- `acquire(client_id, websocket=None) -> (gpu_id, RuntimeSlot)` +- `release(client_id)` +- `RuntimeSlot.{join_user, user_step, leave_user, register_stream_queue, …}` + +Today both impls reach into FastVideo-internal's +`fastvideo.entrypoints.realtime.local_runtime` (which exposes +`RealtimeRuntimeConfig`, `GPUPool`, `GPUSlot`). The remote backend +talks HTTP+WS to a separately-deployed runtime of the same shape. + +**Contract that PR 7.5/7.6 must preserve:** + +- `RealtimeRuntimeConfig` accepts `model_registry`, `default_model_id`, + `default_height/width/num_frames/fps/num_inference_steps/guidance_scale/seed/negative_prompt`, + `default_ltx2_image_crf`, `startup_warmup_{enabled,prompt,timeout_seconds}`. +- `GPUPool(gpu_ids: list[int], config: RealtimeRuntimeConfig)` constructor. +- `pool.initialize() / shutdown() / acquire() / release() / get_status()`. +- HTTP endpoints on the remote variant: `GET /healthz`, `GET /readyz`, + `GET /status`, `WS /ws`. (These already match what + `Dreamverse/server/routes/health.py` consumes.) + +When PR 7.6 lands the upstream of `fastvideo/entrypoints/realtime/`, +Dreamverse should not need any code change unless we rename the import +path. **Open: do we rename `realtime/` → `streaming/` to match the +public package introduced in PR 5.5?** A deprecation alias module +keeps both working during transition. + +### 3. Continuation state (PR 7) + +`Dreamverse/server/video_generation.py:89 ContinuationState` is +Dreamverse's hand-rolled per-session state holder. PR 7 introduces +the typed equivalent at `fastvideo/pipelines/basic/ltx2/continuation.py`. + +#### Field mapping + +| Dreamverse | PR 7 `LTX2ContinuationState` | Notes | +|---|---|---| +| `video_images: list[PIL.Image]` | `video_frames: list[np.ndarray]` (uint8 H×W×3) | numpy is leaner; Dreamverse already round-trips PIL→numpy→PIL just to add noise | +| `audio_latents: torch.Tensor` `[B, C, T, mel]` | `audio_latents: torch.Tensor` (safetensors-serialized; bf16-safe) | unchanged shape; safetensors preserves dtype incl. `bfloat16` | +| `LTX2_VIDEO_CONDITIONING_FRAME_IDX` (env) | `video_conditioning_frame_idx: int` | env constant → per-state field | +| `LTX2_VIDEO_CONDITIONING_STRENGTH` (env) | `video_conditioning_strength: float` | env constant → per-state field | +| `AUDIO_CONDITIONING_NUM_FRAMES` (env) | `audio_conditioning_num_frames: int` | env constant → per-state field | +| `AUDIO_CONDITIONING_STRENGTH` (env) | `audio_conditioning_strength: float` | env constant → per-state field | +| `audio_lps` (passed into `apply_audio`) | `audio_sample_rate: int \| None` | analogous; rename worth confirming with audio team | +| Computed `prefix_sec` per segment | `video_position_offset_sec: float` | **see open question below** | +| `segment_idx` (param to apply_*) | `segment_index: int` | per-state field | +| `VIDEO_CONTEXT_NOISE`, `AUDIO_CONTEXT_NOISE`, `ENABLE_AUDIO_COND` | not on state | runtime policy / regularization knobs, not portable session data | +| `apply_video / apply_audio / save_video / save_audio_latents / clear` | not on PR-7 state class | state is a pure data carrier; runtime owns lifecycle policy | + +PR-7 is a strict superset of Dreamverse's data model **plus** lifts +several env globals into per-session typed fields. + +#### Lifecycle mapping + +| Dreamverse pattern | `SessionStore` API | +|---|---| +| `self.continuation = ContinuationState()` per session | `state = session_store.snapshot(sid) or LTX2ContinuationState()` | +| `apply_video(req_kwargs, segment_idx)` + `apply_audio(req_kwargs, segment_idx, audio_lps)` | `state = session_store.snapshot(sid)`; runtime builds request from `state.video_frames` / `state.audio_latents` etc. | +| `save_video(frames)` + `save_audio_latents(latents)` | runtime constructs new `LTX2ContinuationState`, then `session_store.store(sid, new_state.to_continuation_state())` | +| `clear()` at end of session | `session_store.drop(sid)` | + +`SessionStore` and `BlobStore` ABCs ship with thread-safe in-memory +defaults (`InMemorySessionStore`, `InMemoryBlobStore`). Dreamverse can +adopt them as-is for the local runtime; remote runtimes can plug in +redis-backed implementations later. + +#### Wire format (HTTP/WS round-trip) + +Dreamverse's `FastVideoRealtimePool` already speaks the realtime +runtime's HTTP+WS protocol. When PR 7.5/7.6 land state emission on +the server side, the on-the-wire payload is the public envelope: + +```json +{ + "kind": "ltx2.v1", + "payload": { + "schema_version": 1, + "segment_index": 3, + "video_conditioning_frame_idx": 9, + "video_conditioning_strength": 0.75, + "audio_sample_rate": 24000, + "audio_conditioning_num_frames": 5, + "audio_conditioning_strength": 0.5, + "video_position_offset_sec": 0.2, + "video": {"frames_b64": ["..."]}, + "audio": {"safetensors_b64": "..."}, + "metadata": {} + } +} +``` + +JSON-serializable end-to-end; safetensors blob preserves audio dtype +(incl. bf16). For payloads above the inline threshold a `BlobStore` +indirection replaces the b64-encoded body with `{"blob_id": "..."}`; +the blob itself stays inside the runtime that produced it. + +## Migration Plan + +Per PR landed, Dreamverse adoption is opt-in. + +### After PR 7 merges + +Single-file change in Dreamverse, ~50-line PR: + +1. Replace `server/video_generation.py:89 ContinuationState` import + with `from fastvideo.pipelines.basic.ltx2.continuation import LTX2ContinuationState`. +2. Move `apply_video`, `apply_audio`, `save_video`, `save_audio_latents`, + `clear` off the state class onto `VideoGenerationWorker` (these are + runtime policy that uses the state, not part of the state itself). +3. Update `apply_audio` to read knobs from `state.audio_conditioning_num_frames` + and `state.audio_conditioning_strength` instead of the env globals + `AUDIO_CONDITIONING_NUM_FRAMES` / `AUDIO_CONDITIONING_STRENGTH`. The + env globals can stay as defaults that populate the state when a new + session starts. +4. Same treatment for video knobs: `state.video_conditioning_frame_idx`, + `state.video_conditioning_strength`. +5. Frame storage swaps `list[PIL.Image]` for `list[np.ndarray]` — + simpler `save_video` (no PIL conversion) and simpler `clear` (no + `.close()` loop). + +### After PR 7.5 lands streaming server skeleton + +Dreamverse's runtime/factory.py either: + +- Continues to construct `GPUPool` from `RealtimeRuntimeConfig` (the + current path), now backed by the upstreamed `fastvideo/entrypoints/realtime/`. +- Or migrates to the upstream's `ServeConfig.streaming` shape and + invokes `fastvideo serve --config realtime.yaml` as the launch path. + +Either way, `Dreamverse/server/runtime/interfaces.py` `RuntimePool` / +`RuntimeSlot` Protocol can stay in place — it was modeled after the +realtime runtime's surface. No interface change needed. + +### After PR 7.6 lands the GPU pool upstream + +- The `local_runtime.py` import in + `Dreamverse/server/runtime/gpu_pool.py:24` becomes a public import + with the same symbols (`RealtimeRuntimeConfig`, `GPUPool`, + `get_available_gpus`). +- Per-GPU continuation state inside the worker (`ltx2_continuation_images`, + `ltx2_continuation_audio_latents`) gets replaced by a `SessionStore` + reference. Dreamverse doesn't see this change — it's runtime-internal. +- `request.state` / `result.state` round-trip starts working end-to-end + on the local runtime. Dreamverse's worker can begin reading + `result.state` and feeding `request.state` between segments. + +### After PR 7.10 lands the Dynamo backend contract + +- `VideoGenerator.generate_async(...) -> AsyncGenerator[VideoEvent, None]` + is the canonical API. +- Dreamverse's per-segment `user_step` flow can migrate from the legacy + sync `generate_video(..., **kwargs)` path to consuming the typed + event stream. Optional; the sync wrapper stays. + +## Open Questions + +### `video_position_offset_sec` semantics + +Dreamverse computes `prefix_sec = float(audio_extra) / 24.0` per +segment in `apply_audio`. Not persisted on `ContinuationState`. + +PR-7 has `video_position_offset_sec` as a **state field**. Two valid +interpretations: + +(a) **Persistent across segments** — accumulating time offset for +long sessions; useful for time-coherent audio chaining. +(b) **Per-segment hint that rides on the carrier** — runtime +overwrites every time; field is harmless redundancy. + +Field's docstring leans toward (b). Decide before PR 7.6 starts +emitting/consuming it. If we land on (a), document the accumulation +rule explicitly. + +### `BlobStore` / `SessionStore` lifecycle ownership + +PR 7's in-memory implementations have no eviction, no TTL, no +automatic blob cleanup on state replacement. Documented as a +per-deployment policy decision. + +When PR 7.5/7.6 land the live consumer, who owns: + +- bounded session capacity (LRU? TTL? hard max?) +- blob `drop()` chained when a state is replaced +- session expiry on websocket disconnect + +Probably the streaming server's session manager, but worth stating +explicitly in PR 7.5's design. + +### `realtime/` vs `streaming/` package naming + +Currently: + +- Public PR 5.5 introduced `fastvideo/entrypoints/streaming/` (skeleton + typed config). +- Internal has `fastvideo/entrypoints/realtime/` (live runtime). +- Dreamverse imports from `fastvideo.entrypoints.realtime` (per the internal name). + +PR 7.5 either picks one or ships a deprecation alias module. +Recommendation in `streaming-server-upstream-plan.md`: keep +`streaming/` (it's the post-PR-5.5 public name), provide +`realtime/__init__.py` as a re-export with a `DeprecationWarning` for +one release cycle so internal/Dreamverse can land import updates. + +## Test Coverage on the FastVideo Side + +PR 7 ships: + +- `fastvideo/tests/api/test_ltx2_continuation.py` — typed + state round-trip (inline + blob), bf16 preservation, JSON + serializability, kind/version validation, schema_version guard. +- `fastvideo/tests/entrypoints/streaming/test_session_store.py` — + store/snapshot/hydrate/drop behavior on `InMemorySessionStore`; + put/get/drop on `InMemoryBlobStore`; thread-safety of both. + +PR 7.5+ should add a contract test that exercises the round-trip via +the same wire format Dreamverse's `FastVideoRealtimePool` consumes. + +## Changelog + +| Date | Change | +|------|--------| +| 2026-04-23 | Initial draft. Captures PR 6 / PR 7 mapping; open questions on `video_position_offset_sec`, lifecycle ownership, and `realtime/` vs `streaming/` naming. | diff --git a/.agents/memory/dreamverse-integration/source-archive/dreamverse_review.md b/.agents/memory/dreamverse-integration/source-archive/dreamverse_review.md new file mode 100644 index 0000000000..976cff0361 --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/dreamverse_review.md @@ -0,0 +1,390 @@ +# Dreamverse Integration Review Log + +This document tracks design decisions, open questions, and integration-time +choices made while landing the public-side stacked PRs (7.7 → 8) and switching +Dreamverse from `FastVideo-internal` to public `FastVideo`. The user will +review this carefully — entries are deliberately verbose about *why*. + +## Goal + +Replace Dreamverse's dependency on `FastVideo-internal` with the public +`FastVideo` package, using the upstreamed streaming server stack +(`fastvideo.entrypoints.streaming.*`) where Dreamverse currently has local +copies or imports private modules. + +## Surfaces Dreamverse currently uses from FastVideo-internal + +(from `/home/william5lin/Dreamverse/server/`, scanned 2026-04-26): + +| Dreamverse import | Internal path | Public replacement | +|---|---|---| +| `fastvideo.entrypoints.realtime.local_runtime.RealtimeRuntimeConfig` | `FastVideo-internal/fastvideo/entrypoints/realtime/local_runtime.py` | (none) — Dreamverse rewires through `streaming.gpu_pool.SubprocessGpuPool` | +| `fastvideo.entrypoints.realtime.local_runtime.GPUPool` | same as above | `fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool` (PR 7.6) | +| `fastvideo.configs.pipelines.base.PipelineConfig` | already in public | unchanged | +| `fastvideo.entrypoints.video_generator.VideoGenerator` | already in public | unchanged | +| `fastvideo.layers.quantization.fp4_config.FP4Config` | already in public | unchanged | +| `fastvideo.utils.maybe_download_model` | already in public | unchanged | +| `fastvideo.models.audio.ltx2_audio_processing.AudioProcessor` | already in public | unchanged | +| `fastvideo.models.loader.component_loader.ComponentLoader` | already in public | unchanged | +| `fastvideo.models.dits.ltx2.*` | already in public | unchanged | +| local copy: `Dreamverse/server/prompt_enhancer.py` (1933 lines) | mirrors `FastVideo-internal/.../prompt_enhancer.py` | `fastvideo.entrypoints.streaming.prompt.*` (PR 7.7) | +| local copy: `Dreamverse/server/prompt_safety.py` | mirrors `FastVideo-internal/.../prompt_safety.py` | `fastvideo.entrypoints.streaming.prompt.safety` (PR 7.8) | +| local copy: `Dreamverse/server/session_logger.py` | mirrors `FastVideo-internal/.../session_logger.py` | `fastvideo.entrypoints.streaming.session_logger` (PR 7.8) | +| local copy: `Dreamverse/server/rewrite_prompt_payload.py` | mirrors `FastVideo-internal/.../rewrite_prompt_payload.py` | `fastvideo.entrypoints.streaming.prompt.rewrite` (PR 7.8) | +| local copy: `Dreamverse/server/mock_server.py` (1200 lines) | mirrors `FastVideo-internal/.../mock_server.py` | `fastvideo.entrypoints.streaming.mock_server` (PR 7.8) | +| local copy: `Dreamverse/server/session_init_image.py` | mirrors `FastVideo-internal/.../session_init_image.py` | `fastvideo.entrypoints.streaming.session_init_image` (PR 7.5 — already public) | + +## Design decisions made (auto-resolved) + +### D-1: Realtime runtime → streaming GpuPool migration shape + +**Context.** Dreamverse's `server/runtime/gpu_pool.py` thin-wraps +`fastvideo.entrypoints.realtime.local_runtime.GPUPool`, which takes a +`RealtimeRuntimeConfig(model_registry=…, default_model_id=…, default_height=…, +default_width=…, default_num_frames=…, default_num_inference_steps=…, +startup_warmup_*…)`. The public `streaming.gpu_pool.SubprocessGpuPool` takes a +typed `GeneratorConfig` + `GpuPoolConfig` + `WarmupConfig`. + +The shapes differ in two important ways: + +1. The internal version had a multi-model registry (`model_id → model_config` + dict). The public version is single-model (one `GeneratorConfig`). +2. The internal version flattened a few sampling defaults (height/width/frames/ + steps) into the runtime config. The public version expects them as part of + the per-request `SamplingConfig`. + +**Decision.** Dreamverse will: +1. Drop the multi-model registry on the integration branch (it is not used in + production today — Dreamverse boots one model per replica). +2. Construct a `GeneratorConfig` for the chosen model from `MODEL_REGISTRY[id]` + and pass it to `SubprocessGpuPool`. +3. Move the `default_height` / `default_width` / `default_num_frames` / + `default_num_inference_steps` defaults into a server-side + `default_request: GenerationRequest` template the session controller fills + from per-request input. + +**Why.** Multi-model is feasible to add back later (one pool per model id, +acquire by `(session_id, model_id)`), but not on the migration branch — that +would couple the upstream switch to a feature redesign. Punting keeps the +upstream switch a pure mechanical refactor. + +**Risk.** If a Dreamverse code path silently relied on the registry to swap +models per-session, the migration branch will surface that as a missing-model +error. The integration tests must exercise at least one segment per supported +model id before merging the Dreamverse branch. + +### D-2: PR 7.7 prompt enhancer API surface narrower than the internal one + +**Context.** The upstreamed `PromptEnhancer.enhance/auto_extend/rewrite` returns +`LLMResponse(content, provider, model, latency_ms, fallback_used)`. The internal +`enhance_prompt` / `generate_auto_prompt` / `rewrite_prompt_sequence` returns +`EnhanceResult(prompt, fallback_used, error, provider, model, latency_ms)` / +`RewriteResult(prompts, …, rollout_id, rollout_label, raw_response_text)`. + +**Decision.** The Dreamverse integration branch will adapt at the call site: +- `enhancer.enhance_prompt(...)` → `enhancer.enhance(prompt)` + a thin shim + that maps the structured response into the existing `EnhanceResult` shape + for the session-controller code path. Move the shim to + `Dreamverse/server/prompting/_internal_compat.py`. +- The locked-segment / next-segment-index plumbing the internal version + built into the user payload becomes Dreamverse-side template logic in + the shim. +- The JSON-shaped responses the internal prompts assume (`{"next_prompt": + "..."}` / `{"segment_prompts": [...]}`) become Dreamverse-side + parsing in the shim, since the public `LLMResponse` is intentionally raw. + +**Why.** The public surface stays minimal and provider-agnostic; the +LTX-2-specific orchestration (locked segments, rollout id/label, JSON +schemas) is an internal-UI concern, not something every public consumer +should wear. Dreamverse keeps its existing call shape; the public stays +clean. + +**Open question for review:** Should we promote some of this into +`fastvideo.entrypoints.streaming.prompt.ltx2_orchestration` (or similar) +once a second consumer appears? Logging here so we have the option. + +### D-3: Multi-stage provider race (Dreamverse) vs sequential fallback (public) + +**Context.** The internal enhancer runs all providers in a stage in parallel +and returns the first to succeed (`_run_provider_race`). The public +enhancer runs providers strictly sequentially with retryable-error fallback. + +**Decision.** Public stays sequential for PR 7.7. The race-based fallback is +a Dreamverse-specific tail-latency optimization that depends on parallel API +budgets; promoting it would force every public consumer to have multiple +provider keys configured. Dreamverse can keep `_run_provider_race` as an +internal optimization on its side. + +**Risk.** First-segment latency on Dreamverse may regress slightly when +Cerebras is having a bad minute (sequential fallback waits the full +20s timeout before trying Groq). If this is a real production concern, +add a public knob like `concurrency: int = 1` on `PromptEnhancer` that +gates a race path — but only after measuring. + +### D-4: Skipping PR 7.9 router for the integration branch + +**Context.** The internal stack ships a `router/main.py` that load-balances +across replicas with health checks. Dreamverse's deployment uses a single +replica per region (per `gpu_pool.py:_parse_requested_gpu_limit`). + +**Decision.** Land PR 7.9 on the public side (so the surface is upstreamed) +but skip wiring it into the Dreamverse integration branch. Dreamverse's +`server/main.py` does not import from `router/`. + +### D-5: Audio re-encode (PR 7.10) needed for streaming, deferred + +**Context.** The internal streaming server's per-step path runs an audio +re-encode (`_re_encode_audio` inside `_stream_av_fmp4_events` / +`do_step_ltx2`) so each fMP4 segment ships with continuation-conditioning +audio. The whole-segment `pool.run()` path the public streaming server +currently uses doesn't need this. The PR plan defers re-encode integration +to PR 7.10 (`generate_async` / per-step streaming). + +**Decision.** Land PR 7.10's `generate_async` on the public side. The +Dreamverse integration branch initially keeps using `pool.run()` (whole +segment, no re-encode); a follow-up branch swaps it to +`generate_async` + audio re-encode once that path is exercised end-to-end. + +### D-6: `realtime/local_runtime.py` is *not* upstreamed + +**Context.** It is the FastVideo-internal precursor to `streaming.gpu_pool`. +Upstreaming both would create two GPU pool implementations in the public +repo. + +**Decision.** Don't upstream `realtime/local_runtime.py`. Dreamverse switches +to `streaming.gpu_pool.SubprocessGpuPool` on the integration branch. The +internal module can be deleted from FastVideo-internal at a follow-up. + +## Open questions for user review + +Each section below is a place the auto-decision could plausibly be wrong. +Please flip / annotate these in review. + +### Q-1 Multi-model GPU pool (D-1) + +Does any current Dreamverse production flow load multiple model ids +concurrently? If yes, we need to either (a) keep `realtime/local_runtime` +alive on the internal side until the public side gains a multi-model pool, +or (b) build the multi-model abstraction upstream as part of PR 7.6 follow-up +work. + +### Q-2 Promoting LTX-2 prompt orchestration (D-2) + +The locked-segments / next-segment-index / JSON-response orchestration is +LTX-2-specific. If Cosmos / Wan / Hunyuan ever grow a similar continuation +flow, we'll regret keeping the orchestration on the consumer side. Worth +promoting now? + +### Q-3 Race-based provider fallback (D-3) + +The sequential fallback in the public enhancer adds up to `timeout_ms` of +extra latency per failing provider before the next is tried. For Dreamverse +that's 20s. Should we land the race path now behind a `concurrency: int = 1` +knob, or wait until we have data? + +### Q-4 Router upstream skip on Dreamverse branch (D-4) + +We're upstreaming PR 7.9 (router) but not consuming it in the Dreamverse +integration branch. Is that right? Dreamverse currently has no router +component, so the answer is probably yes — but flagging. + +### Q-5 generate_async cutover for the streaming path (D-5) + +The plan leaves Dreamverse using `pool.run` (whole segment) initially. +Audio re-encode for cross-segment continuity is deferred to a follow-up. +Is that acceptable for the first switch, or does Dreamverse audio quality +regress relative to the internal path until 7.10 is wired in? + +## PR-by-PR execution log + +### PR 7.6 — already opened (#1257) + +`will/api_7.6` rebased onto `origin/main`, with subprocess-pool robustness +review fixes pushed (boot_ok event, dead-worker detection, parallel shutdown, +reader-exit pending-job cleanup). 17/17 gpu_pool tests + 89/89 streaming +tests green at head. + +### PR 7.7 — already opened (#1258) + +`will/api_7.7` rebased onto the new 7.6 + LLM provider review fixes applied +locally (per-instance `retryable`, 4xx-non-retryable, json-decode wrap, +shared `_openai_compat.complete_openai_compatible`, `dataclasses.replace` +for the fallback marker). 29/29 prompt tests + 120/120 streaming tests green. +**Pending push** — the user opted to push this branch themselves. + +### PR 7.8 — rebased onto new 7.7 + +`will/api_7.8` two commits replayed cleanly on the new 7.7. Adds +`fastvideo/entrypoints/streaming/{prompt/safety,prompt/rewrite,session_logger, +mock_server}.py` plus `test_auxiliaries.py`. 141/141 streaming tests green. + +Notable gap vs internal version: the public `PromptSafetyFilter` ships one +classifier slot (`unsafe` label, single threshold) whereas the internal +version chained an NSFW filter and a hate-speech filter with marker-based +label matching. Multi-classifier composition is left to Dreamverse — +operators chain two filters explicitly. See **D-7** below. + +### PR 7.9 — rebased onto new 7.8 + +`will/api_7.9` three commits replayed cleanly. Adds streaming router +(`router/{config,registry,main}.py`), `fastvideo router-serve` CLI +subcommand, and `test_router.py`. 151/151 streaming tests green. + +Caveat: router/main.py uses the deprecated FastAPI `app.on_event("shutdown")` +hook — emits a DeprecationWarning. Migration to lifespan handlers is a +pre-merge cleanup item but not a blocker. + +### PR 7.10 — rebased onto new 7.9 + +`will/api_7.10` three commits replayed with two trivial conflicts (line +wrap in `server.py`, redundant test in `test_cli_translation.py`). Adds +`VideoEvent` hierarchy, `VideoGenerator.generate_async`, +`default_health_check_request`, plus `test_generate_async.py` (273-line +contract test). 184/184 streaming + contract tests green. + +### PR 8 — rebased onto new 7.10 + +`will/api_8` four commits → three (the 4th was a duplicate +`streaming.md` doc that 7.5 already shipped, dropped during rebase). +Adds `docs/design/server_contracts/{dynamo,index,openai}.md`, +`mkdocs.yml` entries, and `fastvideo/tests/contract/test_{dreamverse, +dynamo}_shape.py`. 206/206 streaming + contract tests green. + +### Dreamverse `will/integrate-public-fastvideo` + +Branch created from Dreamverse `master`. Single change: `pyproject.toml` +swaps `fastvideo = { path = "../FastVideo-internal", editable = true }` +to point at `../FastVideo`. Comment added linking back to this review +doc. + +**Verified:** every TRACKED `from fastvideo.*` import in Dreamverse +(`server/video_generation.py` only) resolves against the public +package — except `fastvideo.layers.quantization.fp4_config.FP4Config` +(see **D-7** / Q-6 below). + +**Untracked WIP** in `Dreamverse/server/{config,prompting,runtime,session}/` +imports `fastvideo.entrypoints.realtime.local_runtime` (D-6); this +branch does not migrate that WIP. The user's existing untracked work +stays untouched and will need a separate follow-up to consume +`streaming.gpu_pool.SubprocessGpuPool`. + +## Test ladder (built-up to e2e per user request) + +Each rung verifies the integration switch at one layer. Run from the +narrowest to the broadest before running the full e2e against real +GPU + model weights. + +| # | Layer | Command | Status against the switched stack | +|---|---|---|---| +| 1 | Public FastVideo unit + contract tests | `pytest fastvideo/tests/api/ fastvideo/tests/entrypoints/streaming/ fastvideo/tests/contract/` | 358/358 passing on `will/api_8` | +| 2 | Public FastVideo FP4 lazy-import | `pytest fastvideo/tests/ops/quantization/test_fp4_config.py` | 3/3 passing | +| 3 | Dreamverse Python tests | `cd Dreamverse && uv run pytest server/tests/ -k "not stress and not benchmark and not health_endpoint"` | 73/73 passing against public FastVideo | +| 4 | Dreamverse FE unit/integration (vitest) | `cd Dreamverse/apps/web && npm test` | 54/86 passing — 32 failures are pre-existing copy-mismatches in `reducer.test.ts` etc., not caused by the switch | +| 5 | Backend HTTP smoke (Playwright) | `cd Dreamverse/apps/web && PLAYWRIGHT_SKIP_WEBSERVER=1 PLAYWRIGHT_BASE_URL=http://127.0.0.1:8009 npx playwright test e2e/backend-health.spec.ts` | 4/4 passing (5th correctly skipped because devtools-only route is off) | +| 6 | Frontend shell smoke (Playwright) | `npx playwright test e2e/frontend-shell.spec.ts` | Pending — requires Next.js dev server to be reachable; was stuck during this run, needs a clean restart | +| 7 | Full e2e preset generation | `npx playwright test e2e/preset-prompt-generation.spec.ts` | **8/8 passing** end-to-end after restart with `CUDA_VISIBLE_DEVICES=4 ENABLE_TORCH_COMPILE=0 FASTVIDEO_GPU_COUNT=1 FASTVIDEO_ENABLE_DEVTOOLS=1`. BE warmup + GPU 4 idle slot let `/readyz` flip green; the spec verifies preset → WS → backend handshake → "Generating video…" state. | + +### How to reproduce e2e tier 7 from cold + +``` +# 1. BE — picks an idle GPU and skips torch.compile (avoids the +# aarch64 cross-compiler bug in the conda env's triton stack). +cd ~/Dreamverse +set -a; source ~/.env; set +a +CUDA_VISIBLE_DEVICES=4 ENABLE_TORCH_COMPILE=0 \ + FASTVIDEO_ENABLE_DEVTOOLS=1 FASTVIDEO_GPU_COUNT=1 \ + uv run dreamverse-server & + +# 2. Wait for /readyz (~2 min for warmup x2 segments) +until curl -fsS http://127.0.0.1:8009/readyz >/dev/null; do sleep 5; done + +# 3. FE +cd ~/Dreamverse/apps/web +BACKEND_URL=http://127.0.0.1:8009 NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \ + npm run dev:devtools & + +# 4. Playwright +cd ~/Dreamverse/apps/web +PLAYWRIGHT_SKIP_WEBSERVER=1 \ + PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \ + BACKEND_URL=http://127.0.0.1:8009 \ + npx playwright test --project=chromium --reporter=list +``` + +### Surfaced during the e2e debug pass (logged here for follow-up) + +* **`SamplingParam has no field ltx2_image_crf`** — Dreamverse's + `server/video_generation.py:406` passes `ltx2_image_crf=0.0` to a + `SamplingParam(...)` constructor. The internal SamplingParam (in + `fastvideo/configs/sample/base.py`) declared this field; the public + `fastvideo.api.sampling_param.SamplingParam` does not. Currently + the BE logs an `ERROR` and silently drops the kwarg; warmup still + succeeds because the field is non-load-bearing for FP4-disabled + inference. Either re-add the field to the public schema or update + Dreamverse to stop passing it. **D-8.** + +* **`aarch64-conda-linux-gnu-cc` triton compile failure** — the conda + env we boot from injects an ARM cross-compiler ahead of `gcc` on + `$PATH`, so `torch._inductor`'s triton launcher fails compilation. + Setting `ENABLE_TORCH_COMPILE=0` bypasses it. Long-term fix: clean + the conda env's compiler shadowing or add a `CC=gcc` override in + Dreamverse's worker bootstrap. **D-9.** + +* **GPU pool starts but warmup OOMs on a shared GPU** — when + `CUDA_VISIBLE_DEVICES` lands on a GPU another tenant is using + (107 GiB-pegged training run on GPU 0 in this case), LTX-2 warmup + fails with OOM. Picking an idle GPU (4-7 here) is a manual step. + A pre-warm probe that checks free memory before booting the pool + would prevent this. **D-10.** + +* **ffmpeg fragment write `Broken pipe`** — when the WS client closes + before the backend finishes streaming the first segment, ffmpeg + hits `[Errno 32] Broken pipe`. Currently Dreamverse's + `gpu_pool.handle_command` re-raises this as a session error, + which then propagates to "User step failed". Cosmetic for now — + swallowing pipe-broken on intentional disconnect would clean up + the logs. **D-11.** + +## Additional integration gaps surfaced during the switch + +### D-7: `FP4Config` is private-only + +**Context.** `Dreamverse/server/video_generation.py:271` imports +`fastvideo.layers.quantization.fp4_config.FP4Config` and assigns it to +`pipeline_config.dit_config.quant_config`. The 411-line module lives only +in `FastVideo-internal/fastvideo/layers/quantization/fp4_config.py` and +hard-imports `flashinfer` at module top — it never made the public +upstream pass. Public has `base_config.py` and `absmax_fp8.py` only. + +**Decision (provisional).** Don't upstream `fp4_config.py` in this +session. Reasons: +1. It introduces a new external dependency (`flashinfer`) the public + package has avoided so far. +2. The class hard-codes LTX-2 layer paths + (`ltx2.blocks.{i}.attn1.to_q` etc.) — this is "LTX-2-specific FP4", + not generic FP4. Belongs colocated with `pipelines/basic/ltx2/` if + it goes anywhere. +3. The FP4 pre-quantize/forward op surface is the kind of thing where + a careful review pass matters more than a bulk copy. + +**What this means for the integration branch.** Dreamverse will boot +fine; only the FP4-quantized path inside `video_generation.py:283` +will fail (lazy import). For workflows that don't enable FP4 +quantization, the integration is complete. + +### Q-6 (review): how to land FP4Config publicly? + +Two reasonable next steps: +1. **Colocate.** Move FP4 code to `fastvideo/pipelines/basic/ltx2/quantization.py` + with `flashinfer` as an optional extra: `pip install fastvideo[fp4]`. + Refactor `FP4QuantizeMethod` to take its layer-prefix list from a + pipeline-config field instead of hardcoding ltx2 paths so the + approach generalizes. +2. **Keep private.** Treat FP4 as a Dreamverse-side concern — Dreamverse + imports `fp4_config` from the internal repo via a thin shim. Public + FastVideo stays focused on generic surfaces. This means the + "FastVideo-internal removable" goal is partially undone. + +Recommendation: option 1 once the API refactor settles — wait until +the LTX-2 colocation step (PR 9 / 10 territory) and land FP4 there. + diff --git a/.agents/memory/dreamverse-integration/source-archive/handoff-nvfp4-launch-demo.md b/.agents/memory/dreamverse-integration/source-archive/handoff-nvfp4-launch-demo.md new file mode 100644 index 0000000000..c928bef1a2 --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/handoff-nvfp4-launch-demo.md @@ -0,0 +1,518 @@ +# Handoff: LTX-2 NVFP4 wire-up + Dreamverse launch-demo skill + +This document hands off in-flight work to the next coding agent. It covers +two related streams that landed across two repos: + +1. **FastVideo** (`will/ltx2_sr_port`): wire NVFP4 (NVIDIA's block-scaled + FP4) inference + per-component torch.compile + supporting parity fixes + so the public package matches `FastVideo-internal` for the LTX-2 + distilled streaming path used by Dreamverse. +2. **Dreamverse** (`will/integrate-public-fastvideo`): switch the GPU + worker to the typed `GeneratorConfig` API, rename `FP4Config` → + `NVFP4Config`, add a `launch-demo` skill + canonical + `serve_configs/streaming_demo.yaml` for `fastvideo serve --config`. + +Stack remains green: 222/222 FastVideo unit/contract/api tests pass; 8/8 +Playwright e2e tests pass against the live `dreamverse-server` + Next.js +stack. + +--- + +## Repo + branch state + +| Repo | Path | Branch | Tip | +| --- | --- | --- | --- | +| FastVideo | `/home/william5lin/FastVideo` | `will/ltx2_sr_port` | `c6c14c55` | +| Dreamverse | `/home/william5lin/Dreamverse` | `will/integrate-public-fastvideo` | `3d7fd89` | +| Reference (read-only) | `/home/william5lin/FastVideo-internal` | (their) `main` | source of truth for parity | + +> **Working branch on FastVideo is `will/ltx2_sr_port`, not the default checkout.** +> The shell may report `will/uv-pip-install-everywhere` because that was +> the earlier checkout. Run `git checkout will/ltx2_sr_port` before +> picking up FastVideo work. + +### Live processes (do not duplicate) + +``` +:8009 dreamverse-server pid 2453227 (warmed, /readyz returns 200) +:5274 next-server (dev) pid 2399103 (devtools build) +``` + +### Stashes + +* FastVideo: `stash@{0}: WIP on main: …HunyuanVideo plugin…` — pre-existing, + unrelated to this work, do not pop. +* Dreamverse: `stash@{0}: wip: server modular refactor (split + config/prompting/runtime/session)` — 3867 lines of orphan modular split + off this branch. Do not pop on this branch; recover on a separate + feature branch if anyone wants to resurrect it. + +--- + +## What landed (FastVideo: `cfccd292..c6c14c55`) + +Six commits on top of the i2v / continuation latent port: + +``` +c6c14c55 test(nvfp4): lock LTX-2 wiring + typed transformer_quant flow +94c983a2 refactor(quant): rename FP4 → NVFP4 to disambiguate from other FP4 variants +42b30bf9 feat(ltx2): wire FP4 inference through fastvideo.layers.quantization +6da342ba feat(compile): per-component compile + transformer_refine + prepare hook +221cb20a feat(api): typed per-component CompileConfig + FastVideoArgs carriers +a4760bae fix(api): propagate generic refine_* args + match internal randn +``` + +Each commit message has the rationale. Highlights below. + +### `a4760bae` — three small parity fixes + +* `FastVideoArgs.__post_init__` now calls `_resolve_refine_args()` which + copies the public-facing generic `refine_*` knobs onto their + `ltx2_refine_*` runtime carriers. Was missing → callers that set + `refine_lora_path=...` saw "applied to 0 layers" warnings as the value + was silently dropped. +* `_randn_ltx2_video_latents` patch path reverted from `randn_tensor` → + `torch.randn` to bit-match internal under single-generator inference. + Identical for a single `torch.Generator` but diverges for + `list[Generator]` (per-sample seeds). +* Classified 19 `refine_*` / `ltx2_refine_*` / i2v / `ltx2_audio_*` / + `ltx2_conditioning_latent_*` / `ltx2_video_conditions` fields in the + schema-parity inventory yaml. + +### `221cb20a` — typed CompileConfig + FastVideoArgs carriers + +`CompileConfig` (in `fastvideo/api/schema.py`) gained per-component knobs: + +```python +@dataclass +class CompileConfig: + enabled: bool = False # master DiT switch + backend / fullgraph / mode / dynamic / extras # master kwargs + + # Per-component overlays, None = inherit master `enabled` + text_encoder_enabled: bool | None = None + vae_enabled: bool | None = None + audio_vae_enabled: bool | None = None + + # Per-component kwargs override master when non-empty + dit_kwargs: dict = ... + text_encoder_kwargs: dict = ... + vae_kwargs: dict = ... + audio_vae_kwargs: dict = ... +``` + +Matching carrier fields on `FastVideoArgs`: +`enable_torch_compile_text_encoder/vae/audio_vae` and +`torch_compile_kwargs_dit/text_encoder/vae/audio_vae`. Compat layer +round-trips them through `legacy_from_pretrained_to_config` and +`generator_config_to_fastvideo_args`. **No behavior change yet** — these +are surface ports only; consumed in the next commit. + +### `6da342ba` — refine + per-component compile + prepare_for_compile + +`composed_pipeline_base.post_init` now: + +* compiles `transformer_refine` alongside `transformer` and + `transformer_2` whenever the DiT compile flag is on (closes the LTX-2 + stage-2 silent-eager bug); +* dispatches per-component compile loops (text encoder, VAE, audio VAE) + with per-component kwargs falling back to master when empty; +* calls `module.prepare_for_compile()` on each compiled submodule + before invoking `torch.compile` (hook protocol — model-specific). + Implemented on `Gemma3` to materialize HF weights outside Dynamo's + tracer. + +### `42b30bf9` — NVFP4 LTX-2 inference wire-up *(largest)* + +End-to-end: + +1. `models/dits/ltx2.py` — swap `nn.Linear` → `ReplicatedLinear` for the + FP4-eligible subset (`LTXSelfAttention`, `LTXDistributedSelfAttention`, + `FeedForward`/`GELUApprox`); plumb `quant_config` and `prefix=` from + `BasicAVTransformerBlock` → `_init_transformer_blocks` → `LTXModel` + → `LTX2Transformer3DModel`. Other linears + (`TimestepEmbedding`, `PixArtAlphaTextProjection`, `patchify_proj`, + `proj_out`, `AdaLayerNormSingle.linear`) stay `nn.Linear` — + matches internal exactly. +2. Port `_supports_prequantized_input` and + `_linear_project_with_optional_prequant` helpers. Attention forward + pre-quantizes input once (`quantize_input`), reuses the + `(x_fp4, x_scale, x_global_sf)` tuple for k/v projections when + `context is x` — bit-matches internal's fused path. +3. `models/loader/fsdp_load.py` — new `_maybe_convert_model_to_nvfp4` + helper detects via `isinstance(quant_method, NVFP4QuantizeMethod)` + (no flag); calls `convert_model_to_nvfp4` to materialize + `_nvfp4_weight*` / `_nvfp4_alpha` / `_weight_global_sf` buffers. + `flashinfer` import is lazy (inside the helper), so the loader is a + no-op on hosts without flashinfer. +4. `layers/quantization/__init__.py` — registered `"NVFP4"` in + `QuantizationMethods` literal + `get_quantization_config`. +5. `api/compat.py` + `fastvideo_args.py` — typed + `engine.quantization.transformer_quant: "NVFP4"` resolves to a + concrete `NVFP4Config()` instance, carried on `FastVideoArgs.transformer_quant`, + pinned onto `pipeline_config.dit_config.quant_config` in + `__post_init__._apply_transformer_quant`. **The explicit setter + (legacy mutation pattern) wins** if `dit_config.quant_config` is + already non-None. +6. `layers/linear.py` — `LinearBase.__init__` now falls back to + `UnquantizedLinearMethod` when `quant_config.get_quant_method` returns + `None`. `NVFP4Config` only tags a curated subset of LTX-2 layers, and + the previous `assert quant_method is not None` would crash any + non-tagged layer that received a quant_config. + +### `94c983a2` — FP4 → NVFP4 rename + +NVIDIA's specific block-scaled fp4 format (e2m1 mantissa, fp32 alpha, +`layout_128x4` scale layout, group size 16) — distinct from MX-FP4 / +OCP-FP4 / generic e3m0. Mechanical rename, no behavior change: + +* `fp4_config.py` → `nvfp4_config.py` +* `FP4Config` → `NVFP4Config`; `get_name()` returns `"nvfp4"` +* `FP4QuantizeMethod` → `NVFP4QuantizeMethod` +* `convert_model_to_fp4` → `convert_model_to_nvfp4` +* `QuantizationMethods` literal: `"FP4"` → `"NVFP4"` +* registered buffer names: `_fp4_weight`/`_fp4_alpha` → + `_nvfp4_weight`/`_nvfp4_alpha` +* loader helper renamed +* test file rename + symbol updates + +Internal-scope torch op namespace `fastvideo_fp4::*` and +`_get_ltx2_fp4_stage_profile` deliberately left as-is — purely +internal naming that mirrors FastVideo-internal. + +### `c6c14c55` — contract + numerical lock-in tests + +* `fastvideo/tests/ops/quantization/test_nvfp4_ltx2_wiring.py` (6 tests): + asserts that `LTXSelfAttention.to_q/to_k/to_v/to_out` are + `ReplicatedLinear`; `NVFP4Config()` attaches `NVFP4QuantizeMethod` + on the quantized subset with the correct `layer_prefix`; non-tagged + projections (cross-attn K/V, audio attn, audio FFN) fall back to + `UnquantizedLinearMethod`; `BasicAVTransformerBlock` propagates + `quant_config` and `prefix` correctly to all 4 attention modules + + FFN at once. +* `fastvideo/tests/api/test_typed_quant_flow.py` (4 tests): asserts + typed `engine.quantization.transformer_quant: "NVFP4"` → + `NVFP4Config()` instance flow; default leaves `transformer_quant` + None; explicit `dit_config.quant_config = …` wins over typed carrier. + +--- + +## What landed (Dreamverse: `248060b..3d7fd89`) + +Three commits on top of the e2e tier: + +``` +3d7fd89 feat(skill): launch-demo orchestrator + fastvideo serve YAML +d80c2a8 refactor(server): drive FP4 + per-component compile via typed GeneratorConfig +4cc6b30 chore: gitignore Playwright + Next.js build artifacts under apps/web +``` + +### `d80c2a8` — server/video_generation.py refactor + +Three coordinated changes in the GPU worker: + +* Replace legacy `load_kwargs` dict + `VideoGenerator.from_pretrained(model_root, **kwargs)` + call with the typed `GeneratorConfig` (`EngineConfig` / + `OffloadConfig` / `CompileConfig` / `PipelineSelection` / + `ComponentConfig`). Refine knobs move from `ltx2_refine_*` flat + kwargs into `preset_overrides["refine"]`. **The in-memory + `pipeline_config` pin** (`dit_config.quant_config = NVFP4Config()`) + keeps using the legacy `experimental["pipeline_config"]` carrier + because typed `transformer_quant: "NVFP4"` doesn't yet support + setting `layer_profile`. +* Rename FP4 → NVFP4. +* Re-enable `"mode": "max-autotune-no-cudagraphs"` (was commented out). + Closes the last known divergence vs FastVideo-internal in the + worker-level path trace. + +### `4cc6b30` — gitignore Playwright/Next.js artifacts + +Added `apps/web/{node_modules,.next,test-results,playwright-report}` to +`.gitignore`. Mirror of the existing `prod-ui/` ignore set. + +### `3d7fd89` — launch-demo skill + +``` +.agents/skills/launch-demo/ +├── SKILL.md +└── scripts/ + ├── launch_demo.sh # orchestrator: BE + FE + health probes + Ctrl-C trap + ├── launch_backend_dreamverse.sh # uv run dreamverse-server (default) + ├── launch_backend_fastvideo.sh # uv run fastvideo serve --config (typed path) + └── launch_frontend.sh # next dev (devtools/dev/single5s) +serve_configs/ +└── streaming_demo.yaml # canonical ServeConfig matching internal/ui +``` + +YAML has every field annotated with the internal source line it mirrors: +LTX-2 distilled, NVFP4, 121 frames @ 1088×1920 24fps, 5 inference steps, +2-step refine gs=1.0 add_noise=true, max-autotune-no-cudagraphs compile, +121-frame default request, 300s session timeout, 6 segment cap, av_fmp4 +streaming, cinematic-drone warmup prompt, 2400s warmup timeout, 9 +conditioning frames + 0 end-offset, prompt enhancer on with cerebras / +gpt-oss-120b / 20s timeout. + +**Two BE flavors documented in SKILL.md:** + +| `BE_FLAVOR=` | Boots | Routes served | FE compatible | +| --- | --- | --- | --- | +| `dreamverse` (default) | `dreamverse-server` | `/healthz`, `/readyz`, `/curated-presets`, `/v1/stream`, devtools, session monitor | ✓ full | +| `fastvideo` | `fastvideo serve --config ` | `/health`, `/v1/stream` | ⚠ FE will surface fetch errors for `/curated-presets`, `/readyz` until those routes migrate into FastVideo's `build_app` | + +The fastvideo flavor exists today as the verifiable typed-config path +(YAML parses, streaming worker boots, dotted overrides work). It is not +yet a drop-in for the FE — see "Open follow-ups" below. + +--- + +## Verified + +* `222 passed, 1 skipped` across `fastvideo/tests/api/`, + `fastvideo/tests/contract/`, + `fastvideo/tests/ops/quantization/test_nvfp4_*`, + `tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py`. +* `8 passed` Playwright e2e (backend-health 5, frontend-shell 2, + preset-prompt-generation 1) against the live `dreamverse-server` + + Next.js stack. +* `streaming_demo.yaml` parses cleanly against `ServeConfig`; the + validation path of `fastvideo serve --config ` runs without + error and accepts dotted overrides like `--server.port 8010`. +* FastVideo `bash -n` clean across all four launch scripts. + +--- + +## Critical context (gotchas a successor should know) + +### NVFP4 layer set is asymmetric — by design + +`NVFP4Config.fp4_layers` covers: + +* `attn1.{to_q,to_k,to_v,to_out}` — full self-attention +* `attn2.{to_q,to_out}` — cross-attn Q + out only (text context not quantized) +* `audio_to_video_attn.{to_q,to_out}` — AV cross Q + out +* `video_to_audio_attn.{to_k,to_v}` — VA cross K + V +* `ffn.{fc_in,fc_out}` — video FFN +* `adaln_single.linear` — but this is `nn.Linear` (not `LinearBase`), + so it never actually gets FP4'd. List entry has no effect; matches + internal. + +**NOT in the set:** audio self-attention (`audio_attn1.*`), audio +cross-attention (`audio_attn2.*`), audio FFN (`audio.ffn.*`). Audio +path is cheap enough that quant overhead isn't worth it. Test +`test_basic_av_block_propagates_quant_config_to_all_children` locks +this in — if you add audio quantization later, update the test. + +### `LinearBase` fallback is load-bearing + +`fastvideo/layers/linear.py:191-202`: when `quant_config.get_quant_method` +returns `None` (layer not in the quant config's set), we fall back to +`UnquantizedLinearMethod`. **Do not remove this fallback** — it would +break every non-tagged `ReplicatedLinear` constructed with a +`NVFP4Config`, and `assert quant_method is not None` in +`ReplicatedLinear.__init__` would fire on unmatched layers. + +### Typed `transformer_quant` precedence + +`FastVideoArgs._apply_transformer_quant` only writes +`dit_config.quant_config` when it's currently `None`. If a caller has +explicitly set `pipeline_config.dit_config.quant_config = NVFP4Config(...)`, +the explicit setter wins. Dreamverse's `video_generation.py` relies on +this — it sets `NVFP4Config()` directly because the typed +`transformer_quant: "NVFP4"` doesn't expose `layer_profile`. + +### Pre-existing AbsMaxFP8 test failure is NOT mine + +`fastvideo/tests/ops/quantization/test_absmax_fp8.py::test_create_weights_rejects_invalid_dtype` +fails on `main` and on this branch with the same error +("AssertionError not raised"). I confirmed via `git stash` that the +failure pre-dates my changes. Not blocking; tracked as separate tech +debt. + +### `transformer_refine` is auto-compiled with the master DiT flag + +Set `enable_torch_compile=True` and `transformer_refine` compiles +along with `transformer` and `transformer_2`. There is **no separate +`enable_torch_compile_refine` flag** — by design, refine inherits the +DiT compile state to keep the typed surface small. If you need them +decoupled, add a new field; don't repurpose existing ones. + +### `prepare_for_compile` is a duck-type protocol, not a base class method + +Defined nowhere; called via `getattr(module, "prepare_for_compile", None)` +in `composed_pipeline_base._maybe_compile_pipeline_module`. Currently +only Gemma implements it (to materialize HF weights outside Dynamo). +Add to other models that have lazy external state if you observe +compile-time graph breaks. + +### Public typed `PromptEnhancerConfig.provider` is `Literal["cerebras", "groq"]` + +Internal supports `"cerebras_ifm"` (config.py:143). The public typed +schema does not. The `streaming_demo.yaml` defaults to `"cerebras"`. +For agents that need `cerebras_ifm`, the `dreamverse-server` flavor +respects the `FASTVIDEO_PROMPT_PROVIDER` env var (legacy path); +`fastvideo serve --config` does not currently expose it. + +### Dreamverse `pipeline_config` is still a Python object passed via `experimental` + +The typed `GeneratorConfig` doesn't have a clean home for an +in-memory `PipelineConfig` instance with mutated `dit_config`. We +pass it via `pipeline.experimental["pipeline_config"]` — the +`compat.py` legacy adapter recognizes that key and threads it through +to `FastVideoArgs.from_kwargs`. This is fine but not pretty; if +someone designs a typed `dit_config` carrier later, this becomes +obsolete. + +### `fastvideo serve --config` is not yet a drop-in for the FE + +`fastvideo.entrypoints.streaming.server.build_app` exposes only +`/health` and `/v1/stream`. The Dreamverse Next.js shell expects +`/healthz`, `/readyz`, `/status`, `/curated-presets`, +`/curated-presets/append`, `/prompt-system-config`, and the devtools +routes. These all live in `Dreamverse/server/main.py` + +`Dreamverse/server/routes/`. Until they migrate into FastVideo's +`build_app` (or are exposed via a Dreamverse-side proxy), the +`BE_FLAVOR=fastvideo` flavor is for verifying the typed serve config +path only — not for full FE compatibility. + +--- + +## Open follow-ups (prioritized) + +### High + +1. **Migrate FE-required routes into FastVideo's `build_app`.** + `/healthz`, `/readyz`, `/status` look obviously fastvideo-side + (they're streaming-server health). `/curated-presets` and + `/prompt-system-config` are operator-side surfaces and should + probably stay in Dreamverse (or migrate as opt-in routes that the + FE feature-detects). Without this, `BE_FLAVOR=fastvideo` is + permanently a "diagnostic" flavor. Closes the + `launch-demo` skill TODO. + +2. **AbsMaxFP8 test failure cleanup.** Pre-existing. Either fix the + test (`AbsMaxFP8LinearMethod.create_weights` no longer asserts on + invalid dtype — restore the assert if intentional, otherwise drop + the test). + +### Medium + +3. **Add `cerebras_ifm` to public `PromptEnhancerConfig.provider` + Literal.** Trivial schema change; needs paired enhancer-side + provider implementation in + `fastvideo/entrypoints/streaming/prompt/providers/`. + +4. **Expose `layer_profile` on typed `engine.quantization`.** Today + `transformer_quant: "NVFP4"` always constructs `NVFP4Config()` + with the default `layer_profile="refine"`. To support stage-1 + profiles (no `attn2.to_out`, no cross-modal AV) via typed config, + add `transformer_quant_layer_profile: str | None = None` and + thread it through `compat.py`. Dreamverse currently dodges this + by setting `NVFP4Config()` directly via `experimental`. + +5. **Typed `dit_config.quant_config` carrier.** The + `experimental["pipeline_config"]` escape hatch in Dreamverse + should eventually become a typed field. Design TBD. + +### Low + +6. **Audio attention quantization profile.** If an audio-quant + profile is added to `NVFP4Config.fp4_layers` (currently audio attn + and FFN are bf16), update + `test_basic_av_block_propagates_quant_config_to_all_children`. + +7. **Schema parity inventory.** A few internal-only fields are not + exposed publicly (`PROMPT_HTTP_TIMEOUT_MS`, + `PROMPT_INITIAL_STAGE_TIMEOUT_MS`, `PROMPT_TEMPERATURE`, + `PROMPT_MAX_COMPLETION_TOKENS`, `PROMPT_AUTO_SLEEP_MS`, + `PROMPT_AUTO_TIMEOUT_MS`, the curated-presets file paths). + These all flow via env vars on `dreamverse-server` today; if + `fastvideo serve --config` becomes the canonical entrypoint, + they'll need typed homes. + +8. **Empty `apps/web/test-results/` directory locally.** The + `.gitignore` entry I added makes it invisible to `git status`, + but the dir itself still has a stale `.last-run.json` (45 bytes) + from a prior Playwright run. Harness blocked auto-cleanup + ("pre-existing files"); the user can `rm -rf + apps/web/test-results` whenever convenient. + +--- + +## How to pick up work + +### Quick orientation (run these first) + +```bash +# FastVideo state +cd /home/william5lin/FastVideo +git checkout will/ltx2_sr_port +git log --oneline cfccd292..HEAD # six commits added this round +.venv/bin/python -m pytest fastvideo/tests/api/ \ + fastvideo/tests/contract/ \ + fastvideo/tests/ops/quantization/test_nvfp4_*.py \ + tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py \ + -q --no-header # expect 222 passed, 1 skipped + +# Dreamverse state +cd /home/william5lin/Dreamverse +git log --oneline 248060b..HEAD # three commits added this round +cat serve_configs/streaming_demo.yaml | head -40 +ls .agents/skills/launch-demo/ + +# Live stack health (already running on this host) +curl -s http://localhost:8009/readyz | head -c 200 +curl -s http://localhost:5274/ | head -c 100 +( cd apps/web && npx playwright test --reporter=line ) # expect 8 passed +``` + +### Reference docs + +* **FastVideo internal/ui parity source:** `../FastVideo-internal/ui/ltx2-streaming/server/config.py` +* **NVFP4 source on internal:** `../FastVideo-internal/fastvideo/layers/quantization/fp4_config.py` +* **Worker-trace audit:** `../FastVideo/dreamverse_review.md` (D-1 + multi-model, D-5 audio re-encode, prior gap inventory) +* **Schema parity inventory:** `docs/design/inference_schema_parity_inventory.yaml` +* **PR-plan for the broader migration:** `../FastVideo/PR plan.md` + +### Files most likely to need touches in follow-ups + +* `fastvideo/api/schema.py` — `CompileConfig`, `QuantizationConfig`, + `PromptEnhancerConfig` Literal extension. +* `fastvideo/api/compat.py` — typed → flat translation. +* `fastvideo/fastvideo_args.py` — carrier fields and + `_apply_transformer_quant`. +* `fastvideo/entrypoints/streaming/server.py::build_app` — add + `/healthz`, `/readyz`, `/status` routes for FE compatibility (high + priority follow-up #1). +* `Dreamverse/server/video_generation.py` — typed `GeneratorConfig` + builder (current). +* `Dreamverse/serve_configs/streaming_demo.yaml` — every parity + knob; edit here, not in shell scripts. + +--- + +## Don't / Cautions + +* **Don't pop the Dreamverse stash on this branch.** It's 3867 lines + of orphan modular refactor (server/{config,prompting,runtime,session}/) + with broken absolute imports. If anyone wants to resurrect it, do so + on a separate feature branch. +* **Don't remove the `LinearBase` `UnquantizedLinearMethod` fallback.** + See "Critical context" above. +* **Don't repurpose `enable_torch_compile` to mean DiT-only.** It also + drives `transformer_refine` and `transformer_2` compile. Add a new + flag if decoupling is needed. +* **Don't change `NVFP4Config` buffer names back to `_fp4_*`.** The + rename is intentional to disambiguate from MX-FP4 / OCP-FP4. +* **Don't bypass the typed surface for new options.** New compile / + quant / refine knobs should land on the dataclass + compat.py + + parity inventory together. The existing test suite locks this in. +* **Don't merge to main without a CI run that covers FP4.** Current + CI doesn't run flashinfer-dependent paths; the wiring tests in + `test_nvfp4_ltx2_wiring.py` are CPU-only by design and don't + exercise the actual FP4 kernels. + +--- + +*Last updated: end of session that landed `c6c14c55` on FastVideo and +`3d7fd89` on Dreamverse. Stack remains green; no dirty state.* diff --git a/.agents/memory/dreamverse-integration/source-archive/streaming-server-upstream-plan.md b/.agents/memory/dreamverse-integration/source-archive/streaming-server-upstream-plan.md new file mode 100644 index 0000000000..9cf100f925 --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/streaming-server-upstream-plan.md @@ -0,0 +1,539 @@ +# FastVideo Streaming Server Upstream — Design & Plan + +## Status +Exploration / design draft. Captures the re-evaluation triggered by the +decision to upstream `FastVideo-internal/ui/ltx2-streaming/server/` into +the public repo. Not yet approved for execution. + +## Related Documents +- [PR plan.md](../../PR%20plan.md) — PR-by-PR implementation plan for the API refactor +- [apirefactor.md](../../apirefactor.md) — design spec this plan implements +- `../../../FastVideo-internal/ui/ltx2-streaming/` — upstream source (server side) +- `../../../dynamo/` — local clone of ai-dynamo/dynamo; backend patterns at + `components/src/dynamo/{vllm,sglang,trtllm}/` and `CLAUDE.md` files +- https://github.com/ai-dynamo/dynamo/pull/7544 — draft PR that promotes + FastVideo to a native Dynamo backend (CLOSED, superseded — but establishes + the integration shape) + +## Context + +The internal `FastVideo-internal/ui/ltx2-streaming/` directory contains a +complete LTX2 streaming service. The user has decided: + +- **Frontend clients** (`client/`, `prod-ui/`) stay in the internal repo +- **Everything server-side** — FastAPI/WebSocket server, GPU pool, prompt + enhancer, router, auxiliaries — will be upstreamed to FastVideo + +In parallel, FastVideo is becoming a **first-class Dynamo backend** (same +tier as vllm, sglang, trtllm). The refactor must produce an API that +Dynamo's `components/src/dynamo/fastvideo/` package can consume as a +pure Python import, without re-introducing the legacy flat-kwarg +surface. Draft PR ai-dynamo/dynamo#7544 defines the concrete integration +shape we need to support. + +This materially changes the tail of the API refactor plan. The current +PR 5 ("wire `ServeConfig.default_request` into the OpenAI-compatible +HTTP server") addresses only the stateless endpoint; the real upstream +target is a much larger, session-based stack **plus** a clean Dynamo +backend contract. + +This document captures: +- what's being upstreamed and where it lands +- four design decisions that shape the upstream (continuation model, + streaming server layout, LLM provider abstraction, Dynamo backend + integration) +- a revised PR sequence for the tail of the refactor + +## What's being upstreamed + +| Internal path | Size | Role | Upstream target | +|---|---|---|---| +| `server/main.py` | 94KB | FastAPI + WebSocket, session lifecycle, segment orchestration | `fastvideo/entrypoints/streaming/server.py` + handlers | +| `server/gpu_pool.py` | 66KB | GPU orchestration, subprocess workers | `fastvideo/entrypoints/streaming/gpu_pool.py` | +| `server/prompt_enhancer.py` | 69KB | LLM orchestration (cerebras_ifm, cerebras, groq) | `fastvideo/entrypoints/streaming/prompt/` package | +| `server/mock_server.py` | 45KB | Mock backend for dev/tests | `fastvideo/entrypoints/streaming/mock_server.py` | +| `server/prompt_safety.py` | 7KB | Optional fasttext-gated prompt safety | `fastvideo/entrypoints/streaming/prompt/safety.py` | +| `server/session_init_image.py` | 3KB | i2v init image handling | `fastvideo/entrypoints/streaming/session_init_image.py` | +| `server/rewrite_prompt_payload.py` | 3KB | Rewrite flow payload builder | `fastvideo/entrypoints/streaming/prompt/rewrite.py` | +| `server/session_logger.py` | 1KB | Session JSONL logs | `fastvideo/entrypoints/streaming/session_logger.py` | +| `server/config.py` | 9KB | Env-driven server config | Typed `ServeConfig` extensions | +| `router/main.py` | 27KB | Multi-replica load balancer + WS proxy | `fastvideo/entrypoints/streaming/router/` (or separate package) | +| `slurm/` | — | Deployment scripts | Likely stays internal | + +## FastVideo contact surface today + +Direct calls from the internal stack into FastVideo, all in `gpu_pool.py`: + +| Location | Call | Notes | +|---|---|---| +| `gpu_pool.py:164` | `from fastvideo.entrypoints.video_generator import VideoGenerator` | Subprocess-level import, post-`CUDA_VISIBLE_DEVICES` setup | +| `gpu_pool.py:230` | `PipelineConfig.from_pretrained(config_model_path)` | Direct access to legacy `PipelineConfig` | +| `gpu_pool.py:231` | `pipeline_config.dit_config.quant_config = FP4Config()` | Direct internals mutation | +| `gpu_pool.py:264-267` | `VideoGenerator.from_pretrained(model_root, **load_kwargs)` | Flat legacy kwargs | +| `gpu_pool.py:837` | `generator.generate_video(**request_kwargs)` | Per-segment flat kwargs | +| `gpu_pool.py:282-288` | `LTX2AudioEncoder`, `AudioProcessor`, `get_diffusers_config` | Audio re-encode path | + +`load_kwargs` at `gpu_pool.py:233-260` contains: +`ltx2_refine_enabled`, `ltx2_refine_upsampler_path`, `ltx2_refine_lora_path`, +`ltx2_refine_num_inference_steps`, `ltx2_refine_guidance_scale`, +`ltx2_refine_add_noise`, `pipeline_config`, `torch_compile_kwargs`, +`dit_cpu_offload`, `dit_layerwise_offload`, `vae_cpu_offload`, +`text_encoder_cpu_offload`, `pin_cpu_memory`, `ltx2_vae_tiling`, +`use_fsdp_inference`, `enable_torch_compile`. + +`request_kwargs` at `gpu_pool.py:837` includes: +`ltx2_audio_clean_latent`, `ltx2_audio_denoise_mask`, +`ltx2_video_conditions`, `video_position_offset_sec`, standard sampling +fields. + +**Implication**: upstreaming `gpu_pool.py` as-is perpetuates the flat +kwarg surface inside the public server. We need a typed translation +(PR 6 expansion) at the worker boundary before, or as part of, the +gpu_pool upstream. + +## Session / continuation semantics today + +Per-session state (in `server/main.py`): +- `locked_segment_prompts`, `curated_prompts`, `segment_idx`, + `generated_segment_count`, `loop_iteration` + +Per-**GPU** (not per-session) continuation cache (in `gpu_pool.py`): +- `ltx2_continuation_images` — last 9 decoded frames for clip conditioning +- `ltx2_continuation_audio_latents` — denoised audio latents for audio conditioning + +Segment N+1 automatically conditions on segment N's trailing frames and +audio. On session reset or handoff (`USER_JOIN`), the per-GPU cache is +cleared. There is currently **no way for a client to serialize and +resume continuation state elsewhere** — it lives on the GPU only. + +## Design Decision 1: Continuation model + +### Options + +**A. Opaque client-round-trip payload** (current plan PR 7 design) +- Server returns `ContinuationState(kind, payload)`; client sends it back. +- Pro: stateless server, trivially load-balanceable, survives disconnects. +- Con: large payloads (frames + audio latents) over every request hop; + bandwidth heavy on multi-segment WebSocket sessions. + +**B. Server-held session state** (internal reality) +- Continuation lives per-GPU; implicit between adjacent segments. +- Pro: zero client bandwidth; fast; matches today. +- Con: needs GPU affinity, no resume after disconnect, harder to scale horizontally. + +**C. Hybrid** (recommended) +- Server-held is the default for streaming WebSocket sessions. +- Server exposes a `snapshot_state` message that returns the opaque + payload form for migration/retry. +- Stateless HTTP endpoints always use round-trip opaque payloads. +- One serialization format underlies both surfaces. + +### Decision: **C (Hybrid)** + +Rationale: matches both internal streaming use (server-held, fast) and +stateless API use (client-round-trip, resumable). Cost is one serialization +layer that serves both. + +### Implications +- `ContinuationState.kind` identifies the payload schema + (e.g. `"ltx2.v1"`). +- `ContinuationState.payload` must cover: + - trailing conditioning frames (or a tensor reference) + - audio latents (or a tensor reference) + - segment index / rollout position + - any model-specific conditioning metadata (e.g. audio sample rate, + `video_position_offset_sec`) +- For large tensors, payload may reference a server-side blob by ID + rather than inline everything. +- Streaming server has a `SessionStore` keyed by session ID that holds + a typed `LTX2ContinuationState` object. +- `SessionStore.snapshot(session_id) -> ContinuationState` serializes + the current state for export. +- `SessionStore.hydrate(state: ContinuationState) -> session_id` loads + a state into a new session. +- Plan PR 7 expands to cover both surfaces and define the payload schema. + +## Design Decision 2: Streaming server layout + +### Options + +- **A. `fastvideo/entrypoints/streaming/`** — parallel to + `fastvideo/entrypoints/openai/` +- **B. `fastvideo/entrypoints/server/{stateless,streaming}/`** — reorg both +- **C. `fastvideo/streaming/`** — top-level package, not under entrypoints + +### Decision: **A (parallel subpackage)** + +Rationale: lowest-friction, no existing code moves, both servers share +the same `fastvideo/entrypoints/*` namespace and import style. Shared +utilities can be factored into `fastvideo/entrypoints/server_common/` +later if needed. Option B creates churn across every openai/ import for +marginal organizational win. + +### Target layout + +```text +fastvideo/entrypoints/ +├── openai/ # existing: stateless HTTP POST +│ ├── api_server.py +│ ├── video_api.py +│ ├── image_api.py +│ ├── common_api.py +│ ├── protocol.py +│ ├── state.py +│ ├── stores.py +│ └── utils.py +├── streaming/ # NEW: session WebSocket +│ ├── server.py # FastAPI + WebSocket entry +│ ├── session.py # session lifecycle, state machine +│ ├── session_store.py # typed session state + snapshot/hydrate +│ ├── protocol.py # JSON WebSocket message schemas +│ ├── stream.py # fMP4 encoding (av_fmp4 mode) +│ ├── gpu_pool.py # subprocess workers +│ ├── worker.py # per-GPU worker loop +│ ├── continuation.py # typed LTX2 state payload +│ ├── session_init_image.py +│ ├── session_logger.py +│ ├── mock_server.py +│ ├── prompt/ +│ │ ├── enhancer.py # provider-agnostic prompt ops +│ │ ├── rewrite.py +│ │ ├── safety.py # optional fasttext +│ │ ├── payload.py # rewrite payload builder +│ │ └── providers/ +│ │ ├── base.py # LLMProvider protocol +│ │ ├── cerebras.py +│ │ ├── cerebras_ifm.py +│ │ └── groq.py +│ └── router/ # or separate top-level package +│ ├── main.py +│ └── registry.py +├── cli/ # existing +└── video_generator.py # existing +``` + +### Config integration + +`ServeConfig` gets an optional `streaming: StreamingConfig | None` field: + +```python +@dataclass +class StreamingConfig: + session_timeout_seconds: int = 300 + generation_segment_cap: int = 6 + stream_mode: Literal["av_fmp4", "legacy_jpeg"] = "av_fmp4" + warmup: WarmupConfig = field(default_factory=WarmupConfig) + pool: GpuPoolConfig = field(default_factory=GpuPoolConfig) + prompt: PromptEnhancerConfig | None = None + safety: PromptSafetyConfig | None = None + +@dataclass +class GpuPoolConfig: + num_workers: int | None = None # default: CUDA_VISIBLE_DEVICES count + enable_audio_reencode: bool = True + conditioning_num_frames: int = 9 + conditioning_end_offset: int = 0 + +@dataclass +class PromptEnhancerConfig: + provider: Literal["cerebras_ifm", "cerebras", "groq"] = "cerebras_ifm" + model: str = "gpt-oss-120b" + timeout_ms: int = 20000 + system_prompt_dir: str | None = None # hot-reloadable system prompts + +@dataclass +class PromptSafetyConfig: + enabled: bool = False + classifier_path: str | None = None +``` + +## Design Decision 3: LLM provider abstraction + +### Problem + +`prompt_enhancer.py` (69KB) hard-codes three providers (cerebras_ifm, +cerebras, groq) with provider-specific request/response handling +scattered throughout. Upstreaming as-is locks FastVideo to those three +providers and couples the prompt operations to their response shapes. + +### Shape + +Introduce an `LLMProvider` protocol: + +```python +from typing import Protocol, AsyncIterator, Literal +from dataclasses import dataclass + +@dataclass +class LLMMessage: + role: Literal["system", "user", "assistant"] + content: str + +@dataclass +class LLMRequest: + messages: list[LLMMessage] + model: str + max_tokens: int | None = None + temperature: float | None = None + timeout_ms: int | None = None + +@dataclass +class LLMResponse: + content: str + provider: str + model: str + latency_ms: float + fallback_used: bool = False + +class LLMProvider(Protocol): + name: str + async def complete(self, request: LLMRequest) -> LLMResponse: ... +``` + +### Decision: **Protocol + built-in implementations for cerebras, cerebras_ifm, groq** + +Rationale: keeps the prompt enhancer free of provider-specific branching; +users (and future OpenAI/Anthropic/local additions) can register their +own provider without modifying FastVideo. Each built-in provider is +100-200 LOC; the enhancer becomes provider-agnostic prompt orchestration. + +### Implications +- `prompt_enhancer.py` splits into `enhancer.py` (prompt operations) + + `providers/` (IO). +- Config moves from scattered env vars to typed `PromptEnhancerConfig` + under `ServeConfig.streaming.prompt`. +- Hot-reloadable system prompts stay — exposed as a management endpoint + on the streaming server. +- Fallback behavior (retry across providers in priority order) moves + into the enhancer layer, orthogonal to provider implementations. + +## Design Decision 4 preamble: what Dynamo expects from FastVideo + +Dynamo's backend pattern (observed in +`dynamo/components/src/dynamo/sglang/` and confirmed by PR #7544) is a +**pure Python import** pattern. Dynamo owns the backend subpackage in its +own repo; FastVideo only needs to expose a stable, typed, aggregated +and (later) streaming generation surface. + +### Contract surface Dynamo consumes + +| Surface | Shape | Notes | +|---|---|---| +| Constructor | `VideoGenerator.from_pretrained(model_path, **typed_kwargs)` | Already exists; `typed_kwargs` must be a stable subset from `GeneratorConfig` — no flat LTX2 legacy kwargs. | +| Sync execution | `generator.generate_video(request: GenerationRequest) -> VideoResult` | Aggregated mode; Dynamo wraps in `asyncio.to_thread` under an `asyncio.Lock`. | +| Async execution | `generator.generate_async(request: GenerationRequest) -> AsyncGenerator[VideoEvent, None]` | Needed for: (a) streaming server fMP4 chunks; (b) future Dynamo disaggregation. Events: `Progress`, `Partial?`, `Final`. | +| Typed request | `fastvideo.api.GenerationRequest`, `SamplingConfig`, `InputConfig` | Stable import path; Dynamo's adapter builds this from `NvCreateVideoRequest` + `VideoNvExt`. | +| Typed result | `VideoResult` with `video_bytes` or tensor frames, plus `ContinuationState?` | Must be picklable / JSON-serializable enough for Dynamo RPC. | +| Continuation | `ContinuationState(kind, payload)` with schema-versioned payloads | Used by FastVideo's session store today; tomorrow by Dynamo disaggregated workers. | +| Health check input | `VideoGenerator.default_health_check_request() -> GenerationRequest` | Minimal 256x256 / 8 frames / 1 step; lets Dynamo's `FastVideoHealthCheckPayload.to_dict()` produce the Dynamo `health_check_payload` kwarg without knowledge of FastVideo internals. | +| Config dump | `GeneratorConfig.to_dict()` / `ServeConfig.to_dict()` | Dynamo calls `dynamo.common.config_dump.dump_config(path, config)` at worker start; we already have `config_to_dict()`. | + +### Request/response mapping (Dynamo ↔ FastVideo) + +Dynamo's video protocol (`NvCreateVideoRequest` / `NvVideosResponse`): + +``` +NvCreateVideoRequest -> fastvideo.api.GenerationRequest + prompt -> sampling.prompt + size="WxH" -> sampling.width, sampling.height + seconds -> (seconds * nvext.fps) -> sampling.num_frames + input_reference -> input.image_path / input.video_path + nvext.fps -> sampling.fps + nvext.num_frames -> sampling.num_frames (overrides seconds*fps) + nvext.num_inference_steps -> sampling.num_inference_steps + nvext.guidance_scale -> sampling.guidance_scale + nvext.seed -> sampling.seed + nvext.negative_prompt -> sampling.negative_prompt + response_format -> (handled by adapter at output) + +VideoFinalEvent -> NvVideosResponse + video_bytes -> data[0].b64_json (if response_format=b64_json) + video_url (after upload) -> data[0].url (if response_format=url) + metadata.inference_time_s -> inference_time_s +``` + +All fields already exist (or will exist after PR 6 expansion) on +FastVideo's typed schema. No FastVideo changes required beyond what the +rest of this plan already covers **except**: + +1. `generate_async` must exist (new in PR 7.10). +2. `default_health_check_request()` helper (new in PR 7.10). +3. The sync `generate_video(request=...)` path must be reachable without + extra wrapping (exists since PR 2; confirm stability). + +### Where the Dynamo subpackage lives + +The Dynamo-side integration (`FastVideoHandler`, `register_fastvideo_model`, +`FastVideoHealthCheckPayload`, args parsing, main.py, Dockerfile, +request/response mapping) lives **entirely in the Dynamo repo** at +`components/src/dynamo/fastvideo/`, matching the pattern used by vllm +and sglang. FastVideo does **not** host any Dynamo-related subpackage, +Dynamo dependency, or Dynamo-specific CLI. FastVideo's only obligation +is to expose a clean, stable, typed Python API that Dynamo's backend +package can import. + +## Design Decision 4: Dynamo as first-class backend target + +### Problem + +PR #7544 (closed) shows two frictions with the pre-refactor API: + +1. **Flat legacy kwargs** — the Dynamo handler had to know about + LTX2-specific flat names. +2. **Sync-only generation** — Dynamo's async handler wrapped + `generator.generate(...)` in `asyncio.to_thread` under a lock; no + progress streaming, no disaggregation path. + +The refactor's stateless OpenAI server, WebSocket streaming server, and +Dynamo backend all want the same thing: **a typed async API that yields +progress events and a typed final result**. If we build it once in +`VideoGenerator`, all three adapters become thin. + +### Options + +**A. Keep sync-only, each adapter wraps** +- Simple; matches PR #7544. +- Con: streaming server needs its own async runner; Dynamo loses progress + streaming; no path to disaggregation. + +**B. Add async event stream to `VideoGenerator`** +- `generate_async(request) -> AsyncGenerator[VideoEvent, None]`. +- Sync `generate_video` becomes a thin `asyncio.run` wrapper internally. +- Pro: one canonical execution API; streaming server, OpenAI server, + and Dynamo all consume events directly. +- Con: larger delta in `VideoGenerator` — must thread async through the + pipeline step loop. + +**C. Queue-based `generate(request, event_cb)` callback** +- Middle ground; callback receives events. +- Pro: no async rewrite needed. +- Con: callers have to invert control; awkward for Dynamo's async + handler. + +### Decision: **B (async event stream)** + +Rationale: one substrate serves all three consumers. The cost is a +`generate_async` implementation that runs the pipeline step loop in a +thread and bridges events back via an asyncio queue — standard pattern, +limited surface area. + +### Implications + +- New PR 7.10 adds `generate_async` on `VideoGenerator` with three event + types: `VideoProgressEvent(step, total_steps, stage)`, + `VideoPartialEvent(frames_ndarray, index)` (optional; emitted only in + the streaming path), `VideoFinalEvent(video_bytes_or_tensor, metadata, + continuation_state?)`. +- Sync `generate_video(request=...)` becomes `asyncio.run(...)` over + `generate_async`, collecting events and returning the final. +- Streaming server's fMP4 encoder consumes `VideoPartialEvent` frames + directly, never re-decoding through disk. +- Dynamo adapter consumes `generate_async` and yields one + `NvVideosResponse` per `VideoFinalEvent` (aggregated mode; ignores + intermediate events today; can surface progress via Dynamo's + status/progress fields in the future). +- `ContinuationState` can be attached to `VideoFinalEvent.metadata`, + giving Dynamo a first-class way to surface state for disaggregation + later. +- Stable public exports: `from fastvideo import VideoGenerator`; + `from fastvideo.api import GenerationRequest, SamplingConfig, + ContinuationState, VideoResult, VideoEvent`. +- No Dynamo subpackage, dep, or CLI lives in FastVideo. The adapter + (`NvCreateVideoRequest ↔ GenerationRequest` mapping, handler, + registration) lives entirely in the Dynamo repo at + `components/src/dynamo/fastvideo/`. + +### Constraints this adds to earlier PRs + +- **PR 6** (typed LTX2 kwargs): every flat kwarg must have a typed home + **reachable from `GeneratorConfig`**, so Dynamo can construct the + generator without importing internal compat paths. +- **PR 7** (continuation state): `ContinuationState.payload` must be + JSON/YAML serializable (no raw torch tensors inline; use blob + indirection) so it survives Dynamo RPC transport. +- **PR 7.5** (streaming skeleton): consume `generate_async` rather than + re-implementing a progress loop around `generate_video`. +- **PR 2/3/4 already landed**: the typed request shape is fixed and + matches Dynamo's mapping needs — no backtracking required. + +## Revised PR sequence (PR 5 onwards) + +PRs 0-4 are unchanged and already landed. PR 5 is narrowed; PRs 5.5-7.9 +are new inserts; PRs 8-13 are reshaped or kept. + +| # | Title | Change | Key deliverables | +|---|---|---|---| +| **5** | Stateless `ServeConfig.default_request` merge | **Narrowed.** Wire typed default-request into `fastvideo/entrypoints/openai/`. | `_merge_default_request` helper, validated-against-preset, tests for default+user-override precedence | +| **5.5** | Server architecture split | **NEW.** Introduce `fastvideo/entrypoints/streaming/` subpackage skeleton. No behavior change. | Empty subpackage + stub server.py; CLI subcommand `fastvideo streaming-serve` (raises NotImplementedError); doc on layout | +| **6** | LTX2 public preset + stage overrides + config colocation | **Expanded.** Also add typed replacements for every flat kwarg used by internal `gpu_pool.py`. | `ltx2_two_stage` preset, `LTX2RefineStageOverride`, `CompileConfig` field types, typed `FP4Config` integration, colocation | +| **7** | Continuation state (public + session) | **Expanded.** Define both opaque payload AND server-held session store. | `ContinuationState.payload` schema, `LTX2ContinuationState` typed subclass, `SessionStore` interface, snapshot/hydrate APIs | +| **7.5** | Streaming server skeleton | **NEW.** Minimum viable WebSocket server: session lifecycle, JSON messages, fMP4 output, single-generator. | `server.py`, `session.py`, `protocol.py`, `stream.py` (fMP4), typed `StreamingConfig` | +| **7.6** | GPU pool upstream | **NEW.** Upstream `gpu_pool.py` with typed config boundary. | `gpu_pool.py`, `worker.py`, job queue, session-to-GPU binding, session timeout handling | +| **7.7** | Prompt enhancer upstream | **NEW.** Upstream `prompt_enhancer.py` with `LLMProvider` abstraction. | `prompt/enhancer.py`, `prompt/providers/{base,cerebras,cerebras_ifm,groq}.py`, hot-reloadable system prompts | +| **7.8** | Streaming auxiliaries | **NEW.** Small, isolated. | `prompt/safety.py`, `session_init_image.py`, `prompt/rewrite.py`, `session_logger.py`, `mock_server.py` | +| **7.9** | Router upstream | **NEW.** Multi-replica load balancer + WS proxy. | `streaming/router/` (or separate top-level package), health checks, WS proxy | +| **7.10** | Dynamo backend contract | **NEW.** Add `VideoGenerator.generate_async` event stream + `default_health_check_request()` helper. FastVideo exposes the async API only; the Dynamo backend package (handler, adapter, registration) lives entirely in the Dynamo repo at `components/src/dynamo/fastvideo/`. Streaming server (PR 7.5) and Dynamo backend both consume the same async API. | `generate_async` with `VideoProgressEvent`/`VideoPartialEvent`/`VideoFinalEvent`; sync `generate_video` becomes a thin wrapper; contract tests against a mock Dynamo-style handler that imports only public FastVideo APIs | +| **8** | Internal-UI ↔ public-server contract docs & tests | **Reframed.** Was "Dreamverse Server Adaptation Layer." Also covers Dynamo integration reference. | WebSocket protocol reference, contract tests, migration examples, Dynamo adapter example that upstream PR can copy verbatim | +| **9** | LongCat preset migration + colocation | **Keep.** | Stage overrides, colocation | +| **10** | Hunyuan15 SR preset migration + colocation | **Keep.** | Stage overrides, SR field migration POC, colocation | +| **11** | SSIM / perf test migration | **Keep.** Now blocked on PR 6 expansion. | Typed API migration of golden tests | +| **12** | Docs + examples | **Keep, expand.** | Streaming server docs now part of scope | +| **13** | Deprecation + cleanup | **Keep, expand.** | Also deprecate flat kwargs that internal gpu_pool uses today | + +Total PR count: 13 → ~20 (13 original + 5 streaming-upstream inserts + +1 architecture split + 1 Dynamo contract). Each new PR is small and +self-contained because the streaming components are already cleanly +separated in the internal repo, and the Dynamo contract rides on top of +the async API that the streaming server already needs. + +## Open questions + +1. **Router: in-repo or separate package?** — It's orthogonal to inference; + in-repo couples deploy cycles, separate leaves FastVideo cleaner. + Recommendation: separate package `fastvideo-router/` or + `fastvideo/contrib/router/`; defer final call to PR 7.9. +2. **Session ID authority** — internal uses ad-hoc client IDs. + Recommendation: server-generated UUID, accept externally provided + session ID only for resume flows. +3. **Torch compile kwargs typing** — `CompileConfig.kwargs: dict[str, Any]` + today accepts `mode`, `backend`, `fullgraph`, `dynamic`. Options: keep + as opaque dict; fully type; hybrid (type the common four + allow + extras). Recommendation: hybrid, type common fields. +4. **Prompt safety / fasttext dependency** — heavy for users who don't + need it. Recommendation: ship as optional extra + `pip install fastvideo[prompt-safety]`. +5. **Audio-specific tensor payloads** — `ltx2_audio_clean_latent`, + `ltx2_audio_denoise_mask`, `ltx2_audio_latents` are not in the current + public schema. PR 7 should classify them (probably as opaque fields + inside `LTX2ContinuationState.payload`, not top-level sampling fields). +6. **Batching behavior** — internal `test_batching.py` suggests batching + is exercised. Scope this into PR 7.5 or defer to a post-cleanup perf PR? +7. ~~**Dynamo subpackage home**~~ — **Resolved.** No Dynamo code lives + in FastVideo. The full backend package (handler, adapter, + registration, health check) is owned by the Dynamo repo at + `components/src/dynamo/fastvideo/`, same pattern as vllm/sglang. + FastVideo only guarantees the public API contract listed above. +8. **Disaggregation readiness** — PR #7544 is aggregated-only. Our + `ContinuationState` hybrid already supports a future prefill/decode + split (prefill yields state; decode hydrates it). Should PR 7.10 + explicitly validate that `ContinuationState` survives round-trip + through a Dynamo-style RPC (pickle or JSON), even though Dynamo + isn't using it today? Recommendation: yes; cheap contract test that + prevents drift. +9. **Dynamo progress/status passthrough** — `NvVideosResponse` has + `status` and `progress` fields. Should PR 7.10's handler contract + emit intermediate `NvVideosResponse` chunks keyed off + `VideoProgressEvent`, or stay aggregated-final-only to match PR + #7544? Recommendation: stay aggregated-final for PR 7.10; revisit + after Dynamo clarifies their streaming/progress semantics. + +## Immediate path forward + +1. Land `will/api_5` cleanup commits — **done** (`e03ca7d9`, `41f93179` + force-pushed without Claude co-author). +2. Review this plan with a human — commit the doc to capture the state. +3. Execute PR 5 (narrow stateless merge) and PR 5.5 (subpackage split) + in parallel. Both small; both unblock the streaming upstream that + follows. +4. Start PR 6 expansion (typed replacements for flat LTX2 kwargs) as the + critical path for PR 7.6 (gpu_pool upstream). diff --git a/.agents/memory/dreamverse-integration/source-archive/video-generator-config-api-design.md b/.agents/memory/dreamverse-integration/source-archive/video-generator-config-api-design.md new file mode 100644 index 0000000000..c1d7e586cf --- /dev/null +++ b/.agents/memory/dreamverse-integration/source-archive/video-generator-config-api-design.md @@ -0,0 +1,93 @@ +# Exploration Log: Video Generator Config API Design + +## Status: draft + +## Context +FastVideo's Python inference API currently mixes generator-instance settings, +pipeline initialization settings, and per-request sampling/runtime settings +through broad `**kwargs` surfaces on `VideoGenerator.from_pretrained(...)` and +`VideoGenerator.generate_video(...)`. + +This exploration compares the current FastVideo design with +`sglang/multimodal_gen` and examines how to upstream multi-stage LTX2 / +Dreamverse behavior without growing more ad hoc top-level flags. + +## Progress +- [x] Read FastVideo onboarding, codebase map, and relevant design docs. +- [x] Inspect current FastVideo generator, args, sampling, registry, and + workflow abstractions. +- [x] Inspect internal LTX2 streaming server usage and current two-stage / + continuation requirements. +- [x] Inspect SGL diffusion generator, server args, sampling params, and + request preparation boundary. +- [x] Inspect vLLM-Omni stage config, stage metadata, request, and orchestration + surfaces for multi-stage pipeline ideas. +- [x] Inspect current FastVideo CLI/config-file loading and compare with the + training YAML-only entrypoint. +- [ ] Convert findings into a concrete implementation plan for FastVideo. + +## Findings +- FastVideo already has the right internal separation points: + `FastVideoArgs`, `PipelineConfig`, `SamplingParam`, and `ForwardBatch`. +- The public boundary is the unstable part: + init-time and request-time knobs are mixed through `**kwargs`. +- Unknown init keys can be silently filtered, while unknown request keys can be + only logged rather than rejected. This makes API drift hard to detect. +- SGL's split is cleaner: + `ServerArgs` for engine/runtime, `PipelineConfig` for model-family wiring, + and `SamplingParams` for per-request settings. +- SGL also has better merge semantics for user request overrides: + it preserves model defaults, tracks explicitly provided fields, and validates + request params against pipeline task type. +- SGL still has a design smell worth avoiding in FastVideo: + `SamplingParams._adjust(...)` depends on `ServerArgs`, which leaks + engine/pipeline concerns back into the request object. +- vLLM-Omni contributes a useful extra abstraction beyond SGL: + model-owned multi-stage topology via `ModelPipeline` and `StageConfig`, + with per-stage defaults (`default_sampling_params`) and runtime override + layering. +- vLLM-Omni's best reusable idea for FastVideo is not the serving stack, but + the separation between: + 1. model-defined stage topology and per-stage defaults, + 2. runtime engine overrides, + 3. request-time sampling/state handoff. +- vLLM-Omni also shows the downside of exposing stage-indexed request lists too + directly: `sampling_params_list` works for a serving engine, but is too + positional and low-level for FastVideo's higher-level Python API. +- FastVideo already supports YAML/JSON config files for inference CLI, but the + current mechanism flattens nested documents back into argparse flags. This + preserves backward compatibility but keeps the CLI surface as the canonical + schema instead of a typed document model. +- The training stack has a cleaner precedent: a YAML-first config loaded into a + typed schema, with dotted CLI overrides applied onto the nested document + before parsing. Inference can likely adopt a lighter variant of that pattern. +- Multi-stage generation should be unified at the orchestration layer, not by + forcing LongCat refine, Hunyuan SR, and LTX2 continuation into one leaf config. + +## Mistakes / Dead Ends +- A fully free-form string-dict API would lose too much type safety and would + likely recreate the current drift problem under a different shape. +- A single universal `RefineConfig` for all models would become a sparse bag of + nullable fields and would not map cleanly to existing model families. + +## Proposed Standardization +- Introduce a typed public split: + `GeneratorConfig` for instance-lifetime engine/init settings and + `GenerationRequest` for per-call inputs/sampling/output. +- Allow dict input only as an interchange layer that is parsed immediately into + typed configs with strict unknown-key validation. +- Add a typed `GenerationPlan` / multi-stage orchestration layer with + discriminated stage configs: + `SampleStageConfig`, `LongCatRefineStageConfig`, + `HunyuanSRStageConfig`, `LTX2ContinuationStageConfig`. +- Let model families own stage defaults and stage topology through named + profiles or model-defined stage plans, similar in spirit to vLLM-Omni's + pipeline YAMLs, but expose them through typed Python config objects rather + than raw stage-indexed lists in the primary API. +- Make YAML/JSON a first-class serialization of the same typed inference + schema, not just a file format that expands into CLI flags. +- Prefer a YAML-first CLI pattern for nested configs: + `fastvideo generate --config run.yaml --request.sampling.seed 42`, + while keeping a compatibility layer for existing flat flags during migration. +- Upstream LTX2 two-stage / continuation behavior as a first-class stage or + pipeline profile rather than more `ltx2_*` top-level kwargs. diff --git a/.agents/memory/dreamverse-integration/state.md b/.agents/memory/dreamverse-integration/state.md new file mode 100644 index 0000000000..7e5e18d8e8 --- /dev/null +++ b/.agents/memory/dreamverse-integration/state.md @@ -0,0 +1,196 @@ +# Current State — 2026-05-06 (D-26 rebase onto origin/main; PR base flipped) + +Point-in-time snapshot of branches, commits, and live infrastructure. +Update whenever commits land or services restart. + +For HOW to commit / push / verify see [runbook.md](runbook.md). For +roster of co-authors to credit on every commit see +[authors.md](authors.md). + +## Branch tips + +| Repo | Branch | Tip | Distance | +|---|---|---|---| +| FastVideo | `will/ltx2_sr_port` (**PR #1288 head**) | `fbd823df` | merged-into-main pending; latest tip post-D-16 + integration-review + integration-plan + GPU4 smoke validation | +| FastVideo | **`will/dreamverse-monorepo`** (**REBASED ONTO `origin/main`**, was forked from `will/ltx2_sr_port`) | `83829c5e` | 66 commits ahead of `origin/main` (`c17d33bf`). Contains the full LTX-2 SR port + NVFP4 + Dreamverse monorepo migration + audio kwarg fix + warmup + NVENC build + benchmarks + integration memory dir, all rebased to be openable as a single PR against main. PR #1288 on `will/ltx2_sr_port` is untouched. End-to-end verified on GPU4 pre-rebase with audio continuation across segments 1→2 (`Cached audio latents shape=(1, 8, 126, 16) for segment 2`, `Segment 2: relayed av chunks=22, bytes=3.8MB`, no BrokenPipeError). NVENC build supported but not usable on this dev host (B200 has no NVENC silicon — verified by direct ffmpeg probe). See [decisions-log.md D-19](decisions-log.md#d-19) + [D-20](decisions-log.md#d-20) + [D-21](decisions-log.md#d-21) + [D-26](decisions-log.md#d-26). | +| FastVideo | `will/dreamverse-monorepo-pre-main-rebase-backup-20260506` | `2ee839a3` | **local-only safety backup** of the pre-rebase chain (the same 66 commits stacked on `2aaeee2a`). Keep until the new chain is fully verified by the next round of e2e on a non-stuck dev box or until the PR merges. | +| FastVideo | `will/api_7.10` | `6ae7a99f` | **deprecated** — PR #1287 closed in favor of #1288. Branch can be deleted on origin and locally; kept for now as historical reference. | +| FastVideo | `will/api_8`, `will/ltx2_sr_runtime`, `will/ltx2_nvfp4`, `will/ltx2_post_fixes`, `will/agents_cleanup` | (various) | **deprecated** split bookmarks. Strategy reversed to single mega-PR (D-17). Safe to delete locally; not pushed to origin. | +| FastVideo | `will/ltx2_sr_port-pre-1286-rebase` | `1baa60bb` | **local-only safety backup** of pre-rebase chain (37 commits); keep until next slice merges | +| Dreamverse | `will/integrate-public-fastvideo` | `ec8ef92` | 10 commits ahead of `737f3c1` (the dep switch) | +| FastVideo-internal | their `main` | (read-only ref) | — | + +FastVideo worktree default branch is `will/ltx2_sr_port`. Other agents +share this worktree — if `git branch --show-current` shows something +else, switch back cleanly with `git checkout will/ltx2_sr_port` (don't +disturb their uncommitted work). I observed this happen repeatedly in +the 2026-05-05 session — confirmed harmless; switching back was always +safe with a clean working tree. + +## Post-#1286 rebase summary + +PR #1286 merged at `2aaeee2a` (squash). `will/ltx2_sr_port` was rebased +onto new `origin/main`, dropping 4 commits whose content is now in main: + +- `cd76cf51` `[feat] streaming: router (multi-replica load balancer)` +- `1ac1e732` `[feat] streaming: fastvideo router-serve CLI` +- `b0b7f59c` `[test] streaming: router registry + health loop ...` +- `40e265b8` `[fix] streaming: router polish — bridge cancel + state + machine + deps` (squashed into `2aaeee2a` via cherry-pick `a152cb77`) + +Rebase was clean — no conflicts. All 33 surviving commits got new SHAs +(rebase rewrites). The pre-rebase tip `1baa60bb` is preserved on the +local backup branch `will/ltx2_sr_port-pre-1286-rebase`. + +## New linearized chain (33 commits, slice indices for STACK.md) + +| Slice | PR | Commits | Tip SHA | Subject | +|---|---|---|---|---| +| 1-3 | 7.10 (PR #1287) | 3 | `6ae7a99f` | `[test] streaming: generate_async coverage + refreshed streaming test` | +| 4-6 | 8 | 3 | `f32e31ec` | `[test] streaming: contract tests for Dreamverse + Dynamo shapes` | +| 7-15 | LTX-2 SR | 9 | `e7297519` | `feat(ltx2): full i2v conditioning + continuation latent port` | +| 16-21 | NVFP4 | 6 | `6793166b` | `test(nvfp4): lock LTX-2 wiring + typed transformer_quant flow` | +| 22-23 | LTX-2 post-fixes | 2 | `25897b67` | `[fix]: unwrap list-of-generator before torch.randn in LTX-2 latent prep` | +| 24-33 | agents_cleanup | 10 | `b34d9704` | `[docs] dreamverse-integration: add runbook + fresh-context onboarding` | + +5 PRs landed (7.5, 7.6, 7.7, 7.8, 7.9), 1 in flight (7.10), 5 remaining +(8 / LTX-2 SR / NVFP4 / post-fixes / agents_cleanup). + +## Historical commit chain analysis (pre-#1286 rebase) + +The layered chain analysis below documented the pre-rebase SHAs (LTX-2 +SR layer, NVFP4 layer, post-handoff fixes layer). Those SHAs no longer +exist on `will/ltx2_sr_port` — they live only on +`will/ltx2_sr_port-pre-1286-rebase`. Content semantics are unchanged; +SHAs were rewritten by the rebase. Kept here for narrative continuity. + +## FastVideo: commit chain `cfccd292..156103b9` + +Three layers since LTX-2 i2v port: + +### Layer 1 — LTX-2 SR port + alignment harness (5 commits) + +``` +365a66c7 feat(quantization): upstream LTX-2 FP4Config with lazy flashinfer +433d26b2 feat(ltx2): port LTX-2 SR runtime — upsampler, refine stages, refine args +751d05de feat(ltx2): wire SR pipeline graph + port denoising/latent-prep stages +af6bbfea test(ltx2-sr): add numerical alignment harness — public vs internal +974cd430 fix(ltx2-sr): close port gaps surfaced by alignment harness retries +b6ac7630 test(ltx2-sr): pin ltx2 sampling knobs in harness for parity diff +b043d550 fix(api): align public SamplingParam ltx2 defaults with distilled +663dda80 fix(registry): order LTX-2 detectors so distilled wins for distilled paths +cfccd292 feat(ltx2): full i2v conditioning + continuation latent port (BASE) +``` + +(Predates the May 2 handoff.) + +### Layer 2 — NVFP4 wire-up + per-component compile (6 commits, May 2 handoff) + +``` +a4760bae fix(api): propagate generic refine_* args + match internal randn +221cb20a feat(api): typed per-component CompileConfig + FastVideoArgs carriers +6da342ba feat(compile): per-component compile + transformer_refine + prepare hook +42b30bf9 feat(ltx2): wire FP4 inference through fastvideo.layers.quantization +94c983a2 refactor(quant): rename FP4 → NVFP4 to disambiguate from other FP4 variants +c6c14c55 test(nvfp4): lock LTX-2 wiring + typed transformer_quant flow +``` + +See [quantization.md](quantization.md) for what each commit locks in. + +### Layer 3 — Post-handoff parity/perf fixes (3 commits, since May 2) + +``` +a5fcd19c [fix]: lazy-import flash_attn 2 fallback in attention backend +d4ee5be2 [fix]: avoid model.to() round-trip in Gemma encoder forward +156103b9 [fix]: unwrap list-of-generator before torch.randn in LTX-2 latent prep (HEAD) +``` + +Three small fixes — no new features. Continued parity tightening with internal. + +## Dreamverse: commit chain `737f3c1..ec8ef92` + +``` +737f3c1 chore: switch fastvideo dep from FastVideo-internal to public FastVideo +4cc6b30 chore: gitignore Playwright + Next.js build artifacts under apps/web +33caa92 test(e2e): align Playwright specs with the actual production composer +6fd137c test(e2e): tighten frontend-shell + preset specs to match actual UI +248060b test(e2e): add Playwright tier with backend-health smoke + preset run +d80c2a8 refactor(server): drive FP4 + per-component compile via typed GeneratorConfig +3d7fd89 feat(skill): launch-demo orchestrator + fastvideo serve YAML +72f69b9 Update ffmpeg installation instructions. +1ba5635 fix(server): block startup on GPU warmup readiness, propagate failures +ec8ef92 fix(server): detect worker death in _send_command via proc.sentinel (HEAD) +``` + +The post-handoff trio (`72f69b9`, `1ba5635`, `ec8ef92`) hardens server +startup robustness — ffmpeg install docs, GPU warmup readiness gate, and +worker-death detection. + +## Live services (do not duplicate) + +| Port | Service | PID | Status | +|---|---|---|---| +| 8009 | `dreamverse-server` | 705513 | `/readyz` returns 200, 1 GPU worker on GPU 4, NVFP4 (50.9 GiB), `ENABLE_TORCH_COMPILE=0`, `FASTVIDEO_FFMPEG_BIN=$HOME/opt/ffmpeg-native/bin/ffmpeg`, `FASTVIDEO_VIDEO_CODEC=libx264` (post-D-20 deploy at 2026-05-05 14:??) | +| 5274 | `next-server` (dev) | 707746 | 200 | +| 8000 | unknown FastAPI | — | **Not in handoff.** Probably stray `fastvideo serve`. Verify with `lsof -i :8000` before launching a new BE on the default port. | + +## Stashes — DO NOT POP + +| Repo | Stash | Reason | +|---|---|---| +| FastVideo | `stash@{0}: WIP on main: 71bfc13d HunyuanVideo plugin` | Pre-existing, unrelated to integration work | +| Dreamverse | `stash@{0}: wip: server modular refactor (split config/prompting/runtime/session)` | 3867-line orphan modular split, **not part of `will/integrate-public-fastvideo`**. Recover on a separate branch if needed. | + +## Test status + +| Suite | Status | +|---|---| +| FastVideo `fastvideo/tests/api/` (post-D-20) | 185 passed (was 222 before some tests moved; new `test_extra_overrides_routing.py` adds 7) | +| FastVideo `contract/` + `nvfp4_*` + `ltx2_pipeline_smoke` (May 2 handoff) | 222 passed, 1 skipped | +| Playwright e2e against live BE+FE (D-19) | 8 passed (5 backend-health + 2 frontend-shell + 1 preset-prompt-generation) | +| Live segment-1→segment-2 audio continuation (D-20) | passes — `Cached audio latents shape=(1, 8, 126, 16) for segment 2` + `Segment 2: relayed av chunks=22, bytes=3.8MB`, no BrokenPipeError | +| `dreamverse-deploy.sh` flag parser standalone test (D-20) | 13/13 permutations pass + bad-flag rejection (defaults / single flags / both flags / `--no-*` overrides / env-only / flag-overrides-env / both-env+both-no-flags / flags interleaved with positional args) | +| `fastvideo serve --config streaming_demo.yaml` validation (May 2 handoff) | parses cleanly; dotted overrides work | +| `bash -n` on `apps/dreamverse/scripts/install_native_ffmpeg.sh` + `dreamverse-deploy.sh` (D-20) | clean | + +## Pre-existing failures (NOT caused by this work) + +| Test | Failure | Notes | +|---|---|---| +| `fastvideo/tests/ops/quantization/test_absmax_fp8.py::test_create_weights_rejects_invalid_dtype` | `AssertionError not raised` | Pre-existing on `main`. Verified via `git stash` that NVFP4 work doesn't introduce it. See [open-threads.md](open-threads.md) item #2. | + +## Source docs (archived 2026-05-03) + +The 7 source docs that this memory dir consolidates have been moved into +[`source-archive/`](source-archive/) — see the +[archive README](source-archive/README.md) for the archive policy and +synthesis mapping. + +Other untracked items at the FastVideo repo root: +- Nested clones: `dynamo/`, `ray/`, `vllm-omni/` +- Lock files: `uv.lock`, `fastvideo/tests/ssim/.reference_videos_download.lock` +- Skill dirs: `.agents/skills/diagnose-ssim-failure/`, `.agents/skills/review-pr-link/` +- `.agents/exploration/pr-link-review.md` (kept; already promoted to a skill) + +## Quick orientation commands + +```bash +# FastVideo state +cd /home/william5lin/FastVideo +git log --oneline cfccd292..HEAD # 14 commits this round + +# Dreamverse state +cd /home/william5lin/Dreamverse +git log --oneline 737f3c1..HEAD # 10 commits this round + +# Live stack health (already running) +curl -s http://localhost:8009/readyz | head -c 300 +curl -s http://localhost:5274/ -o /dev/null -w "%{http_code}\n" + +# Re-verify test suite +.venv/bin/python -m pytest fastvideo/tests/api/ \ + fastvideo/tests/contract/ \ + fastvideo/tests/ops/quantization/test_nvfp4_*.py \ + tests/local_tests/pipelines/test_ltx2_pipeline_smoke.py \ + -q --no-header +``` diff --git a/.agents/memory/dreamverse-integration/streaming-server.md b/.agents/memory/dreamverse-integration/streaming-server.md new file mode 100644 index 0000000000..e26775c52e --- /dev/null +++ b/.agents/memory/dreamverse-integration/streaming-server.md @@ -0,0 +1,389 @@ +# Streaming Server Upstream — PRs 5.5 → 7.10 + +The `FastVideo-internal/ui/ltx2-streaming/server/` stack is being +upstreamed into public FastVideo at `fastvideo/entrypoints/streaming/`. +In parallel, FastVideo is becoming a first-class Dynamo backend (same +tier as vllm, sglang, trtllm). This file covers both threads since they +share `generate_async` as the substrate. + +For PR sequence/status see [pr-roadmap.md](pr-roadmap.md). For the +Dreamverse-side adoption see [cross-repo-surfaces.md](cross-repo-surfaces.md). + +**Last updated:** 2026-05-03. + +## What's being upstreamed + +| Internal path | Size | Role | Public target | +|---|---|---|---| +| `server/main.py` | 94 KB | FastAPI + WebSocket, session lifecycle, segment orchestration | `fastvideo/entrypoints/streaming/server.py` + handlers | +| `server/gpu_pool.py` | 66 KB | GPU orchestration, subprocess workers | `fastvideo/entrypoints/streaming/gpu_pool.py` | +| `server/prompt_enhancer.py` | 69 KB | LLM orchestration (cerebras_ifm, cerebras, groq) | `fastvideo/entrypoints/streaming/prompt/` package | +| `server/mock_server.py` | 45 KB | Mock backend for dev/tests | `fastvideo/entrypoints/streaming/mock_server.py` | +| `server/prompt_safety.py` | 7 KB | Optional fasttext-gated prompt safety | `prompt/safety.py` | +| `server/session_init_image.py` | 3 KB | i2v init image handling | `streaming/session_init_image.py` (PR 7.5, already public) | +| `server/rewrite_prompt_payload.py` | 3 KB | Rewrite flow payload builder | `prompt/rewrite.py` | +| `server/session_logger.py` | 1 KB | Session JSONL logs | `streaming/session_logger.py` | +| `server/config.py` | 9 KB | Env-driven server config | typed `ServeConfig.streaming` extensions | +| `router/main.py` | 27 KB | Multi-replica load balancer + WS proxy | `fastvideo/entrypoints/streaming/router/` | +| `slurm/` | — | Deployment scripts | Stays internal | + +Frontend clients (`client/`, `prod-ui/`) stay in the internal repo. + +## Four design decisions that shape the upstream + +### D-1: Continuation model — Hybrid (server-held + opaque client-round-trip) + +Streaming WebSocket sessions hold continuation per-GPU (matches today's +internal behavior, fast, zero client bandwidth). Stateless HTTP endpoints +use opaque round-trip payloads. Server exposes a `snapshot_state` message +that returns the opaque form for migration/retry. + +One serialization layer underlies both surfaces. + +Implementation: `SessionStore` (in-memory default, pluggable for +redis/etc.) keyed by session ID, holds typed `LTX2ContinuationState`. +- `snapshot(session_id) -> ContinuationState` exports for migration +- `hydrate(state: ContinuationState) -> session_id` loads state into new session + +Payload schema covers: trailing conditioning frames (or tensor-blob ID), +audio latents (or blob ID), segment index, audio sample rate, +`video_position_offset_sec`, model-specific metadata. + +Landed in PR 7. See [cross-repo-surfaces.md](cross-repo-surfaces.md) for +the full wire format. + +### D-2: Streaming server layout — Parallel subpackage `fastvideo/entrypoints/streaming/` + +Sits next to `fastvideo/entrypoints/openai/`. No existing code moves. +Both servers share the `entrypoints/*` namespace. Shared utilities can be +factored into `fastvideo/entrypoints/server_common/` later if needed. + +### D-3: LLM provider abstraction — `LLMProvider` protocol + built-in providers + +`prompt_enhancer.py` (69 KB) hard-coded three providers (cerebras_ifm, +cerebras, groq) with provider-specific request/response handling +scattered throughout. Upstreaming as-is would lock FastVideo to those +providers. + +Protocol shape: + +```python +@dataclass +class LLMRequest: + messages: list[LLMMessage] + model: str + max_tokens: int | None = None + temperature: float | None = None + timeout_ms: int | None = None + +@dataclass +class LLMResponse: + content: str + provider: str + model: str + latency_ms: float + fallback_used: bool = False + +class LLMProvider(Protocol): + name: str + async def complete(self, request: LLMRequest) -> LLMResponse: ... +``` + +PR 7.7 ships built-in providers for cerebras, groq. **Public Literal +currently restricts to `Literal["cerebras", "groq"]`** — `cerebras_ifm` +is internal-only and remains environment-driven on `dreamverse-server`. +See [open-threads.md](open-threads.md) follow-up #3. + +Hot-reloadable system prompts via management endpoint. Sequential +fallback across providers in priority order — race-based fallback (the +internal optimization) deferred per [decisions-log.md](decisions-log.md) +D-3. + +### D-4: Dynamo as first-class backend target — async event stream + +PR ai-dynamo/dynamo#7544 (closed draft) showed two frictions: + +1. Flat legacy kwargs — Dynamo handler had to know LTX-2-specific names. +2. Sync-only generation — Dynamo wrapped `generator.generate(...)` in + `asyncio.to_thread` under a lock; no progress streaming, no + disaggregation path. + +Decision: **add `generate_async`** as the canonical execution API. + +```python +async def generate_async( + self, + request: GenerationRequest, +) -> AsyncGenerator[VideoEvent, None]: ... +``` + +Events: + +```python +@dataclass +class VideoProgressEvent: + step: int + total_steps: int + stage: str # "denoise" | "refine" | "decode" | ... + +@dataclass +class VideoPartialEvent: + frames: np.ndarray # (num_frames, H, W, 3) + index: int # monotonic chunk index + +@dataclass +class VideoFinalEvent: + video_bytes: bytes | None + tensor: torch.Tensor | None + metadata: dict[str, Any] + continuation_state: ContinuationState | None +``` + +The sync `generate_video(request=...) -> VideoResult` becomes a thin +`asyncio.run` wrapper over `generate_async` that collects events and +returns the final. + +**Three consumers, one substrate:** + +| Consumer | Transport | Request shape | State | +|---|---|---|---| +| Stateless OpenAI (`fastvideo/entrypoints/openai/`) | HTTP POST | `GenerationRequest` merged onto `ServeConfig.default_request` | Stateless; opaque payload | +| Streaming WebSocket (`fastvideo/entrypoints/streaming/`) | WebSocket JSON + binary fMP4 | `GenerationRequest` per segment, session-scoped | Server-held; per-GPU continuation cache | +| Dynamo native backend (`ai-dynamo/dynamo/components/src/dynamo/fastvideo/`) | Dynamo RPC endpoint | `NvCreateVideoRequest` ↔ adapter ↔ `GenerationRequest` | Aggregated today; future disaggregated via `ContinuationState` | + +**FastVideo does NOT host any Dynamo code.** The full backend package +(`args.py`, `main.py`, `backend.py`, `register.py`, `health_check.py`) +lives entirely in the Dynamo repo at `components/src/dynamo/fastvideo/`, +matching the vllm/sglang pattern. FastVideo's only obligation is the +stable public Python API. + +PR 7.10 lands the FastVideo-side contract. Dynamo backend code lives in +ai-dynamo/dynamo (next iteration of #7544 reopens against PR 8 reference +docs). + +## Target package layout + +``` +fastvideo/entrypoints/ +├── openai/ # existing: stateless HTTP POST +├── streaming/ # NEW: session WebSocket +│ ├── server.py # FastAPI + WebSocket entry +│ ├── session.py # session lifecycle, state machine +│ ├── session_store.py # typed session state + snapshot/hydrate +│ ├── protocol.py # JSON WebSocket message schemas +│ ├── stream.py # fMP4 encoding (av_fmp4 mode) +│ ├── gpu_pool.py # subprocess workers (PR 7.6) +│ ├── worker.py # per-GPU worker loop +│ ├── continuation.py # typed LTX2 state payload +│ ├── session_init_image.py +│ ├── session_logger.py +│ ├── mock_server.py +│ ├── prompt/ +│ │ ├── enhancer.py # provider-agnostic prompt ops +│ │ ├── rewrite.py +│ │ ├── safety.py # optional fasttext +│ │ └── providers/ +│ │ ├── base.py # LLMProvider protocol +│ │ ├── cerebras.py +│ │ ├── cerebras_ifm.py +│ │ └── groq.py +│ └── router/ +│ ├── main.py +│ └── registry.py +├── cli/ +└── video_generator.py +``` + +## Typed config integration + +`ServeConfig` gets an optional `streaming: StreamingConfig | None`: + +```python +@dataclass +class StreamingConfig: + session_timeout_seconds: int = 300 + generation_segment_cap: int = 6 + stream_mode: Literal["av_fmp4", "legacy_jpeg"] = "av_fmp4" + warmup: WarmupConfig = field(default_factory=WarmupConfig) + pool: GpuPoolConfig = field(default_factory=GpuPoolConfig) + prompt: PromptEnhancerConfig | None = None + safety: PromptSafetyConfig | None = None + +@dataclass +class GpuPoolConfig: + num_workers: int | None = None # default: CUDA_VISIBLE_DEVICES count + enable_audio_reencode: bool = True + conditioning_num_frames: int = 9 + conditioning_end_offset: int = 0 + +@dataclass +class PromptEnhancerConfig: + provider: Literal["cerebras", "groq"] = "cerebras" # cerebras_ifm pending + model: str = "gpt-oss-120b" + timeout_ms: int = 20000 + system_prompt_dir: str | None = None # hot-reloadable + +@dataclass +class PromptSafetyConfig: + enabled: bool = False + classifier_path: str | None = None +``` + +## `build_app` route contract — open follow-up + +Today `fastvideo.entrypoints.streaming.server.build_app` exposes only: + +- `GET /health` +- `WS /v1/stream` + +The Dreamverse Next.js shell expects these additional routes that the +upstream plan (and Dreamverse FE today) require: + +| Route | Owner per upstream plan | Status | +|---|---|---| +| `GET /healthz` | Streaming-server-side health (FastVideo) | 🔴 NOT YET MIGRATED | +| `GET /readyz` | Streaming-server-side health (FastVideo) | 🔴 NOT YET MIGRATED | +| `GET /status` | Streaming-server-side health (FastVideo) | 🔴 NOT YET MIGRATED | +| `GET /curated-presets` | Operator-side surface (Dreamverse) | 🟡 stays in Dreamverse, FE feature-detects | +| `POST /curated-presets/append` | Operator-side surface (Dreamverse) | 🟡 stays in Dreamverse | +| `GET /prompt-system-config` | Operator-side surface (Dreamverse) | 🟡 stays in Dreamverse | +| Devtools routes | Dreamverse-only | 🟡 stays in Dreamverse | + +Until the three health routes migrate into FastVideo's `build_app`, the +`BE_FLAVOR=fastvideo` flavor of `launch_demo.sh` is a "diagnostic" flavor +only (verifies typed serve-config path) — not FE-compatible. See +[open-threads.md](open-threads.md) follow-up #1. + +The streaming-upstream plan listed `/healthz`, `/readyz`, `/status`, +`/ws` as the contract that the upstream of `realtime/` → `streaming/` +must preserve. They were deferred from PR 7.5's MVP. + +## PR 7.5 status — open as #1251 + +Single-generator WebSocket end-to-end shipped (8 commits): + +1. `feat(streaming): protocol schemas + session state machine` +2. `feat(streaming): fMP4 encoder + session init-image persistence` +3. `feat(streaming): single-generator WebSocket server entry` +4. `test(streaming): server lifecycle + protocol + fMP4 coverage` +5. `docs(streaming): server contract spec` +6. `fix(streaming): restore missing-streaming-block guard + retire stub-era test` +7. `simplify(streaming): review follow-ups (idle timeout via asyncio.wait_for, _send_error helper, _cleanup_session, Protocol-typed generator, cleanup-on-disconnect)` +8. `fix(streaming): enforce idle timeout on receive_json + flag generator-cancellation gap (TODO → PR 7.10)` + +Deferred TODOs (intentionally) blocking on PR 7.10: + +- **Per-step progress events** — only terminal `step_complete` today; + needs `generate_async` for per-step `VideoProgressEvent` emission. +- **Mid-segment cancellation on client disconnect** — TODO marker in + `server.py` near `pool.run`. Needs `generate_async`'s cancellation + propagation. + +## PR 7.6 status — branch ready, not yet PR'd + +`will/api_7.6` (5 commits, rebased on 7.5): + +1. `feat [7.6/n]: GPU pool manager with typed worker boundary` +2. `refactor [7.6/n]: route streaming server through GpuPool` +3. `test [7.6/n]: GPU pool coverage (in-process + subprocess)` +4. `fix(streaming): restore missing asyncio import in server` (rebase fixup) +5. `feat(streaming): extract worker.py and add two-segment warmup` + +Tests: 17/17 gpu_pool tests + 89/89 streaming tests green. + +Ships: +- `GpuPool` ABC + `InProcessGpuPool` + `SubprocessGpuPool` + + `PoolAssignment` / `PoolHealth` / `PoolAcquireTimeout` +- `worker.py` — per-GPU `worker_main` and two-segment warmup helper +- Subprocess startup uses typed `GeneratorConfig`, NOT flat kwargs +- Session-to-GPU binding with timeout + queue for contention +- Two-segment startup warmup per worker (segment 1 fresh + segment 2 + with returned `ContinuationState` so both compile branches are primed) +- `SessionStore` (from PR 7) wired for per-GPU continuation cache + +Deferred to PR 7.10: + +- **Audio re-encode (`LTX2AudioEncoder`, `AudioProcessor`)**: internal + `_re_encode_audio` runs *inside* the per-step streaming loop + (`_stream_av_fmp4_events` / `do_step_ltx2`). The whole-segment + `pool.run()` path PR 7.6 ships doesn't need it. Re-encode is a + per-step streaming concern that belongs with `generate_async`. +- **Deprecate `VideoGenerator.from_pretrained(**flat_kwargs)`**: belongs + with PR 13 cleanup. + +## PR 7.10 — the unlock PR + +PR 7.10 adds `generate_async` and closes three open threads +simultaneously: + +- Q-5 / D-5: audio re-encode for cross-segment continuity +- Q-9: Dynamo progress passthrough +- PR 7.5's mid-segment cancellation TODO (client disconnect → + `asyncio.CancelledError` → GPU work stops) + +Plus health-check helper: + +```python +def default_health_check_request(self) -> GenerationRequest: ... +# Returns 256x256, 8 frames, 1 step. Lets Dynamo's +# FastVideoHealthCheckPayload.to_dict() produce a Dynamo +# health_check_payload kwarg without knowledge of FastVideo internals. +``` + +Stable public exports: + +```python +from fastvideo import VideoGenerator +from fastvideo.api import ( + GenerationRequest, SamplingConfig, ContinuationState, + VideoResult, VideoEvent, + VideoProgressEvent, VideoPartialEvent, VideoFinalEvent, +) +``` + +Streaming server (PR 7.5) gets rewired to consume `generate_async` +directly — no wrapper duplication. + +## Dynamo request/response mapping + +``` +NvCreateVideoRequest -> fastvideo.api.GenerationRequest + prompt -> sampling.prompt + size="WxH" -> sampling.width, sampling.height + seconds -> seconds * nvext.fps -> sampling.num_frames + input_reference -> input.image_path | input.video_path + nvext.fps -> sampling.fps + nvext.num_frames -> sampling.num_frames (overrides seconds*fps) + nvext.num_inference_steps -> sampling.num_inference_steps + nvext.guidance_scale -> sampling.guidance_scale + nvext.seed -> sampling.seed + nvext.negative_prompt -> sampling.negative_prompt + response_format -> (handled at adapter's output stage) + +VideoFinalEvent -> NvVideosResponse + video_bytes -> data[0].b64_json (response_format=b64_json) + uploaded URL -> data[0].url (response_format=url) + metadata.inference_time_s -> inference_time_s + continuation_state -> (reserved for future disaggregation) +``` + +## Open questions + +1. **Router placement** — in-tree at `fastvideo/entrypoints/streaming/router/` + (current implementation per PR 7.9) or separate package + `fastvideo-router/` / `fastvideo/contrib/router/`. Effectively + resolved in-tree by the PR 7.9 implementation. +2. **Session ID authority** — server-generated UUID; accept externally + provided session ID only for resume flows. +3. **Disaggregation readiness contract test** — should PR 7.10 validate + `ContinuationState` survives round-trip through Dynamo-style RPC + (pickle or JSON), even though Dynamo isn't using it today? + Recommended: yes; cheap regression guard. +4. **Dynamo progress/status passthrough** — should PR 7.10's handler + contract emit intermediate `NvVideosResponse` chunks keyed off + `VideoProgressEvent`, or stay aggregated-final-only? Recommended: + stay aggregated-final for PR 7.10; revisit after Dynamo clarifies. +5. **`video_position_offset_sec` semantics** — see [decisions-log.md](decisions-log.md) + open question; needs decision before PR 7.6 emits state. +6. **`SessionStore` / `BlobStore` lifecycle** — eviction, TTL, blob-drop + on state replacement; defer to PR 7.5 design pass. diff --git a/.agents/memory/index.jsonl b/.agents/memory/index.jsonl index 9a61e2ef4f..0d3919272b 100644 --- a/.agents/memory/index.jsonl +++ b/.agents/memory/index.jsonl @@ -2,3 +2,4 @@ {"name": "evaluation-registry", "description": "Catalog of all evaluation metrics with detailed explanations, implementation status, and usage guides", "path": "evaluation-registry/README.md", "status": "draft", "trust": "medium"} {"name": "experiment-journal", "description": "Living log of all experiments with hypotheses, configs, metrics, and insights", "path": "experiment-journal/README.md", "status": "draft", "trust": "medium"} {"name": "related-work", "description": "Index of related papers, repos, and blog posts with structured comparisons to FastVideo", "path": "related-work/README.md", "status": "draft", "trust": "low"} +{"name": "dreamverse-integration", "description": "Consolidated knowledge base for the FastVideo public API refactor (PRs 0-17), LTX-2 streaming server upstream, Dreamverse migration from FastVideo-internal, and NVFP4 quantization landing", "path": "dreamverse-integration/README.md", "status": "ready", "trust": "high"} diff --git a/.agents/skills/dreamverse-deploy/SKILL.md b/.agents/skills/dreamverse-deploy/SKILL.md new file mode 100644 index 0000000000..7c15e49821 --- /dev/null +++ b/.agents/skills/dreamverse-deploy/SKILL.md @@ -0,0 +1,185 @@ +# dreamverse-deploy — redeploy migrated Dreamverse on a chosen GPU + +**Scope:** project (lives in this repo at `.agents/skills/dreamverse-deploy/`) + +**When to use:** you want to (re)launch the migrated `apps/dreamverse/` backend ++ frontend on this dev node, pinned to a specific physical GPU. Tears down +any existing deploy on the same ports first, then boots fresh and waits for +both `/readyz` and the FE root to return 200. + +**Pairs with:** [`integration-plan.md`](../../memory/dreamverse-integration/integration-plan.md) +"Local GPU4 verification hook" + [`decisions-log.md D-19`](../../memory/dreamverse-integration/decisions-log.md#d-19). + +## Prerequisites + +- Working tree on a branch that has `apps/dreamverse/` (e.g. `will/dreamverse-monorepo`) +- Local conda env at `~/miniconda3/envs/fv-main/` with `flashinfer-python`, + `cerebras-cloud-sdk`, `openai` installed (override the default path with + `DREAMVERSE_PYTHON=/path/to/python`) +- `~/.env` exporting `CEREBRAS_API_KEY`, `GROQ_API_KEY`, etc. +- pnpm installed at `/home/william5lin/.local/share/pnpm/pnpm` (or in `$PATH`) +- `gcc-13` + `g++-13` at `/usr/bin/` (workaround for nvcc gcc-15 rejection) +- **Recommended:** native ffmpeg env file at `apps/dreamverse/scripts/ffmpeg-env.sh` + (built once via `bash apps/dreamverse/scripts/install_native_ffmpeg.sh`). + When present, the deploy sources it inside the backend setsid block so the + worker spawns ffmpeg from `$HOME/opt/ffmpeg-native/bin/ffmpeg` (LTO + libx264 + + native arch) instead of the system `/usr/bin/ffmpeg`. When missing, the + deploy falls back to system ffmpeg with a warning. Set + `DREAMVERSE_REQUIRE_NATIVE_FFMPEG=true` to make the missing env file a hard + failure. + +If any required prereq is missing, the script fails fast with a clear message. + +## Usage + +```bash +# Deploy on GPU 4 with default ports (backend 8009, FE 5274) — torch.compile +# and warmup are both OFF by default so first-segment cold start is ~45s +# instead of ~3-4min. +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh 4 + +# Deploy on GPU 6 with custom ports +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh 6 8089 5275 + +# Deploy on GPU 0 with warmup enabled +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --warmup 0 + +# Deploy with torch.compile enabled (max-autotune; first segment ~3-4min, +# subsequent segments save ~3s — only worth it for benchmarking) +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --torch-compile 4 + +# Deploy with both warmup AND torch.compile enabled +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --warmup --torch-compile 4 + +# Flags can appear before, between, or after positional args +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh 4 8089 5275 --warmup +``` + +### Arguments + +| Position | Name | Default | Notes | +|---|---|---|---| +| 1 | `GPU` | (required) | Physical GPU index, e.g. `4` | +| 2 | `BACKEND_PORT` | `8009` | TCP port for the FastAPI server | +| 3 | `FRONTEND_PORT` | `5274` | TCP port for the Next.js dev server | + +### Flags + +| Flag | Default | Notes | +|---|---|---| +| `--warmup` / `--no-warmup` | off | Run GPU warmup at boot (~minutes). Overrides `DREAMVERSE_WARMUP` | +| `--torch-compile` / `--no-torch-compile` | off | Enable max-autotune `torch.compile`. First segment ~3-4min when on, ~45s when off. Overrides `DREAMVERSE_TORCH_COMPILE` | +| `--nvenc` / `--no-nvenc` | off | Use `h264_nvenc` hardware encoder instead of `libx264` software. Eliminates ~1100ms/segment of CPU encoding cost (raises realtime ratio from ~0.78x → ≥1.0x, eliminating inter-segment buffer-drain stutter). Requires native ffmpeg built with `--enable-nvenc` (the install script's default since the NVENC update). Hard-fails up-front if the binary is missing or lacks NVENC. Overrides `DREAMVERSE_NVENC` | +| `-h` / `--help` | — | Show usage | + +Flags can appear in any position relative to the positional args. Explicit flag values always win over env-var defaults. + +### Environment variables (used when no flag is given) + +| Var | Default | Purpose | +|---|---|---| +| `DREAMVERSE_WARMUP` | `false` | Same as `--warmup`/`--no-warmup`. Flag takes precedence | +| `DREAMVERSE_TORCH_COMPILE` | `false` | Same as `--torch-compile`/`--no-torch-compile`. Flag takes precedence | +| `DREAMVERSE_NVENC` | `false` | Same as `--nvenc`/`--no-nvenc`. Flag takes precedence | +| `DREAMVERSE_PYTHON` | `~/miniconda3/envs/fv-main/bin/python` | Conda env python used for prereq probes (flashinfer import). The wrapper at `apps/dreamverse/scripts/dreamverse-server` still resolves python via the `.venv` symlink, which points at the same interpreter on this dev node | +| `DREAMVERSE_REPO_ROOT` | git rev-parse | Repo root override | +| `DREAMVERSE_LOG_DIR` | `/tmp/opencode/dreamverse-deploy` | Where to write `backend.log` / `frontend.log` | +| `DREAMVERSE_REQUIRE_NATIVE_FFMPEG` | `false` | If `true`, fail when `$HOME/opt/ffmpeg-native/bin/ffmpeg` is absent | + +## What it does + +1. Validates prereqs. +2. Kills any process on the target backend/frontend ports + waits for the + target GPU to release memory (allows up to 30s for cleanup). +3. Sources `~/.env`. +4. Exports the env recipe required for boot: + - `CUDA_VISIBLE_DEVICES=` + - `FASTVIDEO_ENABLE_DEVTOOLS=1` + - `FASTVIDEO_ENABLE_STARTUP_WARMUP=` + - `FASTVIDEO_GPU_COUNT=1` + - `ENABLE_TORCH_COMPILE=<0|1 derived from DREAMVERSE_TORCH_COMPILE>` + - `CC=/usr/bin/gcc-13 CXX=/usr/bin/g++-13 CUDAHOSTCXX=/usr/bin/g++-13` + - `NVCC_PREPEND_FLAGS="-ccbin /usr/bin/gcc-13 -allow-unsupported-compiler"` + - `FASTVIDEO_FFMPEG_BIN=$HOME/opt/ffmpeg-native/bin/ffmpeg` + + `FASTVIDEO_VIDEO_CODEC=libx264` (when the native binary exists) +5. Launches the backend via `apps/dreamverse/scripts/dreamverse-server` in a + detached `setsid` session, captures PID. +6. Polls `/readyz` until 200 (max 5 min). +7. Launches the frontend via `pnpm run dev:devtools` in a detached session, + captures PID. +8. Polls FE `/` until 200 (max 60s). +9. Prints URLs, PIDs, and log paths. + +## What it does NOT do + +- Does not modify `~/.env` or the FastVideo `.venv`. +- Does not push code or commit anything. +- Does not run Playwright. Use the e2e wrapper separately: + ```bash + cd apps/dreamverse/web + PLAYWRIGHT_SKIP_WEBSERVER=1 BACKEND_URL=http://127.0.0.1:8009 \ + PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \ + NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \ + pnpm exec playwright test + ``` + The fast suite (8 specs, ~5s) runs by default; the long-running + two-segment audio-continuation spec is gated behind + `PLAYWRIGHT_LONG_RUNNING=1` (see below). + +## Long-running e2e (paired with `--warmup --torch-compile`) + +[`apps/dreamverse/web/e2e/long-running-segments.spec.ts`](../../../apps/dreamverse/web/e2e/long-running-segments.spec.ts) +drives a real two-segment session through the FE, captures every WS +frame, and asserts segments 1 AND 2 both reach `media_segment_complete` +with at least one binary fMP4 chunk per segment — the canonical +regression guard against the D-20 BrokenPipe pattern documented in +[`decisions-log.md D-20`](../../memory/dreamverse-integration/decisions-log.md#d-20). +Skipped by default. Enable with: + +```bash +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh \ + --warmup --torch-compile 4 + +cd apps/dreamverse/web +PLAYWRIGHT_SKIP_WEBSERVER=1 \ + BACKEND_URL=http://127.0.0.1:8009 \ + PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \ + NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \ + PLAYWRIGHT_LONG_RUNNING=1 \ + pnpm exec playwright test e2e/long-running-segments.spec.ts +``` + +Expected runtime: ~7-9 minutes on a B200 (torch.compile max-autotune +warm-up dominates the cold start; per-test timeout is 900s). The spec +hard-fails on any WS `error`/`step_error` frame so the BrokenPipe +regression surfaces with the actual ffmpeg/audio diagnostics rather +than an opaque "test timed out". + +## Teardown + +Stop both services without redeploying: + +```bash +# Stop services on default ports (port-pattern based) +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --stop + +# Stop AND nuke any process holding GPU N +./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --stop 4 +``` + +The redeploy path (`` mode) automatically nukes any process holding the +target GPU before launching — including orphan `multiproc_executor` worker +subprocesses left over from a parent backend that was killed without grace. +This was the failure mode of an earlier naive port-only kill: parent dies, +children survive, GPU stays full, next deploy OOMs. + +## Notes + +- The wrapper at `apps/dreamverse/scripts/dreamverse-server` is what makes + the migrated `apps/dreamverse/server/main.py` run instead of the legacy + conda-installed Dreamverse — see [decisions-log.md D-19](../../memory/dreamverse-integration/decisions-log.md#d-19) for why + this matters. +- The B200 / sm_100a NVCC flags are mandatory on this dev node because the + conda toolchain ships gcc-15, which nvcc rejects. If you're on a machine + with a supported native gcc, those exports are still safe (no-op when the + paths don't exist; the script verifies them upfront). diff --git a/.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh b/.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh new file mode 100755 index 0000000000..665aaf7155 --- /dev/null +++ b/.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env bash +# See ../SKILL.md for full usage. + +set -euo pipefail + +if [[ "${1:-}" == "--stop" ]]; then + for pat in 'apps/dreamverse/server/main.py' 'main.py --host 0.0.0.0 --port' 'next dev --port' 'next-server (v'; do + pkill -KILL -f "${pat}" 2>/dev/null || true + done + if [[ -n "${2:-}" ]] && [[ "${2}" =~ ^[0-9]+$ ]]; then + gpu_uuid="$(nvidia-smi --query-gpu=index,uuid --format=csv,noheader 2>/dev/null | awk -F', ' -v g="${2}" '$1==g {print $2}')" + if [[ -n "${gpu_uuid}" ]]; then + for pid in $(nvidia-smi --query-compute-apps=pid,gpu_uuid --format=csv,noheader 2>/dev/null \ + | awk -F', ' -v u="${gpu_uuid}" '$2==u {print $1}'); do + kill -9 "${pid}" 2>/dev/null || true + done + fi + fi + sleep 2 + echo "stopped: ports may take a few seconds to free" + exit 0 +fi + +# --------------------------------------------------------------------------- +# Args +# --------------------------------------------------------------------------- + +usage() { + cat < [BACKEND_PORT] [FRONTEND_PORT] + $(basename "$0") --stop [GPU] + +Positional: + GPU Physical GPU index (required), e.g. 4 + BACKEND_PORT default 8009 + FRONTEND_PORT default 5274 + +Flags (override env vars when both set): + --warmup / --no-warmup run GPU warmup at boot (default off) + --torch-compile / --no-torch-compile + enable max-autotune torch.compile + (default off — first segment ~3-4min + when on, ~45s when off) + --nvenc / --no-nvenc use h264_nvenc hardware encoder (default + off — uses libx264 software encoder). + Requires native ffmpeg built with NVENC. + -h, --help show this help + +Env overrides: + DREAMVERSE_WARMUP 'true'|'false' (default false) + DREAMVERSE_TORCH_COMPILE 'true'|'false' (default false) + DREAMVERSE_NVENC 'true'|'false' (default false) + DREAMVERSE_REPO_ROOT default: \$(git rev-parse --show-toplevel) + DREAMVERSE_LOG_DIR default: /tmp/opencode/dreamverse-deploy + DREAMVERSE_REQUIRE_NATIVE_FFMPEG 'true'|'false' (default false) +USAGE +} + +WARMUP_OVERRIDE="" +TORCH_COMPILE_OVERRIDE="" +NVENC_OVERRIDE="" +POSITIONAL=() +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage; exit 0 ;; + --warmup) WARMUP_OVERRIDE=true; shift ;; + --no-warmup) WARMUP_OVERRIDE=false; shift ;; + --torch-compile) TORCH_COMPILE_OVERRIDE=true; shift ;; + --no-torch-compile) TORCH_COMPILE_OVERRIDE=false; shift ;; + --nvenc) NVENC_OVERRIDE=true; shift ;; + --no-nvenc) NVENC_OVERRIDE=false; shift ;; + --) shift; while [[ $# -gt 0 ]]; do POSITIONAL+=("$1"); shift; done ;; + -*) echo "error: unknown flag '$1'" >&2; usage >&2; exit 2 ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done +set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}" + +if [[ $# -lt 1 ]]; then + usage >&2 + exit 2 +fi + +GPU="${1}" +BACKEND_PORT="${2:-8009}" +FRONTEND_PORT="${3:-5274}" + +if ! [[ "${GPU}" =~ ^[0-9]+$ ]]; then + echo "error: GPU must be a non-negative integer (got '${GPU}')" >&2 + exit 2 +fi + +WARMUP="${WARMUP_OVERRIDE:-${DREAMVERSE_WARMUP:-false}}" +case "${WARMUP}" in + true|false) ;; + *) echo "error: warmup must be 'true' or 'false' (got '${WARMUP}')" >&2; exit 2 ;; +esac + +TORCH_COMPILE="${TORCH_COMPILE_OVERRIDE:-${DREAMVERSE_TORCH_COMPILE:-false}}" +case "${TORCH_COMPILE}" in + true|false) ;; + *) echo "error: torch-compile must be 'true' or 'false' (got '${TORCH_COMPILE}')" >&2; exit 2 ;; +esac +TORCH_COMPILE_FLAG=$([[ "${TORCH_COMPILE}" == "true" ]] && echo 1 || echo 0) + +NVENC="${NVENC_OVERRIDE:-${DREAMVERSE_NVENC:-false}}" +case "${NVENC}" in + true|false) ;; + *) echo "error: nvenc must be 'true' or 'false' (got '${NVENC}')" >&2; exit 2 ;; +esac + +REPO_ROOT="${DREAMVERSE_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" +LOG_DIR="${DREAMVERSE_LOG_DIR:-/tmp/opencode/dreamverse-deploy}" + +# --------------------------------------------------------------------------- +# Prereq checks +# --------------------------------------------------------------------------- + +bail() { echo "error: $*" >&2; exit 3; } + +[[ -d "${REPO_ROOT}/apps/dreamverse" ]] \ + || bail "REPO_ROOT '${REPO_ROOT}' does not contain apps/dreamverse/. Are you on a migration branch?" +[[ -x "${REPO_ROOT}/apps/dreamverse/scripts/dreamverse-server" ]] \ + || bail "wrapper script missing or not executable: apps/dreamverse/scripts/dreamverse-server" + +CONDA_ENV_PYTHON="${DREAMVERSE_PYTHON:-${HOME}/miniconda3/envs/fv-main/bin/python}" +[[ -x "${CONDA_ENV_PYTHON}" ]] \ + || bail "conda env python missing at ${CONDA_ENV_PYTHON} (set DREAMVERSE_PYTHON to override)" +"${CONDA_ENV_PYTHON}" -c 'import flashinfer' 2>/dev/null \ + || bail "flashinfer-python not installed in ${CONDA_ENV_PYTHON} (run: ${CONDA_ENV_PYTHON} -m pip install flashinfer-python --no-build-isolation)" + +PNPM="${PNPM:-/home/william5lin/.local/share/pnpm/pnpm}" +command -v "${PNPM}" >/dev/null 2>&1 || PNPM="$(command -v pnpm 2>/dev/null || true)" +[[ -n "${PNPM}" ]] && [[ -x "${PNPM}" ]] || bail "pnpm not found in PATH or at /home/william5lin/.local/share/pnpm/pnpm" + +GCC13=/usr/bin/gcc-13 +GPP13=/usr/bin/g++-13 +[[ -x "${GCC13}" ]] || bail "${GCC13} not executable (needed for nvcc workaround)" +[[ -x "${GPP13}" ]] || bail "${GPP13} not executable (needed for nvcc workaround)" + +[[ -f "${HOME}/.env" ]] || echo "warn: ${HOME}/.env missing — provider API keys may be unset" >&2 + +NATIVE_FFMPEG_BIN="${HOME}/opt/ffmpeg-native/bin/ffmpeg" +if [[ "${NVENC}" == "true" ]]; then + NATIVE_VIDEO_CODEC=h264_nvenc +else + NATIVE_VIDEO_CODEC=libx264 +fi +REQUIRE_NATIVE_FFMPEG="${DREAMVERSE_REQUIRE_NATIVE_FFMPEG:-false}" +case "${REQUIRE_NATIVE_FFMPEG}" in + true|false) ;; + *) bail "DREAMVERSE_REQUIRE_NATIVE_FFMPEG must be 'true' or 'false' (got '${REQUIRE_NATIVE_FFMPEG}')" ;; +esac +if [[ -x "${NATIVE_FFMPEG_BIN}" ]]; then + if [[ "${NVENC}" == "true" ]]; then + encoder_list="$("${NATIVE_FFMPEG_BIN}" -hide_banner -encoders 2>/dev/null || true)" + if [[ "${encoder_list}" != *h264_nvenc* ]]; then + bail "--nvenc requested but ${NATIVE_FFMPEG_BIN} was not built with NVENC. Rebuild: bash apps/dreamverse/scripts/install_native_ffmpeg.sh (with ENABLE_NVENC=1, the default)" + fi + if ! "${NATIVE_FFMPEG_BIN}" -hide_banner -loglevel error -y \ + -f lavfi -i 'color=red:size=64x64:rate=24:duration=0.2' \ + -c:v h264_nvenc -f null - >/dev/null 2>&1; then + bail "--nvenc requested but the GPU on this host has no NVENC silicon (probe failed: 'OpenEncodeSessionEx unsupported device'). Datacenter Blackwell (B200) and some H100 SKUs ship without NVENC; --nvenc only works on hosts with NVENC-capable GPUs (RTX 50-series, T4, A10, etc.)." + fi + fi + echo " native ffmpeg: ${NATIVE_FFMPEG_BIN} (codec=${NATIVE_VIDEO_CODEC})" +elif [[ "${REQUIRE_NATIVE_FFMPEG}" == "true" ]] || [[ "${NVENC}" == "true" ]]; then + bail "${NATIVE_FFMPEG_BIN} missing (required by --nvenc or DREAMVERSE_REQUIRE_NATIVE_FFMPEG=true). Run: bash apps/dreamverse/scripts/install_native_ffmpeg.sh" +else + echo "warn: ${NATIVE_FFMPEG_BIN} missing — backend will fall back to system ffmpeg (\$(command -v ffmpeg))." >&2 + echo " Build native ffmpeg with: bash apps/dreamverse/scripts/install_native_ffmpeg.sh" >&2 +fi +echo " python: ${CONDA_ENV_PYTHON}" + +mkdir -p "${LOG_DIR}" + +# --------------------------------------------------------------------------- +# Teardown anything on target ports +# --------------------------------------------------------------------------- + +echo "[1/8] killing any existing deploy on ports ${BACKEND_PORT}/${FRONTEND_PORT} and GPU ${GPU}..." + +kill_port_pid() { + local port="$1" + local pid + pid="$(ss -tlnp 2>/dev/null | grep -E ":${port}\b" | sed -nE 's/.*pid=([0-9]+).*/\1/p' | head -1 || true)" + if [[ -n "${pid}" ]]; then + kill -9 "${pid}" 2>/dev/null || true + fi +} + +for pat in "main.py --host 0.0.0.0 --port ${BACKEND_PORT}" "next dev --port ${FRONTEND_PORT}" "NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 next dev --port ${FRONTEND_PORT}"; do + pkill -KILL -f "${pat}" 2>/dev/null || true +done +kill_port_pid "${BACKEND_PORT}" +kill_port_pid "${FRONTEND_PORT}" + +gpu_uuid="$(nvidia-smi --query-gpu=index,uuid --format=csv,noheader 2>/dev/null | awk -F', ' -v g="${GPU}" '$1==g {print $2}')" +if [[ -n "${gpu_uuid}" ]]; then + for pid in $(nvidia-smi --query-compute-apps=pid,gpu_uuid --format=csv,noheader 2>/dev/null \ + | awk -F', ' -v u="${gpu_uuid}" '$2==u {print $1}'); do + if [[ -n "${pid}" ]] && [[ "${pid}" != "$$" ]]; then + cmd="$(ps -p "${pid}" -o comm= 2>/dev/null || true)" + kill -9 "${pid}" 2>/dev/null && echo " killed GPU${GPU} pid=${pid} (${cmd:-?})" || true + fi + done +fi + +for i in $(seq 1 30); do + free_be=true + free_fe=true + ss -tln 2>/dev/null | grep -qE ":${BACKEND_PORT}\b" && free_be=false + ss -tln 2>/dev/null | grep -qE ":${FRONTEND_PORT}\b" && free_fe=false + gpu_mem="$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sed -n "$((GPU + 1))p" || echo 99999)" + if "${free_be}" && "${free_fe}" && [[ "${gpu_mem}" -lt 1000 ]]; then + break + fi + sleep 1 +done + +gpu_mem="$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sed -n "$((GPU + 1))p" || echo 0)" +echo " ports cleared; GPU${GPU} at ${gpu_mem} MiB" + +# --------------------------------------------------------------------------- +# Launch backend +# --------------------------------------------------------------------------- + +echo "[2/8] launching backend on GPU ${GPU} port ${BACKEND_PORT} (warmup=${WARMUP} torch_compile=${TORCH_COMPILE} nvenc=${NVENC})..." + +backend_log="${LOG_DIR}/backend-gpu${GPU}.log" +: > "${backend_log}" + +setsid bash -c " + set -a + if [[ -f \"${HOME}/.env\" ]]; then + source \"${HOME}/.env\" + fi + set +a + if [[ -x \"${NATIVE_FFMPEG_BIN}\" ]]; then + export FASTVIDEO_FFMPEG_BIN=\"${NATIVE_FFMPEG_BIN}\" + export FASTVIDEO_VIDEO_CODEC=\"${NATIVE_VIDEO_CODEC}\" + fi + export DREAMVERSE_PYTHON=\"${CONDA_ENV_PYTHON}\" + export CUDA_VISIBLE_DEVICES=${GPU} + export FASTVIDEO_ENABLE_DEVTOOLS=1 + export FASTVIDEO_ENABLE_STARTUP_WARMUP=${WARMUP} + export FASTVIDEO_GPU_COUNT=1 + export ENABLE_TORCH_COMPILE=${TORCH_COMPILE_FLAG} + export CC=${GCC13} + export CXX=${GPP13} + export CUDAHOSTCXX=${GPP13} + export NVCC_PREPEND_FLAGS=\"-ccbin ${GCC13} -allow-unsupported-compiler\" + cd \"${REPO_ROOT}\" + exec ./apps/dreamverse/scripts/dreamverse-server --host 0.0.0.0 --port ${BACKEND_PORT} +" > "${backend_log}" 2>&1 < /dev/null & +disown + +# Wait briefly, then resolve actual python PID (the inner process, not the +# wrapper bash). +sleep 4 +backend_pid="$(pgrep -f "main.py --host 0.0.0.0 --port ${BACKEND_PORT}" | head -1 || true)" + +if [[ -z "${backend_pid}" ]]; then + echo "error: backend failed to spawn. Last 30 lines of log:" >&2 + tail -30 "${backend_log}" >&2 + exit 4 +fi + +echo " backend pid=${backend_pid} log=${backend_log}" + +# Poll /readyz. Deadline scales with warmup + torch.compile flags +# because warmup runs two synthetic segments before /readyz=200, and +# torch.compile max-autotune adds ~3-4min cold start to the first +# segment. Empirical worst case (warmup=true, torch_compile=true): +# ~7 min on B200; we budget 15 min for safety. +if [[ "${WARMUP}" == "true" ]] && [[ "${TORCH_COMPILE}" == "true" ]]; then + READYZ_BUDGET_SECONDS=900 +elif [[ "${WARMUP}" == "true" ]] || [[ "${TORCH_COMPILE}" == "true" ]]; then + READYZ_BUDGET_SECONDS=480 +else + READYZ_BUDGET_SECONDS=300 +fi +READYZ_POLL_INTERVAL=6 +READYZ_MAX_ITERS=$(( READYZ_BUDGET_SECONDS / READYZ_POLL_INTERVAL )) + +echo "[3/8] polling http://127.0.0.1:${BACKEND_PORT}/readyz (budget=${READYZ_BUDGET_SECONDS}s) ..." +ready=0 +for i in $(seq 1 ${READYZ_MAX_ITERS}); do + code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "http://127.0.0.1:${BACKEND_PORT}/readyz" 2>/dev/null || echo 000)" + if [[ "${code}" == "200" ]]; then + ready=1 + break + fi + if ! kill -0 "${backend_pid}" 2>/dev/null; then + echo "error: backend pid ${backend_pid} died. Last 50 lines:" >&2 + tail -50 "${backend_log}" >&2 + exit 5 + fi + sleep ${READYZ_POLL_INTERVAL} +done + +if [[ "${ready}" != "1" ]]; then + echo "error: backend did not become /readyz=200 within ${READYZ_BUDGET_SECONDS}s. Last 50 lines:" >&2 + tail -50 "${backend_log}" >&2 + exit 5 +fi + +echo "[4/8] backend /readyz OK" + +# --------------------------------------------------------------------------- +# Launch frontend +# --------------------------------------------------------------------------- + +echo "[5/8] launching frontend on port ${FRONTEND_PORT}..." + +frontend_log="${LOG_DIR}/frontend-port${FRONTEND_PORT}.log" +: > "${frontend_log}" + +# Resolve dev script: dev:devtools forces port 5274 + devtools env. If the +# requested port differs, run `next dev --port` directly with devtools env. +fe_cmd="run dev:devtools" +if [[ "${FRONTEND_PORT}" != "5274" ]]; then + fe_cmd="exec next dev --port ${FRONTEND_PORT}" +fi + +setsid bash -c " + cd \"${REPO_ROOT}/apps/dreamverse/web\" + export NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 + export BACKEND_URL=http://127.0.0.1:${BACKEND_PORT} + export BACKEND_HOST=127.0.0.1 + export BACKEND_PORT=${BACKEND_PORT} + exec '${PNPM}' ${fe_cmd} +" > "${frontend_log}" 2>&1 < /dev/null & +disown + +sleep 4 +frontend_pid="$(pgrep -f "next dev --port ${FRONTEND_PORT}" | head -1 || true)" +if [[ -z "${frontend_pid}" ]]; then + echo "error: frontend failed to spawn. Last 30 lines:" >&2 + tail -30 "${frontend_log}" >&2 + exit 6 +fi + +echo " frontend pid=${frontend_pid} log=${frontend_log}" + +# Poll FE root +echo "[6/8] polling http://127.0.0.1:${FRONTEND_PORT}/ ..." +fe_ready=0 +for i in $(seq 1 30); do + code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "http://127.0.0.1:${FRONTEND_PORT}/" 2>/dev/null || echo 000)" + if [[ "${code}" == "200" ]]; then + fe_ready=1 + break + fi + if ! kill -0 "${frontend_pid}" 2>/dev/null; then + echo "error: frontend pid ${frontend_pid} died. Last 30 lines:" >&2 + tail -30 "${frontend_log}" >&2 + exit 7 + fi + sleep 2 +done + +if [[ "${fe_ready}" != "1" ]]; then + echo "error: frontend did not respond 200 within 60s. Last 30 lines:" >&2 + tail -30 "${frontend_log}" >&2 + exit 7 +fi + +echo "[7/8] frontend / OK" + +# --------------------------------------------------------------------------- +# Print summary +# --------------------------------------------------------------------------- + +cwd="$(readlink "/proc/${backend_pid}/cwd" 2>/dev/null || echo unknown)" +gpu_mem_now="$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sed -n "$((GPU + 1))p" || echo 0)" +ffmpeg_in_use="$(tr '\0' '\n' < "/proc/${backend_pid}/environ" 2>/dev/null | sed -n 's/^FASTVIDEO_FFMPEG_BIN=//p' | head -1)" +[[ -z "${ffmpeg_in_use}" ]] && ffmpeg_in_use="$(command -v ffmpeg 2>/dev/null || echo '') (system fallback)" + +cat <.md`: - -```markdown ---- -title: -source: <URL or citation> -type: paper | repo | blog -date_indexed: <ISO-8601> -tags: [world-model, distillation, evaluation, ...] ---- - -## Summary -<1-2 paragraph summary.> - -## Key Differences from FastVideo -- <comparison points> - -## Actionable Insights -- <what we could adopt or adapt> -``` - -### 3. Update the catalog - -If `.agents/memory/related-work/_catalog.md` exists, append the new entry. -If not, create it: - -```markdown -# Related Work Catalog - -| Slug | Title | Type | Tags | Date | -|------|-------|------|------|------| -| <slug> | <title> | <type> | <tags> | <date> | -``` - -## Outputs -- New file in `.agents/memory/related-work/<slug>.md`. -- Updated catalog. - -## Example Usage - -``` -Index the Self-Forcing paper: - - source: https://arxiv.org/abs/2406.xxxxx - type: paper - tags: [world-model, self-forcing, distillation] -``` - -## References -- `.agents/memory/related-work/README.md` — schema documentation - -## Changelog -| Date | Change | -|------|--------| -| 2026-03-02 | Initial version | diff --git a/.agents/skills/index.jsonl b/.agents/skills/index.jsonl index 8b84d134ed..9bfb8cebb0 100644 --- a/.agents/skills/index.jsonl +++ b/.agents/skills/index.jsonl @@ -3,8 +3,6 @@ {"name": "summarize-run", "description": "Extract a W&B run summary into a structured experiment report", "path": "summarize-run/SKILL.md", "status": "draft", "trust": "low"} {"name": "log-experiment", "description": "Append or update an experiment entry in the experiment journal", "path": "log-experiment/SKILL.md", "status": "draft", "trust": "low"} {"name": "evaluate-video-quality", "description": "Evaluate generated video quality using available metrics (SSIM, loss trajectory, caption consistency)", "path": "evaluate-video-quality/SKILL.md", "status": "draft", "trust": "low"} -{"name": "index-related-work", "description": "Ingest a paper or repository into the related work index", "path": "index-related-work/SKILL.md", "status": "draft", "trust": "low"} -{"name": "search-related-work", "description": "Query the related work index for relevant papers, repos, or comparisons", "path": "search-related-work/SKILL.md", "status": "draft", "trust": "low"} {"name": "seed-ssim-references", "description": "Run a new or updated fastvideo/tests/ssim/ test on Modal, pull generated videos, and upload them to FastVideo/ssim-reference-videos so the test has a regression baseline", "path": "seed-ssim-references/SKILL.md", "status": "draft", "trust": "low"} {"name": "reseed-ssim-references", "description": "Re-seed (overwrite) HF reference videos for an existing fastvideo/tests/ssim/ test and a single model id on Modal L40S. Always backs up current refs first, regenerates on Modal, pauses for the user to eyeball before-vs-after, then uploads with --force scoped to --model-id. Sister skill to seed-ssim-references; use when intentional code change has invalidated existing refs", "path": "reseed-ssim-references/SKILL.md", "status": "draft", "trust": "low"} {"name": "decompose-pipeline-pr", "description": "Decompose an oversized FastVideo pipeline PR into a stack of independently-reviewable PRs. Tiers the diff by blast radius (invisible / dead code / cross-cutting infra / activation), produces a branch graph and worktree bootstrap, drafts the AGENTS.md manifest, flags missing tests on cross-cutting infra changes, and extracts lessons from the PR body. Worked example: PR #1280 daVinci-MagiHuman (9.8k LOC) decomposed into 10 stacked PRs.", "path": "decompose-pipeline-pr/SKILL.md", "status": "tested", "trust": "medium"} diff --git a/.agents/skills/search-related-work/SKILL.md b/.agents/skills/search-related-work/SKILL.md deleted file mode 100644 index 49ecaf9dc8..0000000000 --- a/.agents/skills/search-related-work/SKILL.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -name: search-related-work -description: Query the related work index for relevant papers, repos, or comparisons ---- - -# Search Related Work - -## Purpose -Search through `.agents/memory/related-work/` to find indexed papers, repos, -or blog posts relevant to a query. Use this when you need to understand how -other work compares to FastVideo's approach, or when looking for techniques -to adopt. - -## Prerequisites -- The related work index has entries (`.agents/memory/related-work/*.md`). - -## Inputs - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `query` | Yes | Natural language query | -| `tags` | No | Filter by tags (e.g., `[distillation, evaluation]`) | -| `type` | No | Filter by type (`paper`, `repo`, `blog`) | - -## Steps - -### 1. Search the index - -Use grep-based search through `.agents/memory/related-work/`: - -```bash -# Search by content -grep -rl "<query>" .agents/memory/related-work/ - -# Search by tags (in frontmatter) -grep -l "tags:.*<tag>" .agents/memory/related-work/*.md -``` - -### 2. Rank results - -For each matching file: -1. Read the file. -2. Score relevance to the query based on: - - Title match - - Tag match - - Content match (summary, differences, insights) -3. Return top results. - -### 3. Format output - -```markdown -## Related Work Search: "<query>" - -### 1. <Title> (relevance: high) -- **Source**: <URL> -- **Tags**: <tags> -- **Key insight**: <most relevant excerpt> -- **File**: `.agents/memory/related-work/<slug>.md` - -### 2. <Title> (relevance: medium) -... -``` - -## Outputs -- Ranked list of relevant related work entries with excerpts. - -## Example Usage - -``` -Search for work related to video quality evaluation metrics: - - query: "video generation quality evaluation metrics" - tags: [evaluation] -``` - -## References -- `.agents/memory/related-work/README.md` — index schema - -## Changelog -| Date | Change | -|------|--------| -| 2026-03-02 | Initial version | diff --git a/.agents/workflows/sync-dashboard.md b/.agents/workflows/sync-dashboard.md deleted file mode 100644 index 4ec13a2548..0000000000 --- a/.agents/workflows/sync-dashboard.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -description: Synchronize the STATUS.md dashboard by scanning .agents/ directories ---- - -# Sync Dashboard - -Updates `.agents/STATUS.md` by scanning the skills, workflows, memory, lessons, -and exploration directories to reflect what actually exists on disk. - -## When to Use - -- After adding, removing, or renaming any file in `.agents/`. -- Periodically (e.g., at end of each conversation session). -- When the dashboard feels out of date. - -## Steps - -### 1. Scan directories - -List all files in each directory: - -```bash -echo "=== Skills ===" -ls -1 .agents/skills/*.md 2>/dev/null | grep -v SKILL_TEMPLATE - -echo "=== Workflows ===" -ls -1 .agents/workflows/*.md 2>/dev/null - -echo "=== Memory ===" -ls -1 .agents/memory/*.md 2>/dev/null -ls -1 .agents/memory/related-work/*.md 2>/dev/null | grep -v README - -echo "=== Lessons ===" -ls -1 .agents/lessons/*.md 2>/dev/null | grep -v README - -echo "=== Exploration ===" -ls -1 .agents/exploration/*.md 2>/dev/null | grep -v README -``` - -### 2. Compare with STATUS.md - -For each file found: -- If it's in STATUS.md → leave it (preserve status/trust/tested fields). -- If it's NOT in STATUS.md → add it with status `🔴 Stub`, trust `None`, tested `❌`. - -For each entry in STATUS.md: -- If the file no longer exists → mark it as `❌ Removed` or delete the row. - -### 3. Update counts - -Recalculate the summary table at the top: -- Count files per category. -- Count by status (Ready, Draft, Stub). - -### 4. Update timestamp - -Set `_Last synced: <current date>_` at the top of STATUS.md. - -### 5. Review - -Read through the updated STATUS.md for accuracy. Flag anything that looks wrong. - -## Notes - -- Do NOT change trust levels during sync — those are set manually after testing. -- Do NOT change status during sync — status changes require actual validation. -- This workflow only handles structural sync (file existence), not content review. diff --git a/.gitignore b/.gitignore index 222c3c3a88..9f69c89a21 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,42 @@ preprocess_output_text/ # Next.js / Node artifacts under ui/: see ui/.gitignore +# Next.js / Node artifacts under apps/dreamverse/web/ +apps/dreamverse/web/node_modules/ +apps/dreamverse/web/.next/ +apps/dreamverse/web/out/ +apps/dreamverse/web/coverage/ +apps/dreamverse/web/test-results/ +apps/dreamverse/web/playwright-report/ +apps/dreamverse/web/.env.local +apps/dreamverse/web/.env.development.local +apps/dreamverse/web/.env.test.local + +# Generated by apps/dreamverse/scripts/install_native_ffmpeg.sh — host-specific +apps/dreamverse/scripts/ffmpeg-env.sh +apps/dreamverse/web/.env.production.local + +# Unignore migrated Dreamverse product assets — root .gitignore globally +# ignores *.png/*.jpg/*.mp4/*.gif, but apps/dreamverse/web/public/ MUST +# be tracked (logo, icons, k2.png, etc.). +!apps/dreamverse/web/public/**/*.png +!apps/dreamverse/web/public/**/*.jpg +!apps/dreamverse/web/public/**/*.jpeg +!apps/dreamverse/web/public/**/*.mp4 +!apps/dreamverse/web/public/**/*.gif +!apps/dreamverse/web/prompts/**/*.png +!apps/dreamverse/web/prompts/**/*.jpg +!apps/dreamverse/web/prompts/**/*.jpeg +!apps/dreamverse/web/prompts/**/*.mp4 +!apps/dreamverse/web/prompts/**/*.gif +!apps/dreamverse/server/prompts/**/*.png +!apps/dreamverse/server/prompts/**/*.jpg +!apps/dreamverse/server/prompts/**/*.jpeg +!apps/dreamverse/server/prompts/**/*.mp4 +!apps/dreamverse/server/prompts/**/*.gif +!apps/dreamverse/gpu-pool.svg +!apps/dreamverse/gpu-pool.drawio + .claude/ .codex/ .sisyphus/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36e742a417..91b47edd95 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ exclude: | scripts/.*| fastvideo/dataset/.*| fastvideo/models/.*| + ^apps/dreamverse/web/.*| examples/.*| \.agents/.*| .github/workflows/publish-fastvideo.yml| diff --git a/CLEANUP.md b/CLEANUP.md new file mode 100644 index 0000000000..94194e2d08 --- /dev/null +++ b/CLEANUP.md @@ -0,0 +1,182 @@ +# `.agents/` Cleanup Log — Phase 1 (Deletes Only) + +**Status:** TEMPORARY — delete this file after the cleanup is reviewed/committed. +**Date:** 2026-05-04 +**Branch:** `will/ltx2_sr_port` +**Scope:** Phase 1 of the `.agents/` cleanup plan (deletes only; no rewrites or additions). + +For the full multi-phase plan, see the prior session analysis. This file tracks +exactly what got deleted, why, and what cross-references still point at deleted +content (to fix in a future phase). + +--- + +## Deletions executed + +### Files deleted + +| Path | Size | Reason | +|---|---|---| +| `.agents/STATUS.md` | 3.85 KB | Stale dashboard, last synced 2026-03-02. Counts wrong (claimed 8 skills/4 workflows/4 memory; actual 9/5/5). References old snake_case filenames (`codebase_map.md`/`experiment_journal.md`) that don't exist. Hand-maintained derivative of `.agents/{memory,skills}/index.jsonl` — strictly redundant. | +| `.agents/exploration/pr-link-review.md` | 1.11 KB | Status: "promoted" to `.agents/skills/review-pr-link/`. Per `.agents/exploration/README.md` lifecycle, promoted exploration logs should not linger after the skill exists. | +| `.agents/workflows/sync-dashboard.md` | 1.87 KB | SOP for maintaining `STATUS.md` (which is also deleted). Contained obsolete file paths (`.agents/skills/launch-experiment.md` flat layout vs. actual `<skill>/SKILL.md` per-dir layout). Has never been run successfully (judging by stale dates everywhere). | + +### Skill directories deleted + +| Path | Size | Reason | +|---|---|---| +| `.agents/skills/index-related-work/` | 2.18 KB | Vapor-skill operating on the empty `.agents/memory/related-work/` registry. Never used (the registry has zero entries despite ~6 weeks since skill creation). Re-add when the related-work catalog gains entries. | +| `.agents/skills/search-related-work/` | 1.91 KB | Same: vapor-skill against empty registry. The skill description literally requires "The related work index has entries" as a prerequisite, and there are none. | + +**Total deleted: 5 items, ~10.9 KB.** + +### Registry updates + +| File | Change | +|---|---| +| `.agents/skills/index.jsonl` | Removed entries for `index-related-work` and `search-related-work`. Was 9 entries; now 7. | + +### Symlink hygiene + +`.agents/scripts/sync-skills.sh` was run to prune now-stale symlinks under +`.claude/skills/` that pointed at the deleted skill directories. Output captured +in the run log. + +--- + +## What was KEPT (despite being candidates) + +| Path | Why kept | +|---|---| +| `.agents/scripts/sync-skills.sh` | User explicitly requested keep. **Verified**: this script is INDEPENDENT of STATUS.md / sync-dashboard.md. It mirrors `.agents/skills/` → `.claude/skills/` via symlinks for Claude Code skill discovery. Self-contained, useful, prunes its own stale symlinks. | +| `.agents/memory/related-work/README.md` | Empty placeholder, but the schema/template is reusable. Kept for when first related-work entry is added. | +| `.agents/memory/experiment-journal/README.md` | Same: empty placeholder with template; kept for when journaling begins. | +| `.agents/lessons/README.md` | Same: empty placeholder, reusable schema. | +| `.agents/exploration/README.md` | Active template for new exploration logs. Kept. | + +--- + +## Remaining broken cross-references (FOLLOW-UP NEEDED) + +These files still reference deleted content. **NOT fixed in Phase 1** — track for +the next pass (Phase 2: rewrites/dedupe). + +### References to deleted `STATUS.md` + +| Referencing file | Action needed | +|---|---| +| `.agents/onboarding/README.md` | Quick-reference tree (line ~65) lists `STATUS.md ← dashboard: completeness & trust of all components`. Remove that line + the `ONBOARDING.md` typo (file is `README.md`). | + +### References to deleted `pr-link-review.md` + +| Referencing file | Action needed | +|---|---| +| `.agents/memory/dreamverse-integration/state.md` | "Untracked but present" / "Source docs (archived)" sections still mention `pr-link-review.md` as kept. Update to reflect deletion. | +| `.agents/memory/dreamverse-integration/README.md` | Same — table row for `pr-link-review.md` says "kept in exploration dir". Update or remove the row. | + +### References to deleted skills (`index-related-work`, `search-related-work`) + +| Referencing file | Action needed | +|---|---| +| `.agents/memory/related-work/README.md` | Says "Use the `index-related-work` skill". Either remove that hint or note "skill removed; re-add when registry has entries". | +| `.agents/workflows/evaluation-development.md` | Step 1 says "Search `.agents/memory/related-work/` for existing evaluation approaches" — that's still valid (manual search). No change needed. | + +### References to deleted `sync-dashboard.md` + +| Referencing file | Action needed | +|---|---| +| `.agents/memory/evaluation-registry/README.md` | Doesn't reference sync-dashboard directly. No change. | +| `.agents/STATUS.md` | Already being deleted. | + +--- + +## Other registry inconsistencies discovered (NOT FIXED in Phase 1) + +While editing `.agents/skills/index.jsonl`, two skill directories were found +that exist on disk but **are not registered** in `index.jsonl`: + +| Skill dir | Status | Why missing from index | +|---|---|---| +| `.agents/skills/diagnose-ssim-failure/` | Untracked locally; NOT on `origin/main`. 12.3 KB SKILL.md + `scripts/compare_latent_pt.py`. Recent mtime (2026-05-01). | Created in a prior session but the registration step was skipped. | +| `.agents/skills/review-pr-link/` | Untracked locally; NOT on `origin/main`. 2.9 KB SKILL.md + `scripts/prepare_pr_review.py` + `agents/openai.yaml`. The promotion target of the deleted `pr-link-review.md` exploration log. | Skipped registration when promoted from exploration log. | + +Both skills are functional and exposed via `sync-skills.sh` symlinks (just verified in +`.claude/skills/`), but agents reading `index.jsonl` to discover skills will miss them. + +**Action for Phase 2**: Add entries to `.agents/skills/index.jsonl` for both, +likely with `trust: medium` since they have working scripts and recent use. + +--- + +## Skill registry parity check + +After Phase 1, `.agents/skills/` contains 9 directories but `index.jsonl` lists 7: + +| In `index.jsonl` | On disk | +|---|---| +| ✓ launch-experiment | ✓ launch-experiment/ | +| ✓ monitor-experiment | ✓ monitor-experiment/ | +| ✓ summarize-run | ✓ summarize-run/ | +| ✓ log-experiment | ✓ log-experiment/ | +| ✓ evaluate-video-quality | ✓ evaluate-video-quality/ | +| ✓ seed-ssim-references | ✓ seed-ssim-references/ | +| ✓ reseed-ssim-references | ✓ reseed-ssim-references/ | +| ❌ (missing) | ⚠ diagnose-ssim-failure/ | +| ❌ (missing) | ⚠ review-pr-link/ | + +`.claude/skills/` symlinks (the runtime-discoverable surface) include all 9 ✓. + +--- + +## Phase 2+ items (NOT executed in this session) + +For future cleanup sessions, the prior plan identified: + +**Phase 2 (rewrites)**: +- Rewrite `.agents/onboarding/worldmodel-training/README.md` to drop ~50% structural duplication with `codebase-map/README.md` +- Refresh `.agents/memory/codebase-map/README.md` (last updated 2026-03-08; missing `fastvideo/api/`, `fastvideo/entrypoints/streaming/`, etc.) +- Refresh `.agents/memory/evaluation-registry/README.md` (last updated 2026-03-02; references old `evaluation_registry.md` filename) +- Merge `.agents/workflows/experiment-journaling.md` into `experiment-lifecycle.md` (one SOP per workflow) +- Fix the broken cross-references listed above + +**Phase 3 (additions)**: +- `fastvideo/api/AGENTS.md` +- `fastvideo/entrypoints/AGENTS.md` +- `tests/AGENTS.md` (top-level, distinct from `fastvideo/tests/AGENTS.md`) +- `fastvideo/distributed/AGENTS.md` +- `examples/AGENTS.md` +- `docs/AGENTS.md` +- `benchmarks/AGENTS.md` + +**Phase 4 (registry)**: +- Add `.agents/workflows/index.jsonl` +- Standardize all three index.jsonl schemas + +**Phase 5 (skills quality)**: +- Promote tested skills (`seed-ssim-references`, `reseed-ssim-references`, `diagnose-ssim-failure`, `review-pr-link`) from `trust: low` to `trust: medium` +- Mark untested skills (`launch-experiment`, `monitor-experiment`, `summarize-run`, `log-experiment`, `evaluate-video-quality`) explicitly with their gating prerequisite (e.g. "operates on empty registry") + +--- + +## Recovery + +All deletions are local (`will/ltx2_sr_port`, not committed). To restore any +deleted file: + +```bash +git restore --source=HEAD .agents/STATUS.md +git restore --source=HEAD .agents/exploration/pr-link-review.md +git restore --source=HEAD .agents/workflows/sync-dashboard.md +git restore --source=HEAD .agents/skills/index-related-work/SKILL.md +git restore --source=HEAD .agents/skills/search-related-work/SKILL.md +``` + +--- + +## When to delete THIS file + +Once: +1. The Phase 1 deletions are committed (or merged), AND +2. Phase 2 (broken cross-reference cleanup) is also committed, + +remove this file. Its purpose is transient bookkeeping for a multi-phase cleanup. diff --git a/CO-AUTHORS.md b/CO-AUTHORS.md new file mode 100644 index 0000000000..d25a1d76d1 --- /dev/null +++ b/CO-AUTHORS.md @@ -0,0 +1,88 @@ +# Co-Authors — `will/ltx2_sr_port` Stack + +**Status:** PERMANENT — keep around as the source of truth for who collaborated on this work, even after the stack merges. +**Last updated:** 2026-05-04 + +This file documents the human co-authors credited on every commit in the +`will/ltx2_sr_port` stack and its 10 split PRs. The 4 collaborators below +worked on the FastVideo-internal precursor of this code (LTX-2 streaming +server, NVFP4 wire-up, GPU pool, prompt enhancer, etc.) and are credited as +co-authors on the public-side upstream commits via Git's standard +[`Co-authored-by`](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors) +trailer convention. + +The trailers are added to every commit on `will/ltx2_sr_port` (see +[`STACK.md`](STACK.md)), which means GitHub will: + +- Show the 4 co-authors on every commit detail page +- Show them on the merge commit / squash commit summary +- Display their avatars in the PR's "Contributors" sidebar +- Surface them in [`/contributors`](https://github.com/hao-ai-lab/FastVideo/contributors) once the stack lands + +## Co-author roster + +| GitHub user | Real name | GitHub ID | Trailer email | +|---|---|---|---| +| [`@Davids048`](https://github.com/Davids048) | Junda (David) Su | 90978028 | `90978028+Davids048@users.noreply.github.com` | +| [`@RandNMR73`](https://github.com/RandNMR73) | Matthew Noto | 99706358 | `99706358+RandNMR73@users.noreply.github.com` | +| [`@XOR-op`](https://github.com/XOR-op) | (unset) | 17672363 | `17672363+XOR-op@users.noreply.github.com` | +| [`@jzhang38`](https://github.com/jzhang38) | Zhang Peiyuan | 42993249 | `42993249+jzhang38@users.noreply.github.com` | + +## Why no-reply emails + +GitHub's `<id>+<username>@users.noreply.github.com` form is the most reliable +way to link a `Co-authored-by` trailer to a GitHub account. It: + +- Always works regardless of whether the user has a public verified email +- Survives the user changing their primary email +- Doesn't expose anyone's personal email to git history +- Is the format GitHub itself produces when you click "Add co-author" in the + web UI + +(All 4 collaborators have this email already used in `FastVideo-internal` +git history, verified via `git log --all` on that repo.) + +## Trailer block (copy-paste ready) + +The trailers added to every commit on `will/ltx2_sr_port`: + +``` +Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com> +Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com> +Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com> +Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com> +``` + +## How the trailers were applied + +```bash +git rebase --exec ' + git commit --amend --no-edit \ + --trailer "Co-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com>" \ + --trailer "Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>" \ + --trailer "Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>" \ + --trailer "Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>" +' origin/main will/ltx2_sr_port +``` + +Git's `--trailer` flag is idempotent (it dedupes by the full `key: value` +string), so re-running the rebase is safe and won't add duplicates. + +## How to add a new co-author later + +1. Add the user to the roster table above. +2. Append their `Co-authored-by` line to the trailer block. +3. Re-run the rebase command above on `will/ltx2_sr_port` — git's + trailer dedupe handles the existing 4; the new one gets appended. +4. Re-slice all 10 split branches per [`STACK.md`](STACK.md). +5. Force-push `will/api_7.6`, `will/api_7.7`, and `will/ltx2_sr_port`. + +## What we do NOT add + +Per [`AGENTS.md`](AGENTS.md): + +> Never add any coding agent or models such as Claude (or Claude Code), GPT, +> Codex or others as a co-author in commits or PRs. + +So no `Co-authored-by: Claude <noreply@anthropic.com>` or similar. Only +human collaborators. diff --git a/apps/dreamverse/AGENTS.md b/apps/dreamverse/AGENTS.md new file mode 100644 index 0000000000..658525c623 --- /dev/null +++ b/apps/dreamverse/AGENTS.md @@ -0,0 +1,203 @@ +# Dreamverse Agent Notes + +## Repo-local skills + +- `.agents/skills/bootstrap-fastvideo-private-fork/`: temporary setup skill for + cloning `git@github.com:hao-ai-lab/FastVideo-internal.git` at + `will/rebase-nbv` into `../FastVideo-internal`, then running + `uv sync --extra server`. +- Prefer the bundled script in that skill instead of inventing a new private + FastVideo bootstrap flow. + +## Repo layout + +Current paths: + +- `apps/web/`: Next.js frontend, client-side stores, websocket event reduction, + prompt-window editing, devtools UI. +- `server/`: current Python FastAPI runtime, websocket protocol, prompt + enhancement, prompt rewrite orchestration, GPU worker lifecycle. +- `server/tests/`: backend unit and integration-oriented tests. +- `server/benchmarks/`: prompt-provider latency/token benchmarking scripts. + +Planned paths during the OSS reorg: + +- `controller/`: local control plane for provider credentials, compute + lifecycle, and proxying. +- `runtime/`: eventual rename of `server/` once the controller/runtime split is + stable. +- `providers/`: provider adapters for local, Runpod, and Modal. + +Important rule: + +- Until the split lands, treat `server/` as the authoritative runtime and keep + provider orchestration out of `apps/web`. + +## System split + +Dreamverse is moving toward a three-part local-first architecture. + +- Frontend owns local UI state, drafts, inspection tools, and user-triggered + actions. +- Controller will own local-only credentials, compute provisioning, runtime + lifecycle, and HTTP/websocket proxying. +- Runtime owns generation sessions, prompt rewrite, prompt safety, and + websocket semantics. + +The browser should only talk to the local Dreamverse process, never directly to +Modal or Runpod. + +## Current runtime responsibilities + +The runtime in `server/` is responsible for: + +- websocket session lifecycle on `/ws` +- queueing, GPU assignment, worker startup, and stream chunk emission +- seed prompt memory and the active prompt window used for generation +- prompt enhancement and prompt rewrite execution +- prompt safety checks +- persistence and reload of prompt system prompt files +- curated preset append/read routes in devtools mode +- health and readiness endpoints + +Relevant files: + +- `server/main.py`: websocket protocol, session state machine, REST routes +- `server/gpu_pool.py`: FastVideo-backed generation workers +- `server/prompt_enhancer.py`: provider clients, prompt enhancement, rewrite + execution +- `server/rewrite_prompt_payload.py`: canonical rewrite request body format +- `server/config.py`: prompt file paths, provider configuration, runtime flags + +## Frontend responsibilities + +The frontend in `apps/web/` is responsible for: + +- collecting user input and deciding whether to send raw prompts or rewrite + requests +- maintaining client-side stores for session, prompt-window, stream, rewrite, + and UI state +- rendering prompt history, playback state, devtools controls, and rewrite + inspection +- building the prompt-window snapshot sent with rewrite requests +- reducing websocket events into UI state +- showing compute status and controller-driven errors once the controller lands + +Relevant files: + +- `apps/web/src/app/page.tsx`: main orchestration, websocket connect/send paths +- `apps/web/src/lib/ws/reducer.ts`: applies normalized websocket events to + stores +- `apps/web/src/stores/promptWindow.ts`: prompt window and preset/editor state +- `apps/web/src/stores/rewrite.ts`: rewrite activity timeline and flags +- `apps/web/src/lib/prompts/promptWindowSnapshot.ts`: rewrite snapshot + normalization and padding + +## Planned controller responsibilities + +The future local controller should own: + +- local-only provider credential loading and storage +- provider selection +- runtime provisioning, reuse, shutdown, and health checks +- proxying frontend HTTP and websocket traffic to the active runtime +- surfacing provisioning, ready, failed, and idle states to the frontend +- durable local settings that should survive ephemeral remote runtimes + +The controller should not own: + +- prompt rewrite logic +- seed prompt memory +- generation queue semantics +- websocket event schemas + +## Prompt rewrite contract + +Prompt rewrite is a shared flow with a strict ownership split. + +Frontend responsibilities: + +- decide when a user action should trigger `rewrite_seed_prompts` instead of + `append_prompt` +- send `rewrite_instruction` and a snapshot of the current prompt window +- pad the rewrite snapshot to the runtime-expected segment count using + `buildRewritePromptWindowSnapshotFromPrompts(...)` +- show rewrite activity and raw LLM output in local inspection UI + +Runtime responsibilities: + +- validate and normalize `prompt_window_prompts` +- choose the rewrite system prompt and provider/model/temperature +- build the canonical LLM request body in `server/rewrite_prompt_payload.py` +- run the rewrite through `PromptEnhancer.rewrite_prompt_sequence(...)` +- apply safety filtering to rewritten prompts +- replace the authoritative seed prompt memory when rewrite succeeds +- emit `seed_prompts_updated` and `rewrite_seed_prompts_complete` + +Controller responsibilities: + +- proxy the request and response +- surface runtime availability and provider lifecycle failures + +Important rule: + +- The frontend may suggest the prompt window to rewrite, but the runtime owns + the actual rewritten rollout and the authoritative prompt window after + acceptance. + +## Rewrite modes + +There are two runtime rewrite modes: + +- edit existing rollout: when `prompt_window_prompts` is non-empty, rewrite the + current rollout while preserving segment count and ordering +- new rollout: when the prompt window is empty but there is a + `rewrite_instruction`, generate a fresh rollout + +The frontend should not emulate runtime rewrite behavior locally. It should +prepare the snapshot, send it, and display the result. + +## Prompt window ownership + +- Frontend owns editable drafts, selected preset UI, and prompt-window + inspection state. +- Runtime owns the active seed prompt memory used for actual generation. +- After any runtime event with reason `rewrite`, the frontend must replace its + prompt window from the server payload instead of keeping a locally-derived + version. + +## Devtools and persistence ownership + +Prompt config editing is runtime-owned persistence with frontend-owned forms +today. + +- Frontend loads and edits drafts through `/prompt-system-config`. +- Runtime reads and writes prompt files and reloads runtime prompt config. + +Curated presets follow the same pattern: + +- frontend submits append requests and may update local UI optimistically from + the response +- runtime persists the JSON file and resolves overlay vs fallback file paths + +During the controller reorg, avoid moving durable user settings into ephemeral +remote runtimes. Controller-owned local persistence is preferred for anything +that must survive provider restarts. + +## Editing guidance + +- Do not move rewrite logic into the frontend or controller. +- Do not make the frontend the source of truth for the generated prompt window + after rewrite. +- If you change websocket message types or payload fields in `server/main.py`, + update the reducer in `apps/web/src/lib/ws/reducer.ts` in the same change. +- If you change rewrite request shape, update both + `apps/web/src/lib/prompts/promptWindowSnapshot.ts` and + `server/rewrite_prompt_payload.py`. +- If you add controller-managed status or error payloads, keep them separate + from runtime websocket events unless there is a strong reason to merge them. +- If you change prompt file paths or devtools persistence, update `server/`, + the frontend devtools UI, and any controller-owned local persistence logic + together. +- Keep provider adapters focused on runtime lifecycle and reachability, not on + prompt or session semantics. diff --git a/apps/dreamverse/README.md b/apps/dreamverse/README.md new file mode 100644 index 0000000000..4dfb823069 --- /dev/null +++ b/apps/dreamverse/README.md @@ -0,0 +1,234 @@ +# Dreamverse + +Dreamverse is the FastVideo realtime video generation & editing platform. It lives in this monorepo under `apps/dreamverse/`. + +## Install Dreamverse + +You can install Dreamverse using one of the methods below. + +### Method 1: With uv pip + +```bash +pip install --upgrade pip +pip install uv +uv venv .venv --python 3.12 +source .venv/bin/activate +uv pip install "fastvideo[dreamverse]" +``` + +### Method 2: From source + +```bash +git clone https://github.com/hao-ai-lab/FastVideo.git +cd FastVideo + +pip install --upgrade pip +pip install uv +uv venv .venv --python 3.12 +source .venv/bin/activate +uv pip install -e ".[dreamverse]" +``` + +### Method 3: Using Docker + +```bash +git clone https://github.com/hao-ai-lab/FastVideo.git +cd FastVideo + +apps/dreamverse/docker/docker_build.sh +``` + +See `apps/dreamverse/docker/README.md` for Docker build and run option details. + +## Optional: Building FFmpeg For Better Performance + +For full streaming performance in a non-Docker install, build a custom FFmpeg +binary: + +```bash +bash apps/dreamverse/scripts/install_native_ffmpeg.sh +``` + +This builds and installs into `~/opt/ffmpeg-native/` and writes +`apps/dreamverse/scripts/ffmpeg-env.sh`. Source it before starting the backend +so Dreamverse uses the custom FFmpeg binary: + +```bash +source apps/dreamverse/scripts/ffmpeg-env.sh +dreamverse-server +``` + +Docker images already run this FFmpeg build during image creation and source the +generated environment file at container startup. The installer supports Linux +`x86_64` and `aarch64`. + +## Launch Dreamverse + +Start the backend with the installed Dreamverse commands: + +```bash +dreamverse-server --port 8009 +dreamverse-mock-server --port 8009 +``` + +## Frontend Setup + +Install the web dependencies once from the FastVideo checkout: + +```bash +cd apps/dreamverse/web +pnpm install --frozen-lockfile +``` + +The frontend package also has an npm lockfile, but the bundled launch scripts +use `pnpm`. + +## Quick Start: Local GPU + +### Start Backend + +Export the API keys used for prompt rewrite and prompt enhancement: + +```bash +export CEREBRAS_API_KEY=... +export GROQ_API_KEY=... +``` + +If you built the optional native FFmpeg binary above, source its environment +file in the same shell before starting the backend: + +```bash +source apps/dreamverse/scripts/ffmpeg-env.sh +dreamverse-server --host 0.0.0.0 --port 8009 +``` + +The Dreamverse backend defaults to `0.0.0.0:8009` and starts one GPU worker on +the first visible GPU by default. + +### Check Readiness + +In another shell, verify that the backend process is alive: + +```bash +curl http://localhost:8009/healthz +``` + +Then wait for GPU workers and startup warmup to finish: + +```bash +curl http://localhost:8009/readyz +``` + +You can also run the same readiness path with: + +```bash +BACKEND_HOST=localhost BACKEND_PORT=8009 apps/dreamverse/scripts/smoke_local.sh +``` + +If a backend is already running and you only want the script to probe it: + +```bash +DREAMVERSE_SMOKE_START_BACKEND=0 apps/dreamverse/scripts/smoke_local.sh +``` + +### Start Frontend + +Start the frontend: + +```bash +cd apps/dreamverse/web +BACKEND_HOST=localhost BACKEND_PORT=8009 pnpm run dev +``` + +Open `http://localhost:5299`. + +## Quick Start: Mock Backend (For UI development) + +The mock server emulates the Dreamverse backend protocol and streams a +synthetic FFmpeg-generated fMP4 clip, so the frontend can run without a GPU. + +```bash +dreamverse-mock-server --latency 200 --port 8009 +``` + +## Tests + +Run the focused backend tests that validate local startup wiring, config, GPU +selection, and mock-server behavior: + +```bash +pytest apps/dreamverse/dreamverse/tests/test_config.py \ + apps/dreamverse/dreamverse/tests/test_entrypoints.py \ + apps/dreamverse/dreamverse/tests/test_gpu_pool.py \ + apps/dreamverse/dreamverse/tests/test_mock_server.py -q +``` + +Run the broader Dreamverse backend suite: + +```bash +pytest apps/dreamverse/dreamverse/tests -q +``` + +Run the frontend tests: + +```bash +cd apps/dreamverse/web +pnpm test +``` + +Run the frontend e2e tests: + +```bash +cd apps/dreamverse/web +pnpm run e2e +``` + +## Troubleshooting + +`dreamverse-server` exits with an install hint + +- install the Dreamverse extra with `uv pip install -e ".[dreamverse]"` from a + source checkout, or `uv pip install "fastvideo[dreamverse]"` from PyPI. + +Prompt-provider environment variable errors + +- set `CEREBRAS_API_KEY` +- set `GROQ_API_KEY` +- direct `dreamverse-server` launches do not source `~/.env`; export the keys + in the shell or use the bundled launch scripts, which source `~/.env`. + +`/readyz` stays at `503` + +- wait for model loading and startup warmup to finish +- confirm a compatible CUDA GPU is visible to the process +- check backend logs for worker startup or warmup failures +- for a startup/debug pass without warmup, set + `FASTVIDEO_ENABLE_STARTUP_WARMUP=0` before starting the backend + +Only one GPU is used + +- this is the default local behavior +- set `FASTVIDEO_GPU_COUNT=<N>` to start N GPU worker subprocesses inside one + backend instance +- set `FASTVIDEO_GPU_COUNT=all` to start one worker for every visible GPU +- use `CUDA_VISIBLE_DEVICES` first if you need to pin the visible GPU set + +Frontend cannot connect to backend + +- confirm the backend is running on `8009`; if not, point the frontend at it + with `BACKEND_HOST=<host> BACKEND_PORT=<port> pnpm run dev` +- confirm `http://localhost:8009/healthz` responds before starting the frontend +- confirm `http://localhost:8009/readyz` returns `200` before clicking Generate +- use `apps/dreamverse/scripts/smoke_local.sh` for a repeatable local startup + check + +Mock backend fails during startup + +- install FFmpeg or set `FASTVIDEO_FFMPEG_BIN` to an FFmpeg binary +- for non-mock local GPU streaming performance, use the native FFmpeg installer + described above + +## Notes + +Dreamverse owns its backend app under `apps/dreamverse/dreamverse/`. It expects +`dreamverse-server`, not `fastvideo serve`. diff --git a/apps/dreamverse/arch.md b/apps/dreamverse/arch.md new file mode 100644 index 0000000000..03101a53bf --- /dev/null +++ b/apps/dreamverse/arch.md @@ -0,0 +1,351 @@ +# Dreamverse Architecture + +## Overview + +Dreamverse currently has two main runtime pieces: + +- `apps/web/`: Next.js frontend +- `server/`: Python FastAPI runtime + +Today, the browser talks directly to the Dreamverse runtime over HTTP and a +single websocket on `/ws`. The frontend owns UI state and interaction flow. The +server owns generation state, prompt rewrite, prompt safety, websocket session +semantics, and GPU-backed execution. + +Near-term OSS note: + +- `server/` is the current runtime implementation. +- A future `controller/` layer is planned for local-only compute management and + provider orchestration, but it does not exist yet. + +## Repo Map + +### Frontend + +- `apps/web/src/app/page.tsx`: main client orchestration, websocket connect, + init payloads, send paths, and top-level app behavior +- `apps/web/src/lib/ws/reducer.ts`: reduces normalized websocket events into + client stores +- `apps/web/src/stores/session.ts`: connection, mode, and top-level session UI +- `apps/web/src/stores/promptWindow.ts`: editable prompt window and seed prompt + UI state +- `apps/web/src/stores/rewrite.ts`: rewrite activity timeline and inspection + state +- `apps/web/src/stores/stream.ts`: playback and stream-related client state +- `apps/web/src/lib/prompts/promptWindowSnapshot.ts`: prompt-window snapshot + building for rewrite requests + +### Server + +- `server/main.py`: websocket endpoint, request handling, session state + machine, rewrite orchestration, REST routes, and stream relay +- `server/gpu_pool.py`: GPU worker processes, warmup, model loading, and + `generate_video()` calls through FastVideo +- `server/prompt_enhancer.py`: prompt enhancement, rollout rewrite execution, + provider selection, and timeout/fallback behavior +- `server/rewrite_prompt_payload.py`: canonical rewrite request payload + building +- `server/config.py`: runtime flags, prompt file paths, provider settings, and + warmup config +- `server/session_init_image.py`: validates and persists uploaded initial + images for segment 1 + +## Current Split Of Responsibility + +### Frontend owns + +- local UI state and client-side stores +- prompt drafts and prompt window editing +- websocket connection management +- deciding which user action to send: + - `session_init_v2` + - `project_init_v1` + - `append_prompt` + - `rewrite_seed_prompts` + - `simple_generate` +- showing rewrite progress, stream status, prompt history, and devtools views + +### Server owns + +- websocket session lifecycle and protocol +- GPU assignment and worker lifecycle +- the authoritative seed prompt memory used for generation +- prompt rewrite execution and prompt safety +- actual generation queue semantics +- stream chunk emission and segment lifecycle events +- prompt config and preset persistence routes +- health and readiness endpoints + +Important rule: + +- The frontend may propose prompt-window state for rewrite, but the server is + the source of truth for the rewritten rollout and the active prompt memory + used for generation. + +## End-To-End Flow + +1. The frontend opens `/ws`. +2. The frontend sends `session_init_v2` with the initial prompt-window state, + preset metadata, and current toggles. +3. The server validates init data, persists an optional initial image, acquires + a GPU slot, and emits session status such as `gpu_assigned`. +4. The server starts or resumes project generation and emits events like + `ltx2_stream_start`, `ltx2_segment_start`, media init/chunks, and completion + events. +5. The frontend reduces those websocket events into its stores and updates the + UI. +6. User actions such as appending prompts, rewriting seed prompts, or starting + a single custom clip go back to the server over the same websocket. + +## Frontend Architecture + +The frontend is store-driven. + +- `page.tsx` wires together websocket setup, send helpers, reducer + application, and top-level interaction flows. +- Store modules separate concerns like session state, rewrite state, prompt + window state, and stream state. +- The websocket reducer is responsible for turning normalized runtime events + into store updates. If the server event schema changes, the reducer must + change with it. + +The frontend is intentionally not responsible for: + +- generating rewritten prompts locally +- deciding final prompt safety outcomes +- reconstructing server session state from scratch +- inventing its own generation semantics independent of the runtime + +## Server Architecture + +The current runtime is a FastAPI app with a single long-lived websocket per +session. + +`server/main.py` manages: + +- websocket connect/init +- prompt queues +- project and segment state +- prompt enhancement/rewrite triggers +- stream relay from GPU workers to the browser +- session logging and REST endpoints + +`server/gpu_pool.py` manages: + +- model loading through FastVideo +- one or more worker processes +- startup warmup +- user join/leave commands +- `USER_STEP` execution for each segment +- continuation state between segments + +`server/prompt_enhancer.py` manages: + +- prompt enhancement for user-submitted prompts +- rollout rewrite requests for the prompt window +- provider selection and fallback across configured prompt providers +- response normalization and safety-aware failure handling + +## FastAPI Surface + +The server is a single FastAPI application created in +`server/main.py`. + +Current built-in FastAPI docs are enabled: + +- `/docs`: Swagger UI +- `/redoc`: ReDoc +- `/openapi.json`: OpenAPI schema + +The runtime also mounts static content: + +- `/server-assets`: server-local static assets rooted at `server/` +- `/`: frontend static build when one of the configured frontend static + directories exists + +## HTTP API + +The current HTTP API is small. Most realtime behavior still goes through the +websocket. + +### Core health and status routes + +- `GET /healthz` + - process liveness probe + - returns a small payload with `status`, `service`, and timestamp +- `GET /readyz` + - readiness probe + - returns `503` until prompt services are initialized and at least one GPU + worker is ready + - returns readiness and GPU pool summary fields such as ready workers, total + GPUs, warmup counts, and queue size +- `GET /status` + - returns the current GPU pool status payload from `gpu_pool` +- `GET /internal/monitor/sessions` + - internal monitoring payload for session dashboards + - includes pending session count, max available sessions, prompt provider + success counts, and timestamp + +### Prompt config routes + +- `GET /prompt-system-config` + - returns the editable prompt-system configuration currently loaded by + `PromptEnhancer` +- `POST /prompt-system-config` + - saves prompt-system configuration to disk and reloads prompt config in the + runtime + - current editable fields include: + - next-segment system prompt + - auto-extension system prompt + - rewrite-window system prompt + - rewrite-user system prompt + - rewrite model + - rewrite temperature + +### Devtools-only preset routes + +These exist only when `DEVTOOLS_ENABLED` is true in `server/config.py`. + +- `GET /curated-presets` + - returns merged curated presets, applying the local overlay file on top of + the fallback file when both exist +- `POST /curated-presets/append` + - appends a new curated preset to the overlay presets file + - validates non-empty label, normalized id, and at least two non-empty + segment prompts + +## Websocket API + +`WS /ws` is the main runtime API. + +The websocket owns: + +- session init +- project init and reset +- prompt append +- prompt rewrite +- generation toggles +- segment lifecycle events +- media stream delivery +- runtime error delivery + +The websocket is the authoritative API for realtime Dreamverse behavior. The +HTTP routes mainly support health checks, devtools persistence, and monitoring. + +## Prompt Rewrite Architecture + +Prompt rewrite is a shared flow with strict ownership boundaries. + +### Frontend responsibilities + +- collect the rewrite instruction +- build the prompt-window snapshot from current client state +- send `rewrite_seed_prompts` +- show rewrite progress, raw output, fallback state, and resulting prompt list + +### Server responsibilities + +- validate and normalize the prompt-window payload +- choose rewrite model, system prompt, timeout, and temperature +- build the canonical prompt payload in `server/rewrite_prompt_payload.py` +- execute rewrite through `PromptEnhancer` +- apply safety filtering to rewritten prompts +- replace the authoritative seed prompt memory when rewrite succeeds +- emit `seed_prompts_updated` and `rewrite_seed_prompts_complete` + +Important rule: + +- The frontend owns editable drafts. +- The server owns the accepted rollout. + +After a successful rewrite, the frontend should replace its prompt-window view +from the server payload instead of preserving a locally-derived version. + +## Prompt Modes + +There are three related prompt paths in the current system: + +### Initial rollout + +- The frontend sends seed prompts during `session_init_v2`. +- The server uses those prompts as the initial seed prompt memory. +- If the rollout starts from an empty prompt window plus an initial rewrite + instruction, the server can pause generation until rewrite completes. + +### Live append + +- The frontend sends `append_prompt`. +- The server may enhance that prompt, safety-check it, enqueue it, and use it + as the next generated segment. + +### Rewrite + +- The frontend sends `rewrite_seed_prompts`. +- The server rewrites the entire seed prompt window or generates a new rollout, + depending on the payload and current state. + +## Initial Image And Segment Handling + +The frontend currently sends `initial_image` as part of session init or +`simple_generate`. + +The server: + +- validates and persists the image +- uses it only for segment 1 when present +- keeps continuation state for later segments in the GPU worker + +This means the runtime, not the frontend, decides how segment 1 image +conditioning and later continuation conditioning are applied. + +## Websocket Contract + +The websocket is the main integration surface between UI and runtime. + +Typical incoming messages from the frontend: + +- `session_init_v2` +- `project_init_v1` +- `append_prompt` +- `rewrite_seed_prompts` +- `simple_generate` +- `set_enhancement` +- `set_auto_extension` +- `set_loop_generation` + +Typical outgoing messages from the server: + +- `gpu_assigned` +- `ltx2_stream_start` +- `ltx2_segment_start` +- `segment_prompt_source` +- `prompt_received` +- `prompt_ready` +- `prompt_enhancing` +- `seed_prompts_updated` +- `rewrite_seed_prompts_complete` +- `media_init` +- `media_segment_complete` +- `project_idle` +- `error` + +Binary websocket frames carry media chunks for playback. + +## Current And Planned Architecture + +Current architecture: + +- browser -> `apps/web` +- `apps/web` -> `server/main.py` +- `server/main.py` -> `gpu_pool.py` +- `gpu_pool.py` -> FastVideo runtime + +Planned architecture: + +- browser -> `apps/web` +- `apps/web` -> local `controller/` +- `controller/` -> local or remote Dreamverse runtime +- runtime -> FastVideo runtime + +That future controller split should not move prompt rewrite, session state, or +generation semantics out of the runtime. diff --git a/apps/dreamverse/design.md b/apps/dreamverse/design.md new file mode 100644 index 0000000000..773a0fd38a --- /dev/null +++ b/apps/dreamverse/design.md @@ -0,0 +1,594 @@ +# Dreamverse OSS Design + +## Overview + +Dreamverse should ship as a local-first open source application. + +- The browser talks only to a local Dreamverse control plane on the user's + machine. +- Provider credentials stay local to that machine. +- Dreamverse may provision compute on the user's behalf, but Dreamverse does + not host that control path as a service. + +This keeps the UX simple without turning Dreamverse into a credential-holding +hosted platform. + +## Goals + +- Support three compute modes behind one product surface: + - local GPU + - managed remote GPU via Runpod + - managed remote GPU via Modal +- Keep prompt rewrite, websocket session state, and generation behavior + consistent across providers. +- Keep provider API keys out of browser state and out of any hosted service. +- Make the existing runtime reusable as the common serving contract. +- Minimize provider-specific code and isolate it behind a narrow interface. + +## Non-goals + +- Do not make the frontend call provider APIs directly. +- Do not unify providers at the level of SSH, VM, serverless, or pod + semantics. +- Do not move prompt rewrite logic into the frontend or controller. +- Do not require remote compute for the basic product path. + +## Current State + +Today the repo contains two major pieces: + +- `apps/web/`: Next.js frontend +- `server/`: FastAPI runtime that owns websocket state, prompt rewrite, prompt + safety, and GPU-backed generation + +The current runtime already exposes useful health and streaming surfaces such +as `/healthz`, `/readyz`, `/status`, and `/ws`. + +## Target Architecture + +The target open source structure should be: + +```text +Dreamverse/ +├── apps/ +│ └── web/ # browser UI +├── controller/ # local control plane and provider lifecycle +├── runtime/ # Dreamverse websocket/generation runtime +├── providers/ # provider adapters +├── tests/ +│ ├── contract/ +│ ├── controller/ +│ └── smoke/ +└── design.md +``` + +Near-term note: + +- `server/` is the current runtime implementation. +- We can keep the code there initially and rename it to `runtime/` only after + the controller lands. + +## Trust Model + +Dreamverse is local-only for control and secrets. + +- The user launches Dreamverse on their own machine. +- Provider API keys are entered into the local app or local CLI. +- The controller uses those credentials to provision or connect to compute. +- The browser never talks to Modal or Runpod directly. +- Dreamverse-hosted infrastructure is not involved. + +This is the key reason the provider-based path is acceptable for OSS. + +## Responsibility Split + +### `apps/web` + +The frontend should own: + +- UI state, drafts, and local interaction state +- websocket event reduction into client stores +- selection of compute mode and display of cost/health/status +- local forms for provider configuration +- sending prompt requests and rewrite requests to the local controller + +The frontend should not own: + +- provider credentials after submission +- provider API calls +- runtime lifecycle +- authoritative prompt window after rewrite +- prompt safety or generation policy + +### `controller` + +The local controller should own: + +- provider credential loading and local-only storage +- compute mode selection +- provisioning, reuse, shutdown, and health monitoring of runtimes +- reverse proxying HTTP and websocket traffic from the frontend to the active + runtime +- user-visible status such as provisioning, ready, failed, and idle shutdown +- local persistence for user settings that must survive ephemeral runtimes + +The controller should not own: + +- prompt rewrite logic +- seed prompt memory semantics +- generation queue behavior +- provider-specific UI state + +### `runtime` + +The runtime should remain the authoritative owner of: + +- `/ws` session state +- prompt rewrite execution +- prompt safety +- seed prompt memory and prompt-window state used for generation +- generation orchestration and GPU worker lifecycle +- websocket event schemas + +This preserves the current model and avoids splitting state across layers. + +## Runtime Contract + +Provider abstraction should happen around a stable Dreamverse runtime contract, +not around infrastructure details. + +Minimum runtime surface: + +- `GET /healthz` +- `GET /readyz` +- `GET /status` +- `GET/POST /prompt-system-config` if devtools persists config through the + runtime +- curated preset routes if those remain runtime-backed +- `WS /ws` + +Important rule: + +- The controller only needs to know how to reach a healthy runtime. +- The runtime remains provider-agnostic. + +## Provider Abstraction + +Use a narrow provider interface: + +```python +class ComputeProvider(Protocol): + async def ensure_runtime(self, spec: RuntimeSpec) -> RuntimeHandle: ... + async def wait_until_ready(self, handle: RuntimeHandle) -> None: ... + async def stop_runtime(self, handle: RuntimeHandle) -> None: ... +``` + +`RuntimeHandle` should include: + +- `provider` +- `runtime_id` +- `base_url` +- `ws_url` +- runtime auth headers or tokens if needed +- lifecycle metadata +- cost or hardware metadata for UI display + +The controller should work only with `RuntimeHandle`, never with raw SSH hosts +or provider-specific payloads after resolution. + +## Provider Notes + +### Local + +Local mode should be the reference implementation. + +- Start the runtime as a local subprocess or connect to an already-running + local runtime URL. +- Reuse the same runtime contract as remote providers. +- Make this the first supported path and the main smoke-test target. + +### Runpod + +Runpod should be treated as pod lifecycle plus runtime reachability. + +- Prefer prepared images or templates that auto-start the Dreamverse runtime. +- Prefer exposed HTTP/TCP ports for steady-state traffic. +- Use SSH only for bootstrap fallback, diagnostics, or repair. +- Avoid a design where the controller shells into the pod for every action. + +### Modal + +Modal should be treated as deployment-based runtime hosting. + +- Wrap the Dreamverse runtime in a thin Modal entrypoint if needed. +- Reuse the same runtime behavior behind that wrapper. +- Do not model Modal as a machine that Dreamverse logs into. +- Do not force the websocket runtime into a per-request serverless handler + shape. + +## Config and Persistence + +Remote compute may be ephemeral, so mutable user configuration should not live +only inside remote runtimes. + +Keep durable state local to the user's machine unless there is a strong reason +otherwise: + +- provider selection +- provider credentials or credential references +- default hardware preferences +- editable prompt presets +- prompt system prompt overrides +- idle shutdown policy + +Runtime-local state should be treated as disposable unless explicitly synced. + +## Prompt Rewrite Ownership + +Prompt rewrite remains runtime-owned even after the controller is added. + +Frontend responsibilities: + +- collect the rewrite instruction +- build the prompt-window snapshot +- display rewrite activity and results + +Runtime responsibilities: + +- validate and normalize the prompt window +- choose the rewrite system prompt and model settings +- execute rewrite +- apply safety filtering +- replace authoritative seed prompt memory +- emit the canonical completion events + +Controller responsibilities: + +- proxy the request and response +- surface runtime availability and failure state + +This boundary should not move. + +## Recommended Rollout + +1. Finish the path reorg so docs and code agree on `apps/web`. +2. Introduce `controller/` as a local-only API/proxy process. +3. Keep `server/` as the runtime and adapt it behind the controller. +4. Add `local` provider first. +5. Add "bring your own runtime URL" as an escape hatch. +6. Add automated Runpod provisioning. +7. Add Modal deployment support. +8. Rename `server/` to `runtime/` once the split is stable. + +## Implementation Plan + +The implementation should start with the smallest milestone that gives users a +working local GPU setup without forcing the controller/provider architecture +into the first patch series. + +### Milestone 0: Make local GPU the official baseline + +Goal: + +- A user with a working `fastvideo` install can run the Dreamverse backend on a + local GPU and connect to it from `apps/web`. + +Non-goals for this milestone: + +- no controller process yet +- no provider abstraction yet +- no Runpod or Modal support yet +- no secret-management UI yet + +Reasoning: + +- `server/` already is the real local GPU runtime. +- `apps/web` already knows how to talk to a backend over `/ws` and REST + rewrites. +- The shortest path is to make the existing local path explicit, reliable, and + tested before adding another layer. + +### Milestone 0 work items + +#### 0.1 Fix repo path assumptions after the frontend move + +Current issue: + +- Some paths still assume `prod-ui/`, but the frontend now lives at + `apps/web/`. + +Required changes: + +- update prompt/preset path resolution in `server/config.py` +- update docs that still mention `prod-ui` +- audit any frontend build settings that assume the old repo root + +This is prerequisite cleanup. Local GPU mode should not depend on stale +monorepo paths. + +#### 0.2 Make local runtime startup the primary supported entrypoint + +Required outcome: + +- one documented backend command +- one documented frontend command +- one clear env contract for local development + +Expected shape: + +```bash +uv pip install -e ".[dreamverse]" +dreamverse-server --host 0.0.0.0 --port 8009 + +cd apps/dreamverse/web +npm ci +BACKEND_HOST=localhost BACKEND_PORT=8009 npm run dev +``` + +Optional but useful: + +- add a small root helper script or Make target for local startup +- add a `dreamverse-doctor` or lightweight startup check later + +#### 0.3 Define the minimum local runtime contract + +For Milestone 0, the frontend should rely only on the current runtime surface: + +- `/ws` +- `/status` +- `/healthz` +- `/readyz` +- existing prompt/devtools routes + +Do not add a second local API layer yet unless the current runtime surface is +proven insufficient. + +#### 0.4 Make failure states explicit in the UI + +Local GPU mode fails in a few predictable ways: + +- backend not reachable +- backend reachable but not ready +- `fastvideo` or model runtime missing +- no compatible GPU available + +Minimum implementation: + +- show a clear connection error when `/ws` or `/status` fails +- surface readiness failures in a human-readable way +- avoid silent retry loops that hide backend startup failures + +This is a small UI pass, not a controller project. + +#### 0.5 Add a minimal local smoke test path + +At this milestone, local GPU support is "done" only if there is a repeatable +test path for the local runtime contract. + +Minimum test additions: + +- backend tests for `/healthz`, `/readyz`, and `/status` +- a frontend integration test that assumes a reachable backend URL and verifies + connection lifecycle behavior +- one local smoke script that starts the backend and verifies readiness before + the frontend is launched + +### Milestone 1: Introduce a thin local controller + +Goal: + +- Preserve the same local GPU behavior, but place a stable local control-plane + API in front of the runtime. + +This should happen only after Milestone 0 is stable. + +Scope: + +- add `controller/` +- proxy `/ws` and the needed REST routes to `server/` +- expose controller-owned status for "backend starting", "runtime ready", and + "runtime failed" +- optionally spawn the local runtime as a subprocess + +Non-goal: + +- do not add remote provider logic yet + +Reasoning: + +- the controller earns its complexity only once it stabilizes the local + contract that future providers will share + +### Milestone 2: Provider abstraction on top of the controller + +Goal: + +- Keep the same frontend contract while allowing the controller to resolve a + runtime via `local`, then later `runpod` and `modal`. + +At this point: + +- define `ComputeProvider` +- implement `providers/local.py` +- move local-runtime subprocess management behind the provider interface + +The first provider should be `local`, because it is cheapest to debug and +matches the runtime most closely. + +## Minimal Code Change Order + +If we want the shortest path to a working local GPU milestone, the change order +should be: + +1. Fix `server/config.py` and any remaining path assumptions from `prod-ui` to + `apps/web`. +2. Update `README.md` to document the real local GPU startup flow. +3. Confirm `apps/dreamverse/web` connects cleanly to the local wrapper-backed backend. +4. Improve frontend error handling for backend-not-ready and backend-missing + cases. +5. Add a local smoke test and keep existing backend/frontend tests green. +6. Only then introduce `controller/`. + +## Test Plan for the Local GPU Milestone + +### Backend + +Keep the current Python test suite as the base: + +- `server/tests/test_health_endpoints.py` +- `server/tests/test_mock_server.py` +- `server/tests/test_prompt_enhancer.py` +- `server/tests/test_rewrite_prompt_payload.py` +- related config and logging tests + +Add or tighten tests for: + +- path resolution in `server/config.py` +- readiness behavior when GPU pool initialization fails +- startup error messaging when `fastvideo` is unavailable + +### Frontend + +Keep the current Vitest suite as the base: + +- websocket reducer tests +- prompt-window snapshot tests +- integration tests under `apps/web/src/app/` + +Add or tighten tests for: + +- connection failure UX when backend is down +- readiness failure UX when backend returns non-ready status +- backend routing configuration through `BACKEND_HOST` and `BACKEND_PORT` + +### Manual smoke path + +The first manual smoke checklist should be: + +1. start `dreamverse-server` +2. confirm `GET /healthz` returns 200 +3. confirm `GET /readyz` returns 200 after warmup +4. start `apps/web` +5. confirm the UI opens and the websocket connects +6. submit a prompt and verify the first generation starts + +This checklist should be written down in the README once the milestone is +implemented. + +## Test Strategy + +The test suite should preserve one rule: provider changes must not be able to +break prompt rewrite, websocket semantics, or runtime behavior silently. + +### 1. Runtime unit and integration tests + +Keep and expand the current `pytest` coverage in `server/test_*.py`. + +Focus areas: + +- config loading +- prompt rewrite payload normalization +- prompt enhancement and prompt safety +- health/readiness endpoints +- websocket session behavior +- session logging +- mock runtime behavior + +These tests should remain provider-agnostic. + +### 2. Controller unit tests + +Add a Python test suite for the controller state machine. + +Key cases: + +- provider selection and validation +- credential loading from local config or env +- runtime lifecycle transitions: + - idle + - provisioning + - ready + - failed + - stopping +- idle timeout and cleanup behavior +- retry and backoff behavior +- HTTP and websocket proxy routing + +These tests should use fake providers and fake runtimes by default. + +### 3. Provider contract tests + +Each provider should pass the same contract tests. + +Examples: + +- `ensure_runtime()` returns a usable `RuntimeHandle` +- `wait_until_ready()` surfaces timeout vs readiness correctly +- `stop_runtime()` is safe to call twice +- provider errors are mapped into stable controller error types + +Use recorded fixtures or fakes wherever possible to avoid spend in CI. + +### 4. Web contract tests + +The frontend already has useful Vitest coverage under `apps/web/src`. +Preserve that and expand around the controller split. + +Priority areas: + +- websocket event reduction +- prompt-window snapshot construction +- rewrite request shaping +- compute-status UI +- failure and reconnect UX + +The frontend should mock the local controller API, not provider APIs. + +### 5. Cross-layer protocol tests + +Add contract fixtures that validate shared payloads across layers. + +Important fixtures: + +- websocket event payloads +- rewrite request payloads +- runtime status payloads +- controller status payloads + +These can be simple JSON fixtures validated by both Python and TypeScript +tests. They will catch drift earlier than end-to-end tests. + +### 6. Smoke tests + +Add a small number of high-signal smoke tests: + +- local provider + mock runtime +- local provider + real runtime when GPU is available +- controller startup + frontend health path + +These should be cheap enough for routine local use. + +### 7. Provider-backed manual or nightly tests + +Real Modal and Runpod tests should be opt-in. + +- Do not run them in default CI. +- Gate them behind explicit credentials and flags. +- Keep them focused on provisioning and reachability, not full product + regression. + +This avoids flaky and expensive CI while still validating real provider flows. + +## Testing Recommendations for the Next Step + +The next practical additions should be: + +1. A controller test suite in Python using fake providers. +2. Shared contract fixtures for websocket and rewrite payloads. +3. A smoke test that starts the local controller against the existing mock + runtime. + +I would not add Playwright yet. The current web stack already has Vitest and +integration-style component tests, which are cheaper and better aligned with +the immediate reorg. Add browser automation only after the controller path is +stable. diff --git a/apps/dreamverse/docker/.dockerignore b/apps/dreamverse/docker/.dockerignore new file mode 100644 index 0000000000..2fb18db609 --- /dev/null +++ b/apps/dreamverse/docker/.dockerignore @@ -0,0 +1,40 @@ +.git/ +**/.git/ +.venv/ +**/.venv/ +**/__pycache__/ +**/*.pyc +**/*.pyo +**/*.egg-info/ +.pytest_cache/ +**/.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.cache/ + +apps/dreamverse/web/node_modules/ +apps/dreamverse/web/.next/ +apps/dreamverse/web/out/ +apps/dreamverse/web/dist/ +apps/dreamverse/web/test-results/ +apps/dreamverse/web/playwright-report/ + +apps/dreamverse/outputs/ +apps/dreamverse/dreamverse/outputs/ +apps/dreamverse/dreamverse/prompts.local/ +apps/dreamverse/logs/ +outputs/ +logs/ +slurm-logs/ +wandb/ + +.env +.env.* +**/prompts.local/ +.codex/ +.agents/exploration/ +.vscode/ +.idea/ +*.log +*.tmp +*.pdf diff --git a/apps/dreamverse/docker/Dockerfile b/apps/dreamverse/docker/Dockerfile new file mode 100644 index 0000000000..3374b37631 --- /dev/null +++ b/apps/dreamverse/docker/Dockerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1.7 +ARG CUDA_TAG=12.9.1-cudnn-devel-ubuntu22.04 +FROM nvidia/cuda:${CUDA_TAG} + +ARG BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=0 + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + UV_LINK_MODE=copy + +SHELL ["/bin/bash", "-c"] + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc-11 g++-11 clang-11 \ + make cmake ninja-build pkg-config nasm \ + git curl wget ca-certificates \ + libssl-dev zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* \ + && update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 \ + --slave /usr/bin/g++ g++ /usr/bin/g++-11 + +ENV CUDA_HOME=/usr/local/cuda-12.9 +ENV PATH=/root/.local/bin:/opt/venv/bin:${CUDA_HOME}/bin:${PATH} +ENV LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${LD_LIBRARY_PATH} +ENV VIRTUAL_ENV=/opt/venv + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh + +RUN uv venv --python 3.12 --seed /opt/venv \ + && echo 'source /opt/venv/bin/activate' >> /root/.bashrc + +WORKDIR /opt/FastVideo + +COPY . /opt/FastVideo + +RUN source /opt/venv/bin/activate \ + && uv pip install --no-cache-dir "/opt/FastVideo[dreamverse]" + +# Standard docker build does not expose GPUs, while fastvideo-kernel/build.sh +# detects the CUDA architecture with torch at build time. The FastVideo package +# install above brings in the pinned fastvideo-kernel package; rebuild from the +# copied source only on hosts configured for build-time GPU access. +RUN if [[ "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}" == "1" ]]; then \ + sed -i 's/^git submodule update --init --recursive$/if [[ -d ..\/.git ]]; then git submodule update --init --recursive; fi/' \ + /opt/FastVideo/fastvideo-kernel/build.sh \ + && source /opt/venv/bin/activate \ + && cd /opt/FastVideo/fastvideo-kernel \ + && ./build.sh; \ + else \ + echo "Skipping source fastvideo-kernel build; using installed fastvideo-kernel package."; \ + fi + +# The monorepo ffmpeg installer force-selects conda compiler triplets for +# local dev shells. Inside this image we explicitly opt into the system +# gcc/g++ toolchain. +RUN source /opt/venv/bin/activate \ + && INSTALL_PREFIX=/opt/ffmpeg-native \ + SOURCE_DIR=/tmp/ffmpeg-native-src \ + FFMPEG_NATIVE_CC=/usr/bin/gcc \ + FFMPEG_NATIVE_CXX=/usr/bin/g++ \ + bash /opt/FastVideo/apps/dreamverse/scripts/install_native_ffmpeg.sh + +ENV FASTVIDEO_DREAMVERSE_HOME=/var/lib/dreamverse \ + STREAM_MODE=av_fmp4 \ + FASTVIDEO_ENABLE_PROMPT_SAFETY=0 \ + HF_HOME=/root/.cache/huggingface + +RUN mkdir -p /var/lib/dreamverse + +EXPOSE 8009 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8009/healthz || exit 1 + +ENTRYPOINT ["/opt/FastVideo/apps/dreamverse/docker/docker_entrypoint.sh"] +CMD ["dreamverse-server", "--host", "0.0.0.0", "--port", "8009"] diff --git a/apps/dreamverse/docker/Dockerfile.dockerignore b/apps/dreamverse/docker/Dockerfile.dockerignore new file mode 100644 index 0000000000..2fb18db609 --- /dev/null +++ b/apps/dreamverse/docker/Dockerfile.dockerignore @@ -0,0 +1,40 @@ +.git/ +**/.git/ +.venv/ +**/.venv/ +**/__pycache__/ +**/*.pyc +**/*.pyo +**/*.egg-info/ +.pytest_cache/ +**/.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.cache/ + +apps/dreamverse/web/node_modules/ +apps/dreamverse/web/.next/ +apps/dreamverse/web/out/ +apps/dreamverse/web/dist/ +apps/dreamverse/web/test-results/ +apps/dreamverse/web/playwright-report/ + +apps/dreamverse/outputs/ +apps/dreamverse/dreamverse/outputs/ +apps/dreamverse/dreamverse/prompts.local/ +apps/dreamverse/logs/ +outputs/ +logs/ +slurm-logs/ +wandb/ + +.env +.env.* +**/prompts.local/ +.codex/ +.agents/exploration/ +.vscode/ +.idea/ +*.log +*.tmp +*.pdf diff --git a/apps/dreamverse/docker/README.md b/apps/dreamverse/docker/README.md new file mode 100644 index 0000000000..9b7879ce37 --- /dev/null +++ b/apps/dreamverse/docker/README.md @@ -0,0 +1,72 @@ +# Dreamverse Docker Image + +This folder contains the backend-only Docker image for Dreamverse inside the +FastVideo monorepo. Build commands use the FastVideo repository root as the +Docker context, so run the helper scripts from this folder or from any path in +the checkout. + +## Build + +```bash +apps/dreamverse/docker/docker_build.sh +``` + +The image defaults to `dreamverse:dev`. Override it with: + +```bash +DREAMVERSE_IMAGE=dreamverse:local apps/dreamverse/docker/docker_build.sh +``` + +The Dockerfile builds a CUDA 12.9.1 image, installs FastVideo from this +checkout with the `dreamverse` extra, installs the FA4 +flash-attention fork, builds native FFmpeg, and installs FlashInfer for NVFP4 +quantization. + +FastVideo's pinned `fastvideo-kernel==0.2.6` package is installed by default. +To rebuild `fastvideo-kernel` from this checkout during the image build, set: + +```bash +BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=1 apps/dreamverse/docker/docker_build.sh +``` + +That source build detects the GPU architecture with torch during `docker +build`. On hosts where Docker does not expose GPUs during build, leave the +default package install path enabled. + +## Run + +```bash +CEREBRAS_API_KEY="<your-key>" \ +GROQ_API_KEY="<your-key>" \ +apps/dreamverse/docker/docker_run.sh +``` + +The container serves Dreamverse on host port `8009` by default and mounts: + +```text +$HOME/.cache/huggingface -> /root/.cache/huggingface +apps/dreamverse/outputs -> /var/lib/dreamverse/outputs +``` + +Override the host port and output directory with `BACKEND_PORT` and +`DREAMVERSE_OUTPUTS_DIR`. + +To pin the container to a specific host GPU, pass Docker's GPU request syntax: + +```bash +DREAMVERSE_DOCKER_GPUS=device=4 FASTVIDEO_GPU_COUNT=1 \ + CEREBRAS_API_KEY="<your-key>" \ + GROQ_API_KEY="<your-key>" \ + apps/dreamverse/docker/docker_run.sh +``` + +## Smoke + +```bash +CEREBRAS_API_KEY=placeholder \ +GROQ_API_KEY=placeholder \ +apps/dreamverse/docker/docker_smoke.sh +``` + +The smoke script starts the container, polls `/healthz`, then polls `/readyz`. +It removes the container on exit unless `DREAMVERSE_KEEP_CONTAINER=1` is set. diff --git a/apps/dreamverse/docker/docker_build.sh b/apps/dreamverse/docker/docker_build.sh new file mode 100755 index 0000000000..a496a8dbbb --- /dev/null +++ b/apps/dreamverse/docker/docker_build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" +IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}" + +build_args=() +[[ -n "${CUDA_TAG:-}" ]] && build_args+=(--build-arg "CUDA_TAG=${CUDA_TAG}") +[[ -n "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE:-}" ]] && \ + build_args+=(--build-arg "BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}") + +exec docker build \ + -f "${SCRIPT_DIR}/Dockerfile" \ + -t "${IMAGE}" \ + "${build_args[@]}" \ + "${REPO_ROOT}" diff --git a/apps/dreamverse/docker/docker_entrypoint.sh b/apps/dreamverse/docker/docker_entrypoint.sh new file mode 100755 index 0000000000..9e3bb933ac --- /dev/null +++ b/apps/dreamverse/docker/docker_entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +source /opt/venv/bin/activate + +if [[ -f /opt/FastVideo/apps/dreamverse/scripts/ffmpeg-env.sh ]]; then + source /opt/FastVideo/apps/dreamverse/scripts/ffmpeg-env.sh +fi + +: "${CEREBRAS_API_KEY:?CEREBRAS_API_KEY must be set (pass with -e CEREBRAS_API_KEY=...)}" +: "${GROQ_API_KEY:?GROQ_API_KEY must be set (pass with -e GROQ_API_KEY=...)}" + +exec "$@" diff --git a/apps/dreamverse/docker/docker_run.sh b/apps/dreamverse/docker/docker_run.sh new file mode 100755 index 0000000000..f94c7e20b8 --- /dev/null +++ b/apps/dreamverse/docker/docker_run.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${CEREBRAS_API_KEY:?CEREBRAS_API_KEY not set on host}" +: "${GROQ_API_KEY:?GROQ_API_KEY not set on host}" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}" +PORT="${BACKEND_PORT:-8009}" +HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}" +OUTPUTS_DIR="${DREAMVERSE_OUTPUTS_DIR:-${DREAMVERSE_ROOT}/outputs}" +GPU_REQUEST="${DREAMVERSE_DOCKER_GPUS:-all}" + +mkdir -p "${HF_CACHE}" "${OUTPUTS_DIR}" + +env_args=( + -e "CEREBRAS_API_KEY=${CEREBRAS_API_KEY}" + -e "GROQ_API_KEY=${GROQ_API_KEY}" +) +[[ -n "${ENABLE_TORCH_COMPILE:-}" ]] && env_args+=(-e "ENABLE_TORCH_COMPILE=${ENABLE_TORCH_COMPILE}") +[[ -n "${FASTVIDEO_GPU_COUNT:-}" ]] && env_args+=(-e "FASTVIDEO_GPU_COUNT=${FASTVIDEO_GPU_COUNT}") + +exec docker run --rm --gpus "${GPU_REQUEST}" --init \ + -p "${PORT}:8009" \ + "${env_args[@]}" \ + -v "${HF_CACHE}:/root/.cache/huggingface" \ + -v "${OUTPUTS_DIR}:/var/lib/dreamverse/outputs" \ + "${IMAGE}" diff --git a/apps/dreamverse/docker/docker_smoke.sh b/apps/dreamverse/docker/docker_smoke.sh new file mode 100755 index 0000000000..83c3d689f8 --- /dev/null +++ b/apps/dreamverse/docker/docker_smoke.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${CEREBRAS_API_KEY:?CEREBRAS_API_KEY not set on host}" +: "${GROQ_API_KEY:?GROQ_API_KEY not set on host}" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" + +IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}" +PORT="${BACKEND_PORT:-8009}" +HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}" +OUTPUTS_DIR="${DREAMVERSE_OUTPUTS_DIR:-${DREAMVERSE_ROOT}/outputs}" +NAME="${DREAMVERSE_NAME:-dreamverse}" +TIMEOUT_SECONDS="${DREAMVERSE_SMOKE_TIMEOUT_SECONDS:-1200}" +POLL_SECONDS="${DREAMVERSE_SMOKE_POLL_SECONDS:-5}" +GPU_REQUEST="${DREAMVERSE_DOCKER_GPUS:-all}" + +mkdir -p "${HF_CACHE}" "${OUTPUTS_DIR}" + +docker rm -f "${NAME}" >/dev/null 2>&1 || true + +env_args=( + -e "CEREBRAS_API_KEY=${CEREBRAS_API_KEY}" + -e "GROQ_API_KEY=${GROQ_API_KEY}" + -e "ENABLE_TORCH_COMPILE=${ENABLE_TORCH_COMPILE:-0}" +) +[[ -n "${FASTVIDEO_GPU_COUNT:-}" ]] && env_args+=(-e "FASTVIDEO_GPU_COUNT=${FASTVIDEO_GPU_COUNT}") + +container_id="$( + docker run -d --rm --gpus "${GPU_REQUEST}" --init \ + -p "${PORT}:8009" \ + "${env_args[@]}" \ + -v "${HF_CACHE}:/root/.cache/huggingface" \ + -v "${OUTPUTS_DIR}:/var/lib/dreamverse/outputs" \ + --name "${NAME}" \ + "${IMAGE}" +)" + +cleanup() { + if [[ "${DREAMVERSE_KEEP_CONTAINER:-0}" != "1" ]]; then + docker rm -f "${NAME}" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +wait_for_endpoint() { + local path="$1" + local label="$2" + local deadline=$((SECONDS + TIMEOUT_SECONDS)) + local url="http://127.0.0.1:${PORT}${path}" + + echo "Waiting for ${label} at ${url}" + while (( SECONDS < deadline )); do + if curl -fsS "${url}" >/dev/null 2>&1; then + echo "${label} ok" + return 0 + fi + if ! docker ps --format '{{.Names}}' | grep -qx "${NAME}"; then + echo "Container exited before ${label} became healthy." >&2 + docker logs "${container_id}" >&2 || true + return 1 + fi + sleep "${POLL_SECONDS}" + done + + echo "Timed out waiting for ${label}." >&2 + docker logs "${container_id}" >&2 || true + return 1 +} + +wait_for_endpoint "/healthz" "healthz" +wait_for_endpoint "/readyz" "readyz" + +echo "Dreamverse Docker smoke passed for ${IMAGE} on host port ${PORT}." diff --git a/apps/dreamverse/dreamverse/__init__.py b/apps/dreamverse/dreamverse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/dreamverse/dreamverse/_deps.py b/apps/dreamverse/dreamverse/_deps.py new file mode 100644 index 0000000000..03295d42eb --- /dev/null +++ b/apps/dreamverse/dreamverse/_deps.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +DREAMVERSE_RUNTIME_DEPS_MESSAGE = ( + "Dreamverse runtime deps missing — install with pip install 'fastvideo[dreamverse]'.") + + +def require_dreamverse_runtime_deps() -> None: + try: + import cerebras.cloud.sdk # noqa: F401 + import openai # noqa: F401 + except ModuleNotFoundError as exc: + missing_root = (exc.name or "").split(".", 1)[0] + if missing_root in {"cerebras", "openai"}: + raise SystemExit(DREAMVERSE_RUNTIME_DEPS_MESSAGE) from exc + raise diff --git a/apps/dreamverse/dreamverse/av_streaming.py b/apps/dreamverse/dreamverse/av_streaming.py new file mode 100644 index 0000000000..c9b4a7cf51 --- /dev/null +++ b/apps/dreamverse/dreamverse/av_streaming.py @@ -0,0 +1,437 @@ +# pyright: reportMissingTypeArgument=false, reportArgumentType=false, reportOptionalSubscript=false, reportOptionalMemberAccess=false, reportConstantRedefinition=false, reportCallIssue=false +# ruff: noqa: UP007, SIM108, SIM105 +# mypy: ignore-errors +"""ffmpeg fMP4 muxing with chunk-level event emission. + +Self-contained: spawns ffmpeg as a subprocess, pipes raw frames into +its stdin, reads fragmented-MP4 chunks from stdout, and publishes each +chunk as a ``StreamEvent`` via the caller-supplied ``publish`` +callback. Knows nothing about multiprocessing queues, the GPU pool, +or individual users — the caller decides what "publish" means. +""" +import fcntl +import os +import shutil +import subprocess +import tempfile +import threading +import time +import uuid +import wave +from dataclasses import dataclass +from typing import Union +from collections.abc import Callable + +import numpy as np +import torch + +FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg")) +AV_MEDIA_MIME = os.getenv( + "STREAM_MIME_TYPE", + 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"', +) +AV_CHUNK_SIZE_BYTES = 1048576 +TARGET_FPS = 24 +AV_FRAGMENT_DURATION_US = int(os.getenv("FASTVIDEO_FRAG_US", "250000")) +X264_GOP_FRAMES = int(os.getenv("FASTVIDEO_X264_GOP", "12")) +X264_PROFILE = os.getenv("FASTVIDEO_X264_PROFILE", "baseline").strip().lower() +if X264_PROFILE not in {"baseline", "main", "high", "main10", "high10"}: + print(f"[WARN] Unsupported FASTVIDEO_X264_PROFILE={X264_PROFILE}; using baseline") + X264_PROFILE = "baseline" +USE_SHARED_STREAM_BUFFER = (os.getenv("FASTVIDEO_USE_SHARED_STREAM_BUFFER", "1").strip().lower() + not in {"0", "false", "no"}) +SHARED_STREAM_BUFFER_BYTES = int(os.getenv("FASTVIDEO_SHARED_STREAM_BUFFER_BYTES", str(256 * 1024 * 1024))) + + +@dataclass +class StreamInit: + """First event emitted — tells the consumer the stream is starting.""" + stream_id: str + mime: str + uses_shared_buffer: bool + + +@dataclass +class StreamChunk: + """One fMP4 chunk. Either ``chunk`` (raw bytes) or + ``chunk_offset``+``chunk_length`` (read from the shared buffer) + will be populated, never both.""" + stream_id: str + chunk: bytes | None = None + chunk_offset: int | None = None + chunk_length: int | None = None + uses_shared_buffer: bool = False + + +@dataclass +class StreamComplete: + """Final event emitted — muxing finished successfully.""" + stream_id: str + chunks: int + + +StreamEvent = Union[StreamInit, StreamChunk, StreamComplete] + + +def generate_stream_id(segment_idx: int) -> str: + """Convenience: build a stream id of the form ``seg007-abcd1234``.""" + return f"seg{segment_idx:03d}-{uuid.uuid4().hex[:8]}" + + +def _normalize_audio_tensor(audio: object) -> tuple[np.ndarray, int] | None: + """Convert audio tensor/array into int16 ndarray [samples, channels].""" + if audio is None: + return None + + if torch.is_tensor(audio): + audio_np = audio.detach().cpu().float().numpy() + else: + audio_np = np.asarray(audio, dtype=np.float32) + + if audio_np.ndim == 1: + audio_np = audio_np[:, None] + elif audio_np.ndim == 2: + if audio_np.shape[0] <= 8 and audio_np.shape[1] > audio_np.shape[0]: + audio_np = audio_np.T + else: + return None + + audio_np = np.clip(audio_np, -1.0, 1.0) + audio_int16 = (audio_np * 32767.0).astype(np.int16) + num_channels = audio_int16.shape[1] + return audio_int16, num_channels + + +def _write_audio_wav( + audio_int16: np.ndarray, + num_channels: int, + sample_rate: int, +) -> str: + """Write normalized int16 audio to a temporary WAV file.""" + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: + wav_path = f.name + with wave.open(wav_path, "wb") as wav_file: + wav_file.setnchannels(num_channels) + wav_file.setsampwidth(2) + wav_file.setframerate(sample_rate) + wav_file.writeframes(audio_int16.tobytes()) + return wav_path + + +def stream_fmp4( + *, + frames: list[np.ndarray], + audio: object, + audio_sample_rate: int | None, + stream_id: str, + timings: dict, + head_trim_frames: int = 0, + head_trim_audio_frames: int | None = None, + shared_buffer=None, + shared_buffer_bytes: int = 0, + publish: Callable[[StreamEvent], None], + log_prefix: str = "", +) -> tuple[bool, str | None]: + """Encode frames+audio with ffmpeg, publish each fMP4 chunk as an event. + + Args: + frames: RGB24 video frames as HxWx3 uint8 arrays. + audio: 1D/2D tensor or ndarray, float values in [-1, 1]. + audio_sample_rate: sample rate of ``audio``. + stream_id: caller-supplied identifier carried on every event. + timings: dict mutated in place with ffmpeg/stream timing metrics. + head_trim_frames: video frames to drop from the start + (conditioning overlap). + head_trim_audio_frames: video-frame-equivalent audio to drop. + Defaults to ``head_trim_frames``. + shared_buffer: optional ``mp.RawArray``-compatible object; when + provided, chunks are written into it and emitted by offset + rather than by bytes (avoids IPC copies). + shared_buffer_bytes: size of ``shared_buffer`` in bytes. + publish: callback invoked once per stream event. + log_prefix: prepended to warning prints (e.g. ``"[GPU 0]"``). + + Returns: + ``(True, None)`` on success, ``(False, error_message)`` on + failure. On mid-stream failure, a ``StreamInit`` may have + already been published — the caller is responsible for + handling that. + """ + if not frames: + return False, "no frames returned" + if audio is None: + return False, "audio is None" + if audio_sample_rate is None: + return False, "audio_sample_rate is None" + if FFMPEG_BIN is None: + return False, "ffmpeg not found" + + if head_trim_audio_frames is None: + head_trim_audio_frames = head_trim_frames + + normalized_audio = _normalize_audio_tensor(audio) + if normalized_audio is None: + shape_hint = getattr(audio, "shape", None) + return False, f"unsupported audio shape={shape_hint}" + audio_int16, num_channels = normalized_audio + + if head_trim_frames < 0: + return False, (f"head_trim_frames must be >= 0, " + f"got {head_trim_frames}") + if head_trim_frames >= len(frames): + return False, (f"head_trim_frames={head_trim_frames} removes " + f"all {len(frames)} frames in segment") + + out_frames = (frames[head_trim_frames:] if head_trim_frames > 0 else frames) + sample_rate = int(audio_sample_rate) + if head_trim_audio_frames > 0: + trim_start_samples = int(round((head_trim_audio_frames / float(TARGET_FPS)) * sample_rate)) + if trim_start_samples >= audio_int16.shape[0]: + return False, ("audio too short after overlap trim: " + f"trim_start_samples={trim_start_samples}" + f", audio_samples={audio_int16.shape[0]}") + keep_samples = int(round((len(out_frames) / float(TARGET_FPS)) * sample_rate)) + trim_end_samples = min( + audio_int16.shape[0], + trim_start_samples + keep_samples, + ) + if trim_end_samples <= trim_start_samples: + return False, ("invalid audio trim range: " + f"start={trim_start_samples}, " + f"end={trim_end_samples}") + audio_int16 = audio_int16[trim_start_samples:trim_end_samples] + + height = int(out_frames[0].shape[0]) + width = int(out_frames[0].shape[1]) + codec = os.getenv("FASTVIDEO_VIDEO_CODEC", "libx264") + t_wav_start = time.perf_counter() + wav_path = _write_audio_wav(audio_int16, num_channels, sample_rate) + wav_write_ms = (time.perf_counter() - t_wav_start) * 1000 + + cmd = [ + FFMPEG_BIN, + "-hide_banner", + "-loglevel", + "error", + "-y", + "-f", + "rawvideo", + "-pix_fmt", + "rgb24", + "-s:v", + f"{width}x{height}", + "-r", + str(TARGET_FPS), + "-i", + "pipe:0", + "-i", + wav_path, + "-c:v", + codec, + ] + if codec.endswith("_nvenc"): + cmd += [ + "-preset", + os.getenv("FASTVIDEO_NVENC_PRESET", "p1"), + "-tune", + os.getenv("FASTVIDEO_NVENC_TUNE", "ull"), + "-rc", + os.getenv("FASTVIDEO_NVENC_RC", "constqp"), + "-qp", + os.getenv("FASTVIDEO_NVENC_QP", "28"), + "-bf", + os.getenv("FASTVIDEO_NVENC_BF", "0"), + ] + else: + cmd += [ + "-preset", + os.getenv("FASTVIDEO_X264_PRESET", "ultrafast"), + "-tune", + "zerolatency", + "-profile:v", + X264_PROFILE, + # Emit frequent keyframes so fragments are independently playable. + "-g", + str(X264_GOP_FRAMES), + "-keyint_min", + str(X264_GOP_FRAMES), + "-x264-params", + "scenecut=0", + ] + cmd += [ + "-c:a", + "aac", + "-pix_fmt", + os.getenv("FASTVIDEO_OUTPUT_PIX_FMT", "yuv420p"), + "-shortest", + "-movflags", + "+frag_keyframe+empty_moov+default_base_moof", + "-frag_duration", + str(AV_FRAGMENT_DURATION_US), + "-flush_packets", + "1", + "-muxdelay", + "0", + "-muxpreload", + "0", + "-f", + "mp4", + "pipe:1", + ] + + proc: subprocess.Popen | None = None + stderr_chunks: list[bytes] = [] + writer_error: list[Exception | None] = [None] + t_stream_start = time.perf_counter() + use_shared_buffer = (USE_SHARED_STREAM_BUFFER and shared_buffer is not None and shared_buffer_bytes > 0) + shared_write_offset = 0 + shared_buffer_fallback = False + shared_np = (np.frombuffer( + shared_buffer, + dtype=np.uint8, + count=shared_buffer_bytes, + ) if use_shared_buffer else None) + chunk_intervals_ms: list[float] = [] + chunk_publish_ms: list[float] = [] + chunk_read_ms: list[float] = [] + + try: + t_proc_spawn_start = time.perf_counter() + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + ) + assert proc.stdin is not None + assert proc.stdout is not None + assert proc.stderr is not None + fcntl.fcntl(proc.stdin.fileno(), fcntl.F_SETPIPE_SZ, 1048576) + fcntl.fcntl(proc.stdout.fileno(), fcntl.F_SETPIPE_SZ, 1048576) + ffmpeg_spawn_ms = (time.perf_counter() - t_proc_spawn_start) * 1000 + + def _write_frames(): + try: + for frame in out_frames: + proc.stdin.write(np.ascontiguousarray(frame).tobytes()) + proc.stdin.close() + except Exception as exc: + writer_error[0] = exc + try: + proc.stdin.close() + except Exception: + pass + + def _read_stderr(): + try: + while True: + data = proc.stderr.read(4096) + if not data: + break + stderr_chunks.append(data) + except Exception: + pass + + writer_thread = threading.Thread(target=_write_frames, daemon=True) + stderr_thread = threading.Thread(target=_read_stderr, daemon=True) + writer_thread.start() + stderr_thread.start() + + publish(StreamInit( + stream_id=stream_id, + mime=AV_MEDIA_MIME, + uses_shared_buffer=use_shared_buffer, + )) + + chunk_count = 0 + total_bytes = 0 + first_chunk_ms: float | None = None + last_chunk_emit_t = time.perf_counter() + while True: + t_read_start = time.perf_counter() + chunk = proc.stdout.read(AV_CHUNK_SIZE_BYTES) + t_read_end = time.perf_counter() + if not chunk: + break + chunk_read_ms.append((t_read_end - t_read_start) * 1000) + chunk_count += 1 + total_bytes += len(chunk) + if first_chunk_ms is None: + first_chunk_ms = (t_read_end - t_stream_start) * 1000 + chunk_intervals_ms.append((t_read_end - last_chunk_emit_t) * 1000) + t_publish_start = time.perf_counter() + if use_shared_buffer and not shared_buffer_fallback: + chunk_len = len(chunk) + write_end = shared_write_offset + chunk_len + if write_end <= shared_buffer_bytes: + shared_np[shared_write_offset:write_end] = np.frombuffer(chunk, dtype=np.uint8) + publish( + StreamChunk( + stream_id=stream_id, + chunk_offset=shared_write_offset, + chunk_length=chunk_len, + uses_shared_buffer=True, + )) + shared_write_offset = write_end + chunk_publish_ms.append((time.perf_counter() - t_publish_start) * 1000) + last_chunk_emit_t = time.perf_counter() + continue + shared_buffer_fallback = True + print(f"{log_prefix} Shared stream buffer exhausted at " + f"{shared_write_offset / (1024 * 1024):.1f}MB; " + "falling back to queue chunk bytes") + publish(StreamChunk( + stream_id=stream_id, + chunk=chunk, + )) + chunk_publish_ms.append((time.perf_counter() - t_publish_start) * 1000) + last_chunk_emit_t = time.perf_counter() + + writer_thread.join(timeout=5.0) + rc = proc.wait() + stderr_thread.join(timeout=1.0) + + if rc != 0: + stderr_tail = b"".join(stderr_chunks).decode(errors="ignore")[-1200:] + return False, f"ffmpeg av_fmp4 stream failed (rc={rc}): {stderr_tail}" + if writer_error[0] is not None: + return False, f"ffmpeg frame writer failed: {writer_error[0]}" + + timings["av_encode_stream_ms"] = (time.perf_counter() - t_stream_start) * 1000 + timings["av_stream_bytes"] = total_bytes + timings["av_trim_head_frames"] = head_trim_frames + timings["av_trim_head_audio_frames"] = head_trim_audio_frames + timings["av_frames_encoded"] = len(out_frames) + timings["av_shared_buffer_used"] = (bool(use_shared_buffer and not shared_buffer_fallback)) + timings["av_wav_write_ms"] = wav_write_ms + timings["av_ffmpeg_spawn_ms"] = ffmpeg_spawn_ms + timings["av_first_chunk_ms"] = first_chunk_ms or 0.0 + if chunk_intervals_ms: + timings["av_chunk_interval_ms_min"] = min(chunk_intervals_ms) + timings["av_chunk_interval_ms_median"] = float(np.median(chunk_intervals_ms)) + timings["av_chunk_interval_ms_p95"] = (float(np.percentile(chunk_intervals_ms, 95))) + timings["av_chunk_interval_ms_max"] = max(chunk_intervals_ms) + if chunk_publish_ms: + timings["av_chunk_publish_ms_median"] = float(np.median(chunk_publish_ms)) + timings["av_chunk_publish_ms_p95"] = float(np.percentile(chunk_publish_ms, 95)) + if chunk_read_ms: + timings["av_chunk_read_ms_median"] = float(np.median(chunk_read_ms)) + timings["av_chunk_read_ms_p95"] = float(np.percentile(chunk_read_ms, 95)) + publish(StreamComplete( + stream_id=stream_id, + chunks=chunk_count, + )) + return True, None + except Exception as exc: + return False, str(exc) + finally: + if proc is not None and proc.poll() is None: + try: + proc.kill() + except Exception: + pass + try: + os.remove(wav_path) + except OSError: + pass diff --git a/apps/dreamverse/dreamverse/benchmarks/__init__.py b/apps/dreamverse/dreamverse/benchmarks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/dreamverse/dreamverse/benchmarks/benchmark_av_streaming.py b/apps/dreamverse/dreamverse/benchmarks/benchmark_av_streaming.py new file mode 100644 index 0000000000..caa4be00b4 --- /dev/null +++ b/apps/dreamverse/dreamverse/benchmarks/benchmark_av_streaming.py @@ -0,0 +1,288 @@ +"""Benchmark the AV streaming hot-path used by dreamverse-server. + +Measures wall-time, encoded byte volume, and chunk count for +``av_streaming.stream_fmp4`` over synthetic frames + audio at +production resolution. Sweeps codecs (default: ``libx264`` and +``h264_nvenc`` if the active ffmpeg supports it) and ffmpeg presets so +the deploy can pick a configuration that achieves a >=1.0 realtime +ratio (5.04s of generated video produced in <=5.04s wall-time). + +Usage:: + + python -m apps.dreamverse.server.benchmarks.benchmark_av_streaming + python -m apps.dreamverse.server.benchmarks.benchmark_av_streaming \\ + --frames 121 --width 1920 --height 1088 --runs 3 \\ + --codecs libx264 h264_nvenc --x264-preset ultrafast \\ + --nvenc-preset p1 + FASTVIDEO_FFMPEG_BIN=$HOME/opt/ffmpeg-native/bin/ffmpeg \\ + python -m apps.dreamverse.server.benchmarks.benchmark_av_streaming + +Skips ``h264_nvenc`` automatically if the binary lacks the encoder. +This is the regression guard documented in +`.agents/memory/dreamverse-integration/decisions-log.md` D-21. +""" +from __future__ import annotations + +import argparse +import os +import shutil +import statistics +import subprocess +import sys +import time +from collections.abc import Iterable +from dataclasses import dataclass + +import numpy as np +import torch + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_SERVER_ROOT = os.path.dirname(_HERE) +if _SERVER_ROOT not in sys.path: + sys.path.insert(0, _SERVER_ROOT) + +from dreamverse.av_streaming import stream_fmp4 # noqa: E402 + + +@dataclass +class BenchResult: + codec: str + preset: str + runs: int + wall_ms_min: float + wall_ms_median: float + wall_ms_p95: float + wall_ms_max: float + bytes_median: int + chunks_median: float + realtime_ratio_median: float + error: str | None = None + + +def _make_synthetic_frames(num: int, width: int, height: int, seed: int) -> list[np.ndarray]: + rng = np.random.default_rng(seed) + base = rng.integers(0, 255, size=(height, width, 3), dtype=np.uint8) + out: list[np.ndarray] = [] + for i in range(num): + f = base.copy() + f[:, :, 0] = (f[:, :, 0].astype(np.int32) + i * 2) % 256 + out.append(f) + return out + + +def _make_synthetic_audio(num_frames: int, fps: int, sample_rate: int, seed: int) -> torch.Tensor: + duration_s = num_frames / fps + samples = int(round(duration_s * sample_rate)) + rng = np.random.default_rng(seed + 1) + audio = (rng.uniform(-0.1, 0.1, (2, samples))).astype(np.float32) + return torch.from_numpy(audio) + + +def _ffmpeg_supports(codec: str, ffmpeg_bin: str) -> bool: + try: + out = subprocess.run( + [ffmpeg_bin, "-hide_banner", "-encoders"], + capture_output=True, + text=True, + check=False, + timeout=10, + ).stdout + except Exception: + return False + needle = f" {codec} " + return any(needle in line for line in out.splitlines()) + + +def _run_one(frames: list[np.ndarray], audio: torch.Tensor, sample_rate: int, codec: str, + preset: str) -> tuple[float, int, int, str | None]: + timings: dict = {} + chunks: list = [] + + def _publish(event): + chunks.append(event) + + prev_codec = os.environ.get("FASTVIDEO_VIDEO_CODEC") + prev_preset = os.environ.get("FASTVIDEO_X264_PRESET") + prev_nvenc_preset = os.environ.get("FASTVIDEO_NVENC_PRESET") + os.environ["FASTVIDEO_VIDEO_CODEC"] = codec + if codec.endswith("_nvenc"): + os.environ["FASTVIDEO_NVENC_PRESET"] = preset + else: + os.environ["FASTVIDEO_X264_PRESET"] = preset + + t0 = time.perf_counter() + try: + ok, err = stream_fmp4( + frames=frames, + audio=audio, + audio_sample_rate=sample_rate, + stream_id="bench", + timings=timings, + head_trim_frames=0, + head_trim_audio_frames=0, + shared_buffer=None, + shared_buffer_bytes=0, + publish=_publish, + log_prefix="[bench]", + ) + finally: + if prev_codec is None: + os.environ.pop("FASTVIDEO_VIDEO_CODEC", None) + else: + os.environ["FASTVIDEO_VIDEO_CODEC"] = prev_codec + if prev_preset is None: + os.environ.pop("FASTVIDEO_X264_PRESET", None) + else: + os.environ["FASTVIDEO_X264_PRESET"] = prev_preset + if prev_nvenc_preset is None: + os.environ.pop("FASTVIDEO_NVENC_PRESET", None) + else: + os.environ["FASTVIDEO_NVENC_PRESET"] = prev_nvenc_preset + wall_ms = (time.perf_counter() - t0) * 1000.0 + if not ok: + return wall_ms, 0, 0, err or "stream_fmp4 returned False" + total_bytes = int(timings.get("av_stream_bytes", 0)) + return wall_ms, total_bytes, len(chunks), None + + +def benchmark(codecs: Iterable[str], runs: int, frames_n: int, width: int, height: int, fps: int, sample_rate: int, + x264_preset: str, nvenc_preset: str, seed: int, ffmpeg_bin: str) -> list[BenchResult]: + print(f"[bench] ffmpeg_bin={ffmpeg_bin}") + print(f"[bench] frames={frames_n} {width}x{height} fps={fps} " + f"audio_sr={sample_rate} runs/codec={runs}") + frames = _make_synthetic_frames(frames_n, width, height, seed) + audio = _make_synthetic_audio(frames_n, fps, sample_rate, seed) + playable_s = frames_n / fps + print(f"[bench] playable={playable_s:.3f}s " + f"(realtime_ratio = playable / wall_time; >= 1.0 means no " + f"buffer drain)") + + results: list[BenchResult] = [] + for codec in codecs: + preset = nvenc_preset if codec.endswith("_nvenc") else x264_preset + if not _ffmpeg_supports(codec, ffmpeg_bin): + results.append( + BenchResult(codec=codec, + preset=preset, + runs=0, + wall_ms_min=0, + wall_ms_median=0, + wall_ms_p95=0, + wall_ms_max=0, + bytes_median=0, + chunks_median=0, + realtime_ratio_median=0, + error=f"{codec} not in ffmpeg")) + continue + walls: list[float] = [] + sizes: list[int] = [] + chunkcounts: list[int] = [] + last_err: str | None = None + for run in range(runs): + wall_ms, total_bytes, chunk_count, err = _run_one(frames, audio, sample_rate, codec, preset) + print(f"[bench] codec={codec:12s} preset={preset:9s} " + f"run={run + 1}/{runs} wall={wall_ms:7.1f}ms " + f"bytes={total_bytes:>9d} chunks={chunk_count:>3d} " + f"realtime={playable_s / (wall_ms / 1000.0):5.2f}x" + f"{' ERR=' + err if err else ''}") + if err is not None: + last_err = err + continue + walls.append(wall_ms) + sizes.append(total_bytes) + chunkcounts.append(chunk_count) + if not walls: + results.append( + BenchResult(codec=codec, + preset=preset, + runs=0, + wall_ms_min=0, + wall_ms_median=0, + wall_ms_p95=0, + wall_ms_max=0, + bytes_median=0, + chunks_median=0, + realtime_ratio_median=0, + error=last_err or "all runs failed")) + continue + walls_sorted = sorted(walls) + p95_idx = max(0, int(round(0.95 * (len(walls_sorted) - 1)))) + wall_med = statistics.median(walls) + results.append( + BenchResult( + codec=codec, + preset=preset, + runs=len(walls), + wall_ms_min=min(walls), + wall_ms_median=wall_med, + wall_ms_p95=walls_sorted[p95_idx], + wall_ms_max=max(walls), + bytes_median=int(statistics.median(sizes)), + chunks_median=statistics.median(chunkcounts), + realtime_ratio_median=playable_s / (wall_med / 1000.0), + )) + return results + + +def _print_summary(results: list[BenchResult]) -> None: + print() + print("=== summary ===") + header = (f"{'codec':14s} {'preset':10s} {'runs':>4s} " + f"{'wall_med_ms':>11s} {'wall_p95_ms':>11s} " + f"{'bytes_med':>10s} {'realtime':>8s} notes") + print(header) + print("-" * len(header)) + for r in results: + if r.error is not None: + print(f"{r.codec:14s} {r.preset:10s} {r.runs:>4d} " + f"{'-':>11s} {'-':>11s} {'-':>10s} {'-':>8s} " + f"ERR: {r.error}") + continue + print(f"{r.codec:14s} {r.preset:10s} {r.runs:>4d} " + f"{r.wall_ms_median:>11.1f} {r.wall_ms_p95:>11.1f} " + f"{r.bytes_median:>10d} {r.realtime_ratio_median:>7.2f}x " + f"{'OK' if r.realtime_ratio_median >= 1.0 else 'BUFFER DRAINS'}") + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--frames", type=int, default=121, help="num frames (default: 121, matches NUM_FRAMES)") + p.add_argument("--width", type=int, default=1920) + p.add_argument("--height", type=int, default=1088) + p.add_argument("--fps", type=int, default=24) + p.add_argument("--sample-rate", type=int, default=24000) + p.add_argument("--runs", type=int, default=3, help="runs per codec (default: 3 — 1 warmup + 2 timed in median)") + p.add_argument("--codecs", + nargs="+", + default=["libx264", "h264_nvenc"], + help="codecs to benchmark; missing ones are skipped") + p.add_argument("--x264-preset", default="ultrafast") + p.add_argument("--nvenc-preset", default="p1") + p.add_argument("--seed", type=int, default=0) + args = p.parse_args() + + ffmpeg_bin = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg")) + if ffmpeg_bin is None: + print("ffmpeg not found", file=sys.stderr) + return 2 + + results = benchmark( + codecs=args.codecs, + runs=args.runs, + frames_n=args.frames, + width=args.width, + height=args.height, + fps=args.fps, + sample_rate=args.sample_rate, + x264_preset=args.x264_preset, + nvenc_preset=args.nvenc_preset, + seed=args.seed, + ffmpeg_bin=ffmpeg_bin, + ) + _print_summary(results) + any_below = any(r.error is None and r.realtime_ratio_median < 1.0 for r in results) + return 1 if any_below else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py b/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py new file mode 100644 index 0000000000..1be9e16087 --- /dev/null +++ b/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py @@ -0,0 +1,362 @@ +"""Benchmark the LTX-2 generation pipeline driven by the dreamverse Python SDK path. + +Mirrors how ``apps/dreamverse/server/video_generation.py`` constructs +``GeneratorConfig`` and calls ``VideoGenerator.generate()``, then +captures per-stage timings via the ``FASTVIDEO_STAGE_LOGGING=1`` log +hooks (same mechanism as ``FastVideo-internal/examples/inference/basic/ +basic_ltx2_distilled_i2v_two_stage_time.py``). + +Reports for each scenario: +* Total wall-time (median, p95) +* Per-stage execution_time (input_validation_stage, + prompt_encoding_stage, ltx2_refine_init_stage, latent_preparation_stage, + denoising_stage, ltx2_upsample_stage, ltx2_refine_lora_stage, + ltx2_refine_denoising_stage, audio_decoding_stage, decoding_stage) +* Realtime ratio (frames / fps / wall_time; >= 1.0 = no buffer drain) +* Peak GPU memory + +Sweep scenarios (default): +* compile=False, warmup=False → cold inference baseline +* compile=True, warmup=False → JIT compile mid-run +* compile=True, warmup=True → fully warmed (production mode) + +Skips compile / NVENC if the host lacks support. Does NOT exercise the +AV streaming path (use ``benchmark_av_streaming.py`` for that). + +Usage:: + + python -m apps.dreamverse.server.benchmarks.benchmark_pipeline + python -m apps.dreamverse.server.benchmarks.benchmark_pipeline \\ + --runs 3 --scenarios compile_warm cold --gpu 4 + +Cross-references D-21 / D-22 in +``.agents/memory/dreamverse-integration/decisions-log.md``. +""" +from __future__ import annotations + +import argparse +import contextlib +import json +import os +import statistics +import time +from collections import OrderedDict +from dataclasses import asdict, dataclass + +if "FASTVIDEO_STAGE_LOGGING" not in os.environ: + os.environ["FASTVIDEO_STAGE_LOGGING"] = "1" +if "FASTVIDEO_ATTENTION_BACKEND" not in os.environ: + os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN" + +import torch # noqa: E402 + +from fastvideo import VideoGenerator # noqa: E402 +from fastvideo.api import ( # noqa: E402 + ComponentConfig, CompileConfig, EngineConfig, GeneratorConfig, OffloadConfig, PipelineSelection, QuantizationConfig, +) + +DEFAULT_PROMPT = ("A cinematic drone shot over coastal cliffs at sunrise, golden " + "light, gentle ocean waves, ultra detailed") +DEFAULT_MODEL = "FastVideo/LTX2-Distilled-Diffusers" + + +@dataclass +class ScenarioConfig: + name: str + enable_compile: bool + do_warmup: bool + nvenc: bool = False + + +DEFAULT_SCENARIOS: list[ScenarioConfig] = [ + ScenarioConfig(name="cold", enable_compile=False, do_warmup=False), + ScenarioConfig(name="compile_cold", enable_compile=True, do_warmup=False), + ScenarioConfig(name="compile_warm", enable_compile=True, do_warmup=True), +] + + +@dataclass +class RunResult: + wall_ms: float + stage_times_ms: OrderedDict[str, float] + peak_gpu_mb: float + error: str | None = None + + +@dataclass +class ScenarioResult: + name: str + enable_compile: bool + do_warmup: bool + runs: int + wall_ms_median: float + wall_ms_p95: float + stage_means_ms: OrderedDict[str, float] + realtime_ratio_median: float + peak_gpu_mb_max: float + error: str | None = None + + +def _build_generator_config(model_path: str, enable_compile: bool, num_gpus: int) -> GeneratorConfig: + components = ComponentConfig(config_root=model_path) + return GeneratorConfig( + model_path=model_path, + engine=EngineConfig( + num_gpus=num_gpus, + offload=OffloadConfig(dit=False, dit_layerwise=False, text_encoder=False, vae=False, pin_cpu_memory=True), + compile=CompileConfig(enabled=enable_compile, + text_encoder_enabled=enable_compile, + backend="inductor", + fullgraph=True, + mode="max-autotune-no-cudagraphs", + dynamic=False), + use_fsdp_inference=False, + quantization=QuantizationConfig(transformer_quant="NVFP4"), + ), + pipeline=PipelineSelection( + components=components, + vae_tiling=False, + preset_overrides={ + "refine": { + "enabled": True, + "num_inference_steps": 2, + "guidance_scale": 1.0, + "add_noise": True, + }, + }, + ), + ) + + +def _extract_stage_times(result: dict) -> OrderedDict[str, float]: + out: OrderedDict[str, float] = OrderedDict() + info = result.get("logging_info") if isinstance(result, dict) else None + if info is None: + return out + stages = getattr(info, "stages", None) + if not stages: + return out + for name, metrics in stages.items(): + exec_time = float(metrics.get("execution_time", 0.0)) + out[name] = exec_time * 1000.0 + return out + + +def _peak_gpu_mb() -> float: + if not torch.cuda.is_available(): + return 0.0 + try: + return torch.cuda.max_memory_allocated() / (1024 * 1024) + except Exception: + return 0.0 + + +def _reset_peak_gpu() -> None: + if torch.cuda.is_available(): + with contextlib.suppress(Exception): + torch.cuda.reset_peak_memory_stats() + + +def _do_one_run(generator: VideoGenerator, prompt: str, *, height: int, width: int, num_frames: int, seed: int, + num_inference_steps: int) -> RunResult: + _reset_peak_gpu() + t0 = time.perf_counter() + try: + result = generator.generate_video( + prompt=prompt, + negative_prompt="", + save_video=False, + height=height, + width=width, + num_frames=num_frames, + fps=24, + num_inference_steps=num_inference_steps, + guidance_scale=1.0, + seed=seed, + ltx2_image_crf=0.0, + ) + if torch.cuda.is_available(): + torch.cuda.synchronize() + except Exception as exc: + return RunResult(wall_ms=(time.perf_counter() - t0) * 1000.0, + stage_times_ms=OrderedDict(), + peak_gpu_mb=0.0, + error=f"{type(exc).__name__}: {exc}") + wall_ms = (time.perf_counter() - t0) * 1000.0 + return RunResult( + wall_ms=wall_ms, + stage_times_ms=_extract_stage_times(result), + peak_gpu_mb=_peak_gpu_mb(), + ) + + +def benchmark_scenario(scenario: ScenarioConfig, model_path: str, num_gpus: int, prompt: str, num_runs: int, + num_frames: int, height: int, width: int, num_inference_steps: int, seed: int) -> ScenarioResult: + print() + print(f"=== scenario: {scenario.name} " + f"(compile={scenario.enable_compile} warmup={scenario.do_warmup}) ===") + + config = _build_generator_config(model_path, scenario.enable_compile, num_gpus) + generator = VideoGenerator.from_config(config) + + if scenario.do_warmup: + print(f"[{scenario.name}] warmup: 2 generate calls " + "(triggers compile + first-shape graphs)") + for warmup_idx in range(2): + t0 = time.perf_counter() + _do_one_run(generator, + prompt, + height=height, + width=width, + num_frames=num_frames, + seed=seed + 100 + warmup_idx, + num_inference_steps=num_inference_steps) + print(f"[{scenario.name}] warmup {warmup_idx + 1}: " + f"{(time.perf_counter() - t0) * 1000:.0f}ms") + + runs: list[RunResult] = [] + last_error: str | None = None + for run_idx in range(num_runs): + result = _do_one_run(generator, + prompt, + height=height, + width=width, + num_frames=num_frames, + seed=seed + run_idx, + num_inference_steps=num_inference_steps) + if result.error is not None: + print(f"[{scenario.name}] run {run_idx + 1}: " + f"ERROR {result.error}") + last_error = result.error + continue + playable_s = num_frames / 24.0 + rt = playable_s / (result.wall_ms / 1000.0) + print(f"[{scenario.name}] run {run_idx + 1}/{num_runs}: " + f"wall={result.wall_ms:.0f}ms peak={result.peak_gpu_mb:.0f}MB " + f"realtime={rt:.2f}x stages={len(result.stage_times_ms)}") + runs.append(result) + + if not runs: + return ScenarioResult(name=scenario.name, + enable_compile=scenario.enable_compile, + do_warmup=scenario.do_warmup, + runs=0, + wall_ms_median=0.0, + wall_ms_p95=0.0, + stage_means_ms=OrderedDict(), + realtime_ratio_median=0.0, + peak_gpu_mb_max=0.0, + error=last_error or "all runs failed") + + walls = [r.wall_ms for r in runs] + walls_sorted = sorted(walls) + p95_idx = max(0, int(round(0.95 * (len(walls_sorted) - 1)))) + stage_means: OrderedDict[str, float] = OrderedDict() + if runs: + for stage_name in runs[0].stage_times_ms: + vals = [r.stage_times_ms.get(stage_name, 0.0) for r in runs] + stage_means[stage_name] = sum(vals) / len(vals) + return ScenarioResult( + name=scenario.name, + enable_compile=scenario.enable_compile, + do_warmup=scenario.do_warmup, + runs=len(runs), + wall_ms_median=statistics.median(walls), + wall_ms_p95=walls_sorted[p95_idx], + stage_means_ms=stage_means, + realtime_ratio_median=(num_frames / 24.0) / (statistics.median(walls) / 1000.0), + peak_gpu_mb_max=max(r.peak_gpu_mb for r in runs), + ) + + +def _print_summary(results: list[ScenarioResult]) -> None: + print() + print("=== summary ===") + header = (f"{'scenario':14s} {'runs':>4s} " + f"{'wall_med_ms':>11s} {'wall_p95_ms':>11s} " + f"{'peak_mb':>8s} {'realtime':>8s}") + print(header) + print("-" * len(header)) + for r in results: + if r.error is not None: + print(f"{r.name:14s} {r.runs:>4d} ERR: {r.error}") + continue + print(f"{r.name:14s} {r.runs:>4d} " + f"{r.wall_ms_median:>11.0f} {r.wall_ms_p95:>11.0f} " + f"{r.peak_gpu_mb_max:>8.0f} {r.realtime_ratio_median:>7.2f}x") + for r in results: + if r.error is not None or not r.stage_means_ms: + continue + print() + print(f"=== {r.name} per-stage means (ms) ===") + for stage, mean_ms in r.stage_means_ms.items(): + pct = 100.0 * mean_ms / r.wall_ms_median if r.wall_ms_median > 0 \ + else 0.0 + print(f" {stage:35s} {mean_ms:>9.1f}ms ({pct:5.1f}%)") + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--model", default=DEFAULT_MODEL) + p.add_argument("--prompt", default=DEFAULT_PROMPT) + p.add_argument("--scenarios", + nargs="+", + default=[s.name for s in DEFAULT_SCENARIOS], + choices=[s.name for s in DEFAULT_SCENARIOS], + help="which scenarios to run") + p.add_argument("--runs", type=int, default=3) + p.add_argument("--num-frames", type=int, default=121) + p.add_argument("--height", type=int, default=1088) + p.add_argument("--width", type=int, default=1920) + p.add_argument("--num-inference-steps", type=int, default=5) + p.add_argument("--num-gpus", type=int, default=1) + p.add_argument("--gpu", type=int, default=None, help="set CUDA_VISIBLE_DEVICES to this single GPU index") + p.add_argument("--seed", type=int, default=10) + p.add_argument("--output-json", default=None, help="write structured results to this path") + args = p.parse_args() + + if args.gpu is not None: + os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu) + + selected = [s for s in DEFAULT_SCENARIOS if s.name in args.scenarios] + print(f"[bench] model={args.model} num_gpus={args.num_gpus}") + print(f"[bench] frames={args.num_frames} {args.width}x{args.height} " + f"steps={args.num_inference_steps} runs/scenario={args.runs}") + print(f"[bench] scenarios={[s.name for s in selected]}") + + results: list[ScenarioResult] = [] + for scenario in selected: + try: + result = benchmark_scenario(scenario, args.model, args.num_gpus, args.prompt, args.runs, args.num_frames, + args.height, args.width, args.num_inference_steps, args.seed) + except Exception as exc: + result = ScenarioResult(name=scenario.name, + enable_compile=scenario.enable_compile, + do_warmup=scenario.do_warmup, + runs=0, + wall_ms_median=0.0, + wall_ms_p95=0.0, + stage_means_ms=OrderedDict(), + realtime_ratio_median=0.0, + peak_gpu_mb_max=0.0, + error=f"{type(exc).__name__}: {exc}") + results.append(result) + + _print_summary(results) + + if args.output_json: + out = [] + for r in results: + entry = asdict(r) + entry["stage_means_ms"] = dict(r.stage_means_ms) + out.append(entry) + with open(args.output_json, "w") as f: + json.dump({"args": vars(args), "results": out}, f, indent=2) + print(f"[bench] wrote {args.output_json}") + + any_below = any(r.error is None and r.realtime_ratio_median < 1.0 for r in results) + return 1 if any_below else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/apps/dreamverse/dreamverse/config.py b/apps/dreamverse/dreamverse/config.py new file mode 100644 index 0000000000..7d77a53df2 --- /dev/null +++ b/apps/dreamverse/dreamverse/config.py @@ -0,0 +1,281 @@ +import os +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_SERVER_ROOT = Path(__file__).resolve().parent +_FASTVIDEO_DREAMVERSE_HOME = os.environ.get("FASTVIDEO_DREAMVERSE_HOME") +_XDG_STATE_HOME = os.environ.get("XDG_STATE_HOME") +_DEFAULT_STATE_ROOT = (Path(_FASTVIDEO_DREAMVERSE_HOME) if _FASTVIDEO_DREAMVERSE_HOME else + (Path(_XDG_STATE_HOME) if _XDG_STATE_HOME else Path.home() / ".local/state") / + "fastvideo/dreamverse") +_OUTPUTS_ROOT = _DEFAULT_STATE_ROOT / "outputs" +_PROMPTS_ROOT = _SERVER_ROOT / "prompts" +_PROMPTS_LOCAL_ROOT = _SERVER_ROOT / "prompts.local" +_APP_ROOT = _REPO_ROOT + + +def _resolve_frontend_root() -> Path: + for candidate in ( + _APP_ROOT / "web", + _APP_ROOT / "prod-ui", + ): + if candidate.is_dir(): + return candidate + return _APP_ROOT / "web" + + +FRONTEND_ROOT = _resolve_frontend_root() +_CLIENT_PROMPTS_ROOT = FRONTEND_ROOT / "prompts" +_CLIENT_PROMPTS_LOCAL_ROOT = FRONTEND_ROOT / "prompts.local" +FRONTEND_STATIC_DIR_CANDIDATES = tuple(str(FRONTEND_ROOT / dirname) for dirname in ("out", "dist")) + +# Model registry +MODEL_REGISTRY = { + "fast-ltx2": { + "name": "FastLTX2", + "model_path": "FastVideo/LTX2-Distilled-Diffusers", + "config_model_path": "FastVideo/LTX2-Distilled-Diffusers", + }, + "fast-ltx23": { + "name": "FastLTX23", + "model_path": "FastVideo/LTX-2.3-Distilled-Diffusers", + "config_model_path": "FastVideo/LTX-2.3-Distilled-Diffusers", + }, +} + +DEFAULT_MODEL_ID = "fast-ltx2" + +# Active model configuration +MODEL_CONFIG = MODEL_REGISTRY[DEFAULT_MODEL_ID] + +# Generation limits +SESSION_TIMEOUT_SECONDS = 300 + +# Frame settings +NUM_FRAMES = 121 +FRAME_HEIGHT = 1088 +FRAME_WIDTH = 1920 +NUM_INFERENCE_STEPS = 5 +JPEG_QUALITY = 100 +BATCH_SIZE = 3 + +# Streaming mode: +# - legacy_jpeg: send frame_batch JSON payloads with base64 JPEGs +# - av_fmp4: send muxed fMP4 binary chunks over WebSocket +STREAM_MODE = os.getenv("STREAM_MODE", "av_fmp4").strip().lower() + + +def _env_int(name: str, default: int) -> int: + value = os.getenv(name) + if value is None: + return default + try: + return int(value) + except ValueError: + return default + + +def _env_float(name: str, default: float) -> float: + value = os.getenv(name) + if value is None: + return default + try: + return float(value) + except ValueError: + return default + + +def _env_bool(name: str, default: bool) -> bool: + value = os.getenv(name) + if value is None: + return default + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + return default + + +def _env_choice(name: str, default: str, allowed: tuple[str, ...]) -> str: + value = os.getenv(name) + normalized = default.strip().lower() if value is None else value.strip().lower() + if normalized in allowed: + return normalized + allowed_values = ", ".join(allowed) + raise RuntimeError(f"Invalid {name}: {normalized!r}. Expected one of {allowed_values}.") + + +def _env_csv(name: str, default: str) -> list[str]: + raw = os.getenv(name, default) + values = [item.strip() for item in raw.split(",")] + unique_values: list[str] = [] + for value in values: + if not value or value in unique_values: + continue + unique_values.append(value) + return unique_values + + +def _required_env(*names: str) -> str: + for name in names: + value = os.getenv(name) + if not isinstance(value, str): + continue + normalized = value.strip() + if normalized: + return normalized + joined_names = ", ".join(names) + raise RuntimeError(f"Missing required environment variable: one of {joined_names}") + + +def _optional_env(*names: str) -> str | None: + for name in names: + value = os.getenv(name) + if not isinstance(value, str): + continue + normalized = value.strip() + if normalized: + return normalized + return None + + +DEVTOOLS_ENABLED = _env_bool("FASTVIDEO_ENABLE_DEVTOOLS", False) +PROMPT_SAFETY_ENABLED = _env_bool("FASTVIDEO_ENABLE_PROMPT_SAFETY", False) + + +def _resolve_devtools_paths( + default_path: Path, + overlay_path: Path, + env_name: str | None = None, +) -> tuple[str, str | None]: + env_value = os.getenv(env_name) if env_name else None + if isinstance(env_value, str) and env_value.strip(): + return env_value.strip(), None + if DEVTOOLS_ENABLED: + return str(overlay_path), str(default_path) + return str(default_path), None + + +# Prompt LLM configuration. +PROMPT_SUPPORTED_PROVIDERS = ( + "cerebras", + "groq", +) +if os.getenv("FASTVIDEO_PROMPT_PROVIDER") is not None: + _env_choice( + "FASTVIDEO_PROMPT_PROVIDER", + "cerebras", + PROMPT_SUPPORTED_PROVIDERS, + ) +PROMPT_PROVIDER = "cerebras" +PROMPT_PROVIDER_RUNTIME_STAGES = (("cerebras", "groq"), ) +PROMPT_PROVIDER_PRIORITY = ( + "cerebras", + "groq", +) +PROMPT_PROVIDER_API_KEY_NAMES = { + "cerebras": ("CEREBRAS_API_KEY", ), + "groq": ("GROQ_API_KEY", ), +} +PROMPT_API_KEYS = { + provider: _optional_env(*PROMPT_PROVIDER_API_KEY_NAMES[provider]) + for provider in PROMPT_SUPPORTED_PROVIDERS +} +PROMPT_API_BASE_URLS = { + "cerebras": (os.getenv("FASTVIDEO_PROMPT_CEREBRAS_API_BASE_URL", "").strip() or None), + "groq": (os.getenv( + "FASTVIDEO_PROMPT_GROQ_API_BASE_URL", + "https://api.groq.com/openai/v1", + ).strip() or None), +} +PROMPT_API_KEY = PROMPT_API_KEYS[PROMPT_PROVIDER] +PROMPT_API_BASE_URL = PROMPT_API_BASE_URLS[PROMPT_PROVIDER] +PROMPT_MODEL = (os.getenv("FASTVIDEO_PROMPT_MODEL", "gpt-oss-120b").strip() or "gpt-oss-120b") +_PROMPT_CEREBRAS_REQUEST_MODEL = (os.getenv("FASTVIDEO_PROMPT_CEREBRAS_MODEL", PROMPT_MODEL).strip() or PROMPT_MODEL) +PROMPT_PROVIDER_MODELS = { + "cerebras": _PROMPT_CEREBRAS_REQUEST_MODEL, + "groq": (os.getenv( + "FASTVIDEO_PROMPT_GROQ_MODEL", + f"openai/{PROMPT_MODEL}", + ).strip() or f"openai/{PROMPT_MODEL}"), +} +PROMPT_REWRITE_MODEL = PROMPT_MODEL +PROMPT_REWRITE_MODEL_OPTIONS = [PROMPT_REWRITE_MODEL] +PROMPT_TIMEOUT_MS = 20000 +PROMPT_HTTP_TIMEOUT_MS = 3000 +PROMPT_INITIAL_STAGE_TIMEOUT_MS = 1500 +PROMPT_TEMPERATURE = 1.0 +PROMPT_MAX_COMPLETION_TOKENS = 3000 +( + PROMPT_ENHANCE_SYSTEM_PROMPT_PATH, + PROMPT_ENHANCE_SYSTEM_PROMPT_FALLBACK_PATH, +) = _resolve_devtools_paths( + _PROMPTS_ROOT / "next_segment_system_prompt.md", + _PROMPTS_LOCAL_ROOT / "next_segment_system_prompt.md", + "FASTVIDEO_PROMPT_ENHANCE_SYSTEM_PROMPT_PATH", +) +( + PROMPT_AUTO_SYSTEM_PROMPT_PATH, + PROMPT_AUTO_SYSTEM_PROMPT_FALLBACK_PATH, +) = _resolve_devtools_paths( + _PROMPTS_ROOT / "auto_extension_system_prompt.md", + _PROMPTS_LOCAL_ROOT / "auto_extension_system_prompt.md", + "FASTVIDEO_PROMPT_AUTO_SYSTEM_PROMPT_PATH", +) +( + PROMPT_REWRITE_ALL_SYSTEM_PROMPT_PATH, + PROMPT_REWRITE_ALL_SYSTEM_PROMPT_FALLBACK_PATH, +) = _resolve_devtools_paths( + _PROMPTS_ROOT / "rewrite_window_system_prompt.md", + _PROMPTS_LOCAL_ROOT / "rewrite_window_system_prompt.md", + "FASTVIDEO_PROMPT_REWRITE_ALL_SYSTEM_PROMPT_PATH", +) +( + PROMPT_REWRITE_USER_SYSTEM_PROMPT_PATH, + PROMPT_REWRITE_USER_SYSTEM_PROMPT_FALLBACK_PATH, +) = _resolve_devtools_paths( + _PROMPTS_ROOT / "rewrite_user_system_prompt.md", + _PROMPTS_LOCAL_ROOT / "rewrite_user_system_prompt.md", + "FASTVIDEO_PROMPT_REWRITE_USER_SYSTEM_PROMPT_PATH", +) +( + CURATED_PRESETS_FILE_PATH, + CURATED_PRESETS_FALLBACK_FILE_PATH, +) = _resolve_devtools_paths( + _CLIENT_PROMPTS_ROOT / "selected_ltx2_continuation_story_presets.json", + _CLIENT_PROMPTS_LOCAL_ROOT / "selected_ltx2_continuation_story_presets.json", + "FASTVIDEO_CURATED_PRESETS_FILE_PATH", +) +PROMPT_REWRITE_LOG_PATH = os.getenv( + "FASTVIDEO_PROMPT_REWRITE_LOG_PATH", + str(_OUTPUTS_ROOT / "prompt_rewrite.jsonl"), +).strip() +PROMPT_ENHANCE_LOG_PATH = os.getenv( + "FASTVIDEO_PROMPT_ENHANCE_LOG_PATH", + str(_OUTPUTS_ROOT / "prompt_enhance.jsonl"), +).strip() +PROMPT_AUTO_EXTENSION_LOG_PATH = os.getenv( + "FASTVIDEO_PROMPT_AUTO_EXTENSION_LOG_PATH", + str(_OUTPUTS_ROOT / "prompt_auto_extension.jsonl"), +).strip() +SESSION_LOG_ROOT = os.getenv( + "FASTVIDEO_SESSION_LOG_ROOT", + str(_OUTPUTS_ROOT / "session_logs"), +).strip() + +# Auto extension behavior. +# Sleep is used as idle backoff when no prompt source is available. +PROMPT_AUTO_SLEEP_MS = _env_int("FASTVIDEO_PROMPT_AUTO_SLEEP_MS", 120) +PROMPT_AUTO_TIMEOUT_MS = _env_int("FASTVIDEO_PROMPT_AUTO_TIMEOUT_MS", 1800) +GENERATION_SEGMENT_CAP = max(0, _env_int("FASTVIDEO_GENERATION_SEGMENT_CAP", 6)) + +# Startup warmup behavior. Warmup compiles segment 1 and segment 2 inference +# paths before the worker is considered ready for serving. +STARTUP_WARMUP_ENABLED = _env_bool("FASTVIDEO_ENABLE_STARTUP_WARMUP", True) +STARTUP_WARMUP_PROMPT = os.getenv( + "FASTVIDEO_STARTUP_WARMUP_PROMPT", + ("A cinematic drone shot over coastal cliffs at sunrise, " + "golden light, gentle ocean waves, ultra detailed"), +).strip() +STARTUP_WARMUP_TIMEOUT_SECONDS = max(1, _env_int("FASTVIDEO_STARTUP_WARMUP_TIMEOUT_SECONDS", 2400)) diff --git a/apps/dreamverse/dreamverse/gpu_pool.py b/apps/dreamverse/dreamverse/gpu_pool.py new file mode 100644 index 0000000000..0d14191033 --- /dev/null +++ b/apps/dreamverse/dreamverse/gpu_pool.py @@ -0,0 +1,961 @@ +# pyright: reportMissingTypeArgument=false, reportArgumentType=false, reportOptionalSubscript=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportUndefinedVariable=false +# ruff: noqa: UP038, SIM105, F821 +# mypy: ignore-errors +import asyncio +import multiprocessing as mp +import os +import subprocess +import time +import traceback +from dataclasses import dataclass +from enum import Enum +from multiprocessing import Process, Queue + +from dreamverse.config import ( + DEFAULT_MODEL_ID, + MODEL_REGISTRY, + STARTUP_WARMUP_ENABLED, + STARTUP_WARMUP_PROMPT, + STARTUP_WARMUP_TIMEOUT_SECONDS, +) +from dreamverse.av_streaming import ( + SHARED_STREAM_BUFFER_BYTES, + USE_SHARED_STREAM_BUFFER, + StreamChunk, + StreamComplete, + StreamEvent, + StreamInit, + generate_stream_id, + stream_fmp4, +) +from dreamverse.worker_ipc import ( + CommandPayload, + InitAck, + JoinAck, + LeaveAck, + MediaChunk, + MediaComplete, + MediaInit, + ReloadAck, + ReloadModelPayload, + ShutdownAck, + StepComplete, + UserStepPayload, + WarmupComplete, + WarmupPayload, + WorkerError, + WorkerEvent, +) + + +def _parse_requested_gpu_limit() -> int | None: + raw_value = os.getenv("FASTVIDEO_GPU_COUNT", "").strip().lower() + if not raw_value: + return 1 + if raw_value == "all": + return None + try: + requested = int(raw_value) + except ValueError as exc: + raise RuntimeError("Invalid FASTVIDEO_GPU_COUNT. Use a positive integer or 'all'.") from exc + if requested <= 0: + raise RuntimeError("Invalid FASTVIDEO_GPU_COUNT. Use a positive integer or 'all'.") + return requested + + +def _limit_gpu_ids(gpu_ids: list[int]) -> list[int]: + requested_limit = _parse_requested_gpu_limit() + print(f"[INFO] Using GPU limit={requested_limit}") + if requested_limit is None: + return gpu_ids + return gpu_ids[:requested_limit] or gpu_ids + + +class CommandType(Enum): + """Commands sent from main process to GPU worker.""" + INIT = "init" + WARMUP = "warmup" + SHUTDOWN = "shutdown" + USER_JOIN = "user_join" + USER_STEP = "user_step" + USER_LEAVE = "user_leave" + RELOAD_MODEL = "reload_model" + + +@dataclass +class Command: + """Command sent to GPU worker subprocess. + + Commands that carry data (USER_STEP, WARMUP, RELOAD_MODEL) + populate ``payload`` with a typed payload from ``worker_ipc``. + Commands that don't (INIT, SHUTDOWN, USER_JOIN, USER_LEAVE) + leave ``payload`` as ``None``. + """ + type: CommandType + payload: CommandPayload | None = None + user_id: str | None = None + + +def _stream_event_to_worker_event( + event: StreamEvent, + user_id: str, + segment_idx: int, +) -> WorkerEvent: + """Translate an av_streaming event into a typed worker event. + + ``StreamEvent`` (ffmpeg output layer) carries no routing info; + this adds ``user_id`` + ``segment_idx`` so the pool's + ``_response_reader`` can dispatch to the right per-user queue. + """ + match event: + case StreamInit(stream_id=sid, mime=m, uses_shared_buffer=u): + return MediaInit( + user_id=user_id, + segment_idx=segment_idx, + stream_id=sid, + mime=m, + uses_shared_buffer=u, + ) + case StreamChunk( + stream_id=sid, + chunk=c, + chunk_offset=co, + chunk_length=cl, + uses_shared_buffer=u, + ): + return MediaChunk( + user_id=user_id, + segment_idx=segment_idx, + stream_id=sid, + chunk=c, + chunk_offset=co, + chunk_length=cl, + uses_shared_buffer=u, + ) + case StreamComplete(stream_id=sid, chunks=n): + return MediaComplete( + user_id=user_id, + segment_idx=segment_idx, + stream_id=sid, + chunks=n, + ) + case _: + raise ValueError(f"unknown stream event: {type(event).__name__}") + + +def gpu_worker_process( + gpu_id: int, + cuda_device: str, + command_queue: Queue, + response_queue: Queue, + shared_stream_buffer=None, + shared_stream_buffer_bytes: int = 0, +): + """Worker process that runs on a single GPU. + + CUDA_VISIBLE_DEVICES must be set BEFORE importing VideoGenerationWorker + (which transitively touches CUDA). Delegates model lifecycle and + generation to VideoGenerationWorker; AV muxing to av_streaming. + """ + os.environ["CUDA_VISIBLE_DEVICES"] = cuda_device + os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN" + + from dreamverse.video_generation import VideoGenerationWorker + + worker = VideoGenerationWorker(gpu_id) + + def event_loop(first_cmd: Command = None): + """Blocking event loop for LTX2; dispatches user commands.""" + print(f"[GPU {gpu_id}] Entering event loop") + + def handle_command(cmd: Command): + if cmd.type == CommandType.USER_JOIN: + print(f"[GPU {gpu_id}] User {cmd.user_id[:8]} joined") + worker.clear_conditioning() + response_queue.put(JoinAck(user_id=cmd.user_id)) + + elif cmd.type == CommandType.USER_STEP: + try: + assert isinstance(cmd.payload, UserStepPayload), (f"USER_STEP requires UserStepPayload, " + f"got {type(cmd.payload).__name__}") + payload = cmd.payload + segment_idx = payload.segment_idx + step_result = worker.generate_step( + payload.prompt, + segment_idx, + image_path=payload.image_path, + reset_conditioning=payload.reset_conditioning, + ) + head_trim_frames = step_result.head_trim_frames + head_trim_audio_frames = step_result.head_trim_audio_frames + if head_trim_frames > 0 or head_trim_audio_frames > 0: + print(f"[GPU {gpu_id}] Segment {segment_idx}: " + f"trimming video={head_trim_frames} " + f"audio={head_trim_audio_frames} " + f"overlap frames from AV output") + audio_shape = getattr(step_result.audio, "shape", None) + print(f"[GPU {gpu_id}] AV attempt segment " + f"{segment_idx}: " + f"audio_present={step_result.audio is not None}, " + f"audio_shape={audio_shape}, " + f"audio_sample_rate={step_result.audio_sample_rate}") + stream_id = generate_stream_id(segment_idx) + + def _publish(event: StreamEvent) -> None: + response_queue.put(_stream_event_to_worker_event(event, cmd.user_id, segment_idx)) + + av_ok, av_error = stream_fmp4( + frames=step_result.frames, + audio=step_result.audio, + audio_sample_rate=step_result.audio_sample_rate, + stream_id=stream_id, + timings=step_result.timings, + head_trim_frames=head_trim_frames, + head_trim_audio_frames=head_trim_audio_frames, + shared_buffer=shared_stream_buffer, + shared_buffer_bytes=shared_stream_buffer_bytes, + publish=_publish, + log_prefix=f"[GPU {gpu_id}]", + ) + if not av_ok: + raise RuntimeError(av_error or "worker av_fmp4 stream failed") + print(f"[GPU {gpu_id}] AV streamed segment {segment_idx}: " + f"encode_total={step_result.timings.get('av_encode_stream_ms', 0):.0f}ms " + f"wav_write={step_result.timings.get('av_wav_write_ms', 0):.1f}ms " + f"spawn={step_result.timings.get('av_ffmpeg_spawn_ms', 0):.1f}ms " + f"first_chunk={step_result.timings.get('av_first_chunk_ms', 0):.0f}ms " + f"chunk_interval_med={step_result.timings.get('av_chunk_interval_ms_median', 0):.1f}ms " + f"chunk_interval_p95={step_result.timings.get('av_chunk_interval_ms_p95', 0):.1f}ms " + f"publish_med={step_result.timings.get('av_chunk_publish_ms_median', 0):.2f}ms " + f"read_med={step_result.timings.get('av_chunk_read_ms_median', 0):.1f}ms") + step_result.timings["ipc_put_start_ns"] = time.time_ns() + response_queue.put( + StepComplete( + user_id=cmd.user_id, + segment_idx=segment_idx, + timings=step_result.timings, + )) + except Exception as e: + print(f"[GPU {gpu_id}] Step error: {e}") + traceback.print_exc() + response_queue.put(WorkerError( + user_id=cmd.user_id, + message=str(e), + )) + + elif cmd.type == CommandType.USER_LEAVE: + print(f"[GPU {gpu_id}] User {cmd.user_id[:8]} left") + worker.clear_conditioning() + response_queue.put(LeaveAck(user_id=cmd.user_id)) + + elif cmd.type == CommandType.SHUTDOWN: + return False # Signal to exit + + elif cmd.type == CommandType.WARMUP: + try: + assert isinstance(cmd.payload, WarmupPayload), (f"WARMUP requires WarmupPayload, " + f"got {type(cmd.payload).__name__}") + timings = worker.warmup(cmd.payload.prompt) + response_queue.put(WarmupComplete( + user_id=cmd.user_id, + timings=timings, + )) + except Exception as e: + print(f"[GPU {gpu_id}] Warmup error: {e}") + traceback.print_exc() + response_queue.put(WorkerError( + user_id=cmd.user_id, + message=str(e), + )) + + elif cmd.type == CommandType.RELOAD_MODEL: + try: + assert isinstance(cmd.payload, ReloadModelPayload), (f"RELOAD_MODEL requires ReloadModelPayload, " + f"got {type(cmd.payload).__name__}") + worker.initialize(cmd.payload.model_config) + response_queue.put(ReloadAck(user_id=cmd.user_id)) + except Exception as e: + print(f"[GPU {gpu_id}] Reload error: {e}") + traceback.print_exc() + response_queue.put(WorkerError( + user_id=cmd.user_id, + message=str(e), + )) + + return True # Continue loop + + if first_cmd is not None: + if first_cmd.type == CommandType.SHUTDOWN: + worker.shutdown() + response_queue.put(ShutdownAck()) + return + handle_command(first_cmd) + + import queue as queue_module + while True: + try: + cmd = command_queue.get(timeout=1.0) + except queue_module.Empty: + continue + + if cmd.type == CommandType.SHUTDOWN: + print(f"[GPU {gpu_id}] Event loop shutting down") + worker.shutdown() + response_queue.put(ShutdownAck()) + return + + if not handle_command(cmd): + return + + print(f"[GPU {gpu_id}] Worker process starting...") + + try: + while True: + cmd: Command = command_queue.get() + + if cmd.type == CommandType.SHUTDOWN: + print(f"[GPU {gpu_id}] Shutting down...") + worker.shutdown() + response_queue.put(ShutdownAck()) + break + + elif cmd.type == CommandType.INIT: + try: + worker.initialize() + response_queue.put(InitAck(success=True)) + except Exception as e: + print(f"[GPU {gpu_id}] Init error: {e}") + traceback.print_exc() + response_queue.put(InitAck(success=False, error=str(e))) + + elif cmd.type == CommandType.WARMUP: + try: + assert isinstance(cmd.payload, WarmupPayload), (f"WARMUP requires WarmupPayload, " + f"got {type(cmd.payload).__name__}") + timings = worker.warmup(cmd.payload.prompt) + response_queue.put(WarmupComplete( + user_id=cmd.user_id, + timings=timings, + )) + except Exception as e: + print(f"[GPU {gpu_id}] Warmup error: {e}") + traceback.print_exc() + response_queue.put(WorkerError( + user_id=cmd.user_id, + message=str(e), + )) + + elif cmd.type == CommandType.RELOAD_MODEL: + try: + assert isinstance(cmd.payload, ReloadModelPayload), (f"RELOAD_MODEL requires ReloadModelPayload, " + f"got {type(cmd.payload).__name__}") + worker.initialize(cmd.payload.model_config) + response_queue.put(ReloadAck(user_id=cmd.user_id)) + except Exception as e: + print(f"[GPU {gpu_id}] Reload error: {e}") + traceback.print_exc() + response_queue.put(WorkerError( + user_id=cmd.user_id, + message=str(e), + )) + + elif cmd.type in (CommandType.USER_JOIN, CommandType.USER_STEP, CommandType.USER_LEAVE): + event_loop(first_cmd=cmd) + break + + except Exception as e: + print(f"[GPU {gpu_id}] Worker crashed: {e}") + traceback.print_exc() + + print(f"[GPU {gpu_id}] Worker process exiting") + + +class GPUSlot: + """Manages a single GPU worker subprocess.""" + + def __init__(self, gpu_id: int, cuda_device: str): + self.gpu_id = gpu_id + self.cuda_device = cuda_device + self.process: Process | None = None + self.command_queue: Queue | None = None + self.response_queue: Queue | None = None + self.ready: bool = False + self.warmup_enabled: bool = STARTUP_WARMUP_ENABLED + self.warmup_success: bool = False + self.warmup_error: str | None = None + self.warmup_timings: dict[str, float] = {} + self._lock = asyncio.Lock() + + # Client state + self.connected_users: set[str] = set() + self._pending_futures: dict[str, asyncio.Future] = {} + self._stream_queues: dict[str, asyncio.Queue] = {} + self._response_reader_task: asyncio.Task | None = None + self._active: bool = False + self._reader_lock: asyncio.Lock | None = None + self.current_model_id: str = DEFAULT_MODEL_ID + self.shared_stream_buffer = None + self.shared_stream_buffer_size = SHARED_STREAM_BUFFER_BYTES + + @property + def client_count(self) -> int: + return len(self.connected_users) + + @property + def is_available(self) -> bool: + """A GPU is available if it has no active users.""" + alive = self.ready and self.process is not None and self.process.is_alive() + if not alive: + return False + return len(self.connected_users) == 0 + + @property + def is_empty(self) -> bool: + return len(self.connected_users) == 0 + + async def start(self): + """Start the GPU worker subprocess.""" + self.ready = False + self.warmup_success = False + self.warmup_error = None + self.warmup_timings = {} + + ctx = mp.get_context("spawn") + self.command_queue = ctx.Queue() + self.response_queue = ctx.Queue() + if USE_SHARED_STREAM_BUFFER and self.shared_stream_buffer_size > 0: + # Fixed shared byte buffer to avoid per-chunk IPC payload copies. + self.shared_stream_buffer = mp.RawArray("B", self.shared_stream_buffer_size) + + self.process = ctx.Process( + target=gpu_worker_process, + args=( + self.gpu_id, + self.cuda_device, + self.command_queue, + self.response_queue, + self.shared_stream_buffer, + self.shared_stream_buffer_size, + ), + daemon=False, + ) + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self.process.start) + + # Send init command and wait for response + init_response = await self._send_command(Command(CommandType.INIT), timeout=600.0) + if not isinstance(init_response, InitAck) or not init_response.success: + error_msg = (init_response.error if isinstance(init_response, InitAck) else + f"unexpected init response: {type(init_response).__name__}") + raise RuntimeError(f"GPU {self.gpu_id} failed to initialize: {error_msg}") + + if self.warmup_enabled: + try: + warmup_response = await self._send_command( + Command( + CommandType.WARMUP, + payload=WarmupPayload(prompt=STARTUP_WARMUP_PROMPT), + user_id=f"__warmup_gpu_{self.gpu_id}__", + ), + timeout=float(STARTUP_WARMUP_TIMEOUT_SECONDS), + ) + except Exception as exc: + self.warmup_error = str(exc) + raise RuntimeError(f"GPU {self.gpu_id} warmup failed: {self.warmup_error}") from exc + match warmup_response: + case WarmupComplete(timings=timings): + self.warmup_timings = { + key: float(value) + for key, value in timings.items() if isinstance(value, (int, float)) + } + self.warmup_success = True + case WorkerError(message=msg): + self.warmup_error = msg or "Warmup failed." + raise RuntimeError(f"GPU {self.gpu_id} warmup failed: {self.warmup_error}") + case _: + self.warmup_error = (f"unexpected warmup response: " + f"{type(warmup_response).__name__}") + raise RuntimeError(f"GPU {self.gpu_id} warmup failed: {self.warmup_error}") + else: + print(f"[GPU {self.gpu_id}] Startup warmup disabled by " + "FASTVIDEO_ENABLE_STARTUP_WARMUP") + + self.ready = True + + async def _send_command(self, cmd: Command, timeout: float = 300.0) -> WorkerEvent: + """Send a command and wait for the response or worker death. + + Whichever arrives first wins. A worker dying (exit, signal, OOM, + segfault) flips the kernel-level sentinel fd readable, which + asyncio notices via add_reader — typically within ~10 ms. The + queue timeout still bounds hangs where the worker stays alive but + never replies. + """ + loop = asyncio.get_running_loop() + process = self.process + await loop.run_in_executor(None, self.command_queue.put, cmd) + + response_fut = loop.run_in_executor(None, lambda: self.response_queue.get(timeout=timeout)) + + death_fut: asyncio.Future | None = None + sentinel_fd = process.sentinel if process is not None else None + + if sentinel_fd is not None: + death_fut = loop.create_future() + + def _on_death() -> None: + try: + loop.remove_reader(sentinel_fd) + except (ValueError, OSError): + pass + if death_fut is not None and not death_fut.done(): + death_fut.set_result(None) + + loop.add_reader(sentinel_fd, _on_death) + + waiters = [response_fut] + ([death_fut] if death_fut is not None else []) + try: + done, _ = await asyncio.wait(waiters, return_when=asyncio.FIRST_COMPLETED) + finally: + if (sentinel_fd is not None and death_fut is not None and not death_fut.done()): + try: + loop.remove_reader(sentinel_fd) + except (ValueError, OSError): + pass + death_fut.cancel() + + if (death_fut is not None and death_fut in done and response_fut not in done): + try: + return self.response_queue.get_nowait() + except Exception: + pass + self.ready = False + pid = process.pid if process is not None else "?" + # Reap the process so .exitcode is populated. Sentinel + # readability means the kernel has already exited the + # process; join is non-blocking in practice. + if process is not None: + try: + process.join(timeout=1) + except Exception: + pass + exitcode = process.exitcode if process is not None else None + raise RuntimeError(f"GPU {self.gpu_id} worker died during command " + f"(pid={pid}, exitcode={exitcode})") + + return response_fut.result() + + async def _send_command_tagged(self, cmd: Command, timeout: float = 300.0) -> WorkerEvent: + """Send a tagged command and wait for the matching response. + + Uses the response reader background task to route responses. + """ + loop = asyncio.get_event_loop() + future = loop.create_future() + + # Register this request's pending future + self._pending_futures[cmd.user_id] = future + + # Send command + await loop.run_in_executor(None, self.command_queue.put, cmd) + + # Ensure response reader is running + await self._ensure_response_reader() + + # Wait for the response + try: + response = await asyncio.wait_for(future, timeout=timeout) + except asyncio.TimeoutError: + if (cmd.user_id in self._pending_futures and self._pending_futures[cmd.user_id] is future): + self._pending_futures.pop(cmd.user_id, None) + raise + + return response + + async def _ensure_response_reader(self): + """Start the response reader background task if not running.""" + if self._reader_lock is None: + self._reader_lock = asyncio.Lock() + + async with self._reader_lock: + if (self._response_reader_task is None or self._response_reader_task.done()): + self._response_reader_task = asyncio.create_task(self._response_reader()) + + async def _response_reader(self): + """Background task that reads response queue and routes to per-user futures.""" + loop = asyncio.get_event_loop() + + while self._active: + try: + + def get_response_nonblocking(): + try: + return self.response_queue.get(timeout=0.5) + except Exception: + return None + + event = await loop.run_in_executor(None, get_response_nonblocking) + + if event is None: + continue + + # AV streaming events → route to the user's stream queue. + if isinstance(event, (MediaInit, MediaChunk, MediaComplete)): + stream_queue = self._stream_queues.get(event.user_id) + if stream_queue is not None: + await stream_queue.put(event) + else: + print(f"[GPU {self.gpu_id}] Unmatched stream event for user " + f"{event.user_id[:8]}") + continue + + # System-level acks shouldn't reach the tagged router; they + # belong to the untagged `_send_command` path. + if isinstance(event, (InitAck, ShutdownAck)): + print(f"[GPU {self.gpu_id}] System event leaked into " + f"tagged reader: {type(event).__name__}") + continue + + # Late-mutation of timings for observability. Only events + # that actually carry timings get this annotation. + if isinstance(event, (StepComplete, WarmupComplete)): + event.timings["ipc_get_done_ns"] = time.time_ns() + + user_id = event.user_id + if user_id and user_id in self._pending_futures: + future = self._pending_futures.pop(user_id) + if not future.done(): + future.set_result(event) + else: + print(f"[GPU {self.gpu_id}] Unmatched response for user " + f"{user_id[:8] if user_id else 'None'}") + except asyncio.CancelledError: + break + except Exception as e: + print(f"[GPU {self.gpu_id}] Response reader error: {e}") + await asyncio.sleep(0.01) + + def register_stream_queue(self, user_id: str) -> asyncio.Queue: + """Register stream-event queue for a specific user.""" + queue = asyncio.Queue() + self._stream_queues[user_id] = queue + return queue + + def unregister_stream_queue(self, user_id: str) -> None: + """Remove stream-event queue for a specific user.""" + self._stream_queues.pop(user_id, None) + + async def join_user(self, user_id: str, model_id: str = None) -> JoinAck: + """Add a user to this GPU.""" + if model_id is None: + model_id = DEFAULT_MODEL_ID + + # Reload model if a different one is requested + if model_id != self.current_model_id and model_id in MODEL_REGISTRY: + print(f"[GPU {self.gpu_id}] Model switch: " + f"{self.current_model_id} -> {model_id}") + + for uid, future in list(self._pending_futures.items()): + if not future.done(): + future.set_exception(RuntimeError("Model changed, session reset")) + self._pending_futures.clear() + self._stream_queues.clear() + self.connected_users.clear() + + model_config = MODEL_REGISTRY[model_id] + reload_response = await self._send_command(Command(CommandType.RELOAD_MODEL, + payload=ReloadModelPayload(model_config=model_config), + user_id="__reload__"), + timeout=600.0) + match reload_response: + case ReloadAck(): + pass + case WorkerError(message=msg): + raise RuntimeError(f"Model reload failed: {msg}") + case _: + raise RuntimeError(f"Unexpected reload response: " + f"{type(reload_response).__name__}") + + self.current_model_id = model_id + print(f"[GPU {self.gpu_id}] Model reloaded: {model_id}") + + self._active = True + self.connected_users.add(user_id) + try: + response = await self._send_command_tagged(Command(CommandType.USER_JOIN, user_id=user_id), timeout=600.0) + match response: + case JoinAck() as ack: + return ack + case WorkerError(message=msg): + self.connected_users.discard(user_id) + raise RuntimeError(f"User join failed for {user_id[:8]}: {msg}") + case _: + self.connected_users.discard(user_id) + raise RuntimeError(f"Unexpected join response for {user_id[:8]}: " + f"{type(response).__name__}") + except Exception: + self.connected_users.discard(user_id) + raise + + async def user_step( + self, + user_id: str, + prompt: str, + segment_idx: int = 1, + image_path: str | None = None, + reset_conditioning: bool = False, + ) -> dict[str, float]: + """Execute a generation step for a specific user. + + Returns the timings dict. Frames/audio are no longer part of + this return — they stream asynchronously via the AV media + events (MediaInit/MediaChunk/MediaComplete) which the caller + consumes through ``register_stream_queue``. + """ + payload = UserStepPayload( + prompt=prompt, + segment_idx=segment_idx, + image_path=image_path, + reset_conditioning=bool(reset_conditioning), + ) + response = await self._send_command_tagged(Command(CommandType.USER_STEP, payload=payload, user_id=user_id), + timeout=1800.0) + match response: + case StepComplete(timings=timings): + return timings + case WorkerError(message=msg): + raise RuntimeError(f"User step failed for {user_id[:8]}: {msg}") + case _: + raise RuntimeError(f"Unexpected step response for {user_id[:8]}: " + f"{type(response).__name__}") + + async def leave_user(self, user_id: str) -> None: + """Remove a user from this GPU.""" + try: + await self._send_command_tagged(Command(CommandType.USER_LEAVE, user_id=user_id), timeout=30.0) + except Exception as e: + print(f"[GPU {self.gpu_id}] Leave user error: {e}") + finally: + self.connected_users.discard(user_id) + self._pending_futures.pop(user_id, None) + self._stream_queues.pop(user_id, None) + + async def shutdown(self): + """Shutdown the worker subprocess.""" + self._active = False + if self._response_reader_task and not self._response_reader_task.done(): + self._response_reader_task.cancel() + try: + await self._response_reader_task + except asyncio.CancelledError: + pass + + if self.process is not None: + if self.process.is_alive(): + try: + await self._send_command(Command(CommandType.SHUTDOWN), timeout=30.0) + except Exception: + pass + self.process.terminate() + self.process.join(timeout=5) + if self.process.is_alive(): + self.process.kill() + self.process.join(timeout=5) + else: + # Process already died (sentinel path). Reap it so the + # OS releases the PID. + try: + self.process.join(timeout=1) + except Exception: + pass + + for q in (self.command_queue, self.response_queue): + if q is not None: + try: + q.close() + except Exception: + pass + try: + q.join_thread() + except Exception: + pass + + +class GPUPool: + """Manages multiple GPU worker subprocesses.""" + + def __init__(self, gpu_ids: list[int]): + self.gpu_ids = gpu_ids + self.slots: dict[int, GPUSlot] = {gpu_id: GPUSlot(gpu_id, str(gpu_id)) for gpu_id in gpu_ids} + self.waiting_list: list[tuple[str, asyncio.Event, WebSocket]] = [] + self.client_gpu_map: dict[str, int] = {} + self._pool_lock = asyncio.Lock() + + async def initialize(self): + """Initialize all GPU workers and wait for them to be ready. + + Any per-GPU failure aborts startup so uvicorn refuses to serve + traffic with no functional GPUs. + """ + print(f"Initializing GPU pool with {len(self.gpu_ids)} GPUs: {self.gpu_ids}") + await asyncio.gather(*(self._init_gpu(gpu_id) for gpu_id in self.gpu_ids)) + + async def _init_gpu(self, gpu_id: int): + """Initialize a single GPU and assign any waiting clients.""" + try: + await self.slots[gpu_id].start() + except Exception as e: + print(f"GPU {gpu_id} failed to initialize: {e}") + await self.slots[gpu_id].shutdown() + raise + + print(f"GPU pool: {gpu_id} ready " + f"({sum(1 for s in self.slots.values() if s.ready)}/{len(self.gpu_ids)})") + + # Check if anyone is waiting for a GPU + async with self._pool_lock: + slot = self.slots[gpu_id] + if slot.is_available and self.waiting_list: + waiting_client_id, ready_event, _ = self.waiting_list.pop(0) + self.client_gpu_map[waiting_client_id] = gpu_id + print(f"Client {waiting_client_id[:8]} assigned GPU {gpu_id} from queue") + ready_event.set() + + await self._send_queue_updates() + + async def acquire(self, client_id: str, websocket=None) -> tuple[int, GPUSlot]: + """Acquire a GPU slot for a client.""" + async with self._pool_lock: + for gpu_id, slot in self.slots.items(): + if slot.is_available: + self.client_gpu_map[client_id] = gpu_id + print(f"Client {client_id[:8]} acquired GPU {gpu_id}") + return gpu_id, slot + + # No slot available, wait in queue + print(f"Client {client_id[:8]} waiting in queue " + f"(all {len(self.gpu_ids)} GPUs at capacity)") + ready_event = asyncio.Event() + async with self._pool_lock: + self.waiting_list.append((client_id, ready_event, websocket)) + await self._send_queue_updates() + + try: + await ready_event.wait() + except asyncio.CancelledError: + # Client disconnected while queued; remove stale queue entry. + async with self._pool_lock: + self.waiting_list = [ + item for item in self.waiting_list if not (item[0] == client_id and item[1] is ready_event) + ] + self.client_gpu_map.pop(client_id, None) + await self._send_queue_updates() + raise + + gpu_id = self.client_gpu_map.get(client_id) + if gpu_id is None: + raise RuntimeError(f"Client {client_id} was signaled but has no GPU assigned") + + return gpu_id, self.slots[gpu_id] + + async def release(self, client_id: str): + """Release a client from its GPU slot.""" + async with self._pool_lock: + # Remove stale queue entries if the client disconnected while waiting. + prev_wait_len = len(self.waiting_list) + self.waiting_list = [item for item in self.waiting_list if item[0] != client_id] + removed_from_queue = len(self.waiting_list) != prev_wait_len + + gpu_id = self.client_gpu_map.pop(client_id, None) + if gpu_id is None: + if removed_from_queue: + await self._send_queue_updates() + return + + slot = self.slots[gpu_id] + print(f"Client {client_id[:8]} released GPU {gpu_id}") + + try: + await slot.leave_user(client_id) + except Exception as e: + print(f"[GPU {gpu_id}] Leave user failed: {e}") + + # Assign to next waiting client if GPU has capacity + if slot.is_available and self.waiting_list: + waiting_client_id, ready_event, _ = self.waiting_list.pop(0) + self.client_gpu_map[waiting_client_id] = gpu_id + print(f"Client {waiting_client_id[:8]} assigned GPU {gpu_id} from queue") + ready_event.set() + + await self._send_queue_updates() + + async def _send_queue_updates(self): + """Send updated queue positions to all waiting clients.""" + for i, (cid, _, ws) in enumerate(self.waiting_list): + if ws is not None: + try: + await ws.send_json({ + "type": "queue_status", + "position": i + 1, + "total_gpus": len(self.gpu_ids), + "available_gpus": 0, + }) + except Exception: + pass + + async def shutdown(self): + """Shutdown all GPU workers.""" + print("Shutting down GPU pool...") + tasks = [slot.shutdown() for slot in self.slots.values()] + await asyncio.gather(*tasks, return_exceptions=True) + print("GPU pool shutdown complete") + + def get_status(self) -> dict: + """Get the current status of the GPU pool.""" + warmed_count = sum(1 for slot in self.slots.values() if slot.warmup_success) + warmup_failures = sum(1 for slot in self.slots.values() + if slot.warmup_enabled and slot.warmup_error is not None) + return { + "total_gpus": len(self.gpu_ids), + "available_gpus": sum(1 for slot in self.slots.values() if slot.is_available), + "queue_size": len(self.waiting_list), + "warmup_enabled": STARTUP_WARMUP_ENABLED, + "warmup_successful_gpus": warmed_count, + "warmup_failed_gpus": warmup_failures, + "gpu_status": { + gpu_id: { + "ready": slot.ready, + "available": slot.is_available, + "client_count": slot.client_count, + "current_model_id": slot.current_model_id, + "process_alive": (slot.process.is_alive() if slot.process else False), + "warmup_enabled": slot.warmup_enabled, + "warmup_success": slot.warmup_success, + "warmup_error": slot.warmup_error, + "warmup_timings": slot.warmup_timings, + } + for gpu_id, slot in self.slots.items() + } + } + + +def get_available_gpus() -> list[int]: + """Get list of available GPU IDs from environment or auto-detect.""" + cuda_visible = os.environ.get("CUDA_VISIBLE_DEVICES", "") + if cuda_visible: + visible_gpu_ids = [int(x.strip()) for x in cuda_visible.split(",") if x.strip()] + return _limit_gpu_ids(visible_gpu_ids) + + # Auto-detect available GPUs + try: + result = subprocess.run(["nvidia-smi", "--query-gpu=index", "--format=csv,noheader"], + capture_output=True, + text=True) + if result.returncode == 0: + detected_gpu_ids = [int(x.strip()) for x in result.stdout.strip().split("\n") if x.strip()] + print(f"Auto-detected GPU IDs: {detected_gpu_ids}") + return _limit_gpu_ids(detected_gpu_ids) + except Exception: + pass + + return _limit_gpu_ids([0]) diff --git a/apps/dreamverse/dreamverse/main.py b/apps/dreamverse/dreamverse/main.py new file mode 100644 index 0000000000..9874d6aedd --- /dev/null +++ b/apps/dreamverse/dreamverse/main.py @@ -0,0 +1,138 @@ +# pyright: reportArgumentType=false, reportMissingImports=false +from __future__ import annotations + +import logging +import os +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI, WebSocket +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastvideo.entrypoints.streaming import build_health_router +from dreamverse.gpu_pool import GPUPool, get_available_gpus +from dreamverse.session_logger import SessionEventLogger + +from dreamverse.config import ( + DEVTOOLS_ENABLED, + FRONTEND_STATIC_DIR_CANDIDATES, + PROMPT_SAFETY_ENABLED, + SESSION_LOG_ROOT, +) +from dreamverse.prompt_enhancer import PromptEnhancer +from dreamverse.prompt_safety import PromptSafetyFilter + +import dreamverse.runtime as runtime +from dreamverse.routes.health import ( + router as internal_monitor_router, ) +from dreamverse.routes.presets import ( + prompt_config_router, + curated_presets_router, +) +from dreamverse.session.controller import SessionController +if DEVTOOLS_ENABLED: + pass + + +class _HeartbeatAccessLogFilter(logging.Filter): + """Drop noisy access logs for frequent health/readiness probes.""" + + def filter(self, record: logging.LogRecord) -> bool: + message = record.getMessage() + return ('"GET /healthz ' not in message and '"GET /readyz ' not in message) + + +def _install_heartbeat_log_filter() -> None: + access_logger = logging.getLogger("uvicorn.access") + for existing in access_logger.filters: + if isinstance(existing, _HeartbeatAccessLogFilter): + return + access_logger.addFilter(_HeartbeatAccessLogFilter()) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager.""" + print("Starting server...") + + # Get available GPUs + gpu_ids = get_available_gpus() + print(f"Selected GPU ids: {gpu_ids}") + + # Initialize GPU pool (spawns subprocess per GPU) + runtime.gpu_pool = GPUPool(gpu_ids) + await runtime.gpu_pool.initialize() + + runtime.prompt_enhancer = PromptEnhancer() + runtime.session_event_logger = SessionEventLogger(Path(SESSION_LOG_ROOT)) + runtime.prompt_safety_filter = (PromptSafetyFilter() if PROMPT_SAFETY_ENABLED else None) + if runtime.prompt_safety_filter is not None: + print("Prompt safety filter enabled") + + print("Server started") + yield + + print("Shutting down server...") + await runtime.gpu_pool.shutdown() + runtime.prompt_safety_filter = None + + +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(build_health_router(lambda: runtime.gpu_pool)) +app.include_router(internal_monitor_router) +app.include_router(prompt_config_router) +if DEVTOOLS_ENABLED: + app.include_router(curated_presets_router) + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + controller = SessionController( + ws=websocket, + gpu_pool=runtime.gpu_pool, + prompt_enhancer=runtime.prompt_enhancer, + prompt_safety_filter=runtime.prompt_safety_filter, + session_event_logger=runtime.session_event_logger, + ) + await controller.run() + + +# Serve server-side assets (images, etc.) +server_dir = os.path.dirname(os.path.abspath(__file__)) +app.mount("/server-assets", StaticFiles(directory=server_dir), name="server-assets") + +# Serve an exported frontend bundle when present. +for static_dir in FRONTEND_STATIC_DIR_CANDIDATES: + if os.path.isdir(static_dir): + app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") + break + + +def cli() -> None: + import argparse + import uvicorn + + from dreamverse._deps import require_dreamverse_runtime_deps + + require_dreamverse_runtime_deps() + + parser = argparse.ArgumentParser() + parser.add_argument("--host", default="0.0.0.0") + parser.add_argument("--port", type=int, default=8009) + args = parser.parse_args() + + _install_heartbeat_log_filter() + uvicorn.run(app, host=args.host, port=args.port) + + +if __name__ == "__main__": + cli() diff --git a/apps/dreamverse/dreamverse/mock_server.py b/apps/dreamverse/dreamverse/mock_server.py new file mode 100644 index 0000000000..6c58277c34 --- /dev/null +++ b/apps/dreamverse/dreamverse/mock_server.py @@ -0,0 +1,1164 @@ +# pyright: reportMissingTypeArgument=false, reportConstantRedefinition=false, reportGeneralTypeIssues=false +# ruff: noqa: UP022, F841, SIM105 +# mypy: ignore-errors +""" +Protocol-compatible mock server for the current LTX2 streaming UI. + +This mock speaks the same websocket API shape as `server/main.py` for: +- `session_init_v2` +- `append_prompt` +- `simple_generate` +- prompt window reset / rewrite control messages + +It streams a small fragmented MP4 clip generated by ffmpeg so the frontend can +exercise the real AV pipeline without needing a GPU backend. +""" + +from __future__ import annotations + +import asyncio +from contextlib import asynccontextmanager +import json +import logging +import os +import shutil +import subprocess +import uuid + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles + +from dreamverse._deps import require_dreamverse_runtime_deps +from dreamverse.config import FRONTEND_STATIC_DIR_CANDIDATES, GENERATION_SEGMENT_CAP +from dreamverse.session_init_image import cleanup_session_init_image, persist_session_init_image + +LATENCY_MS = 200 +SESSION_TIMEOUT_SECONDS = 300 +MOCK_FRAME_WIDTH = 640 +MOCK_FRAME_HEIGHT = 352 +MOCK_FPS = 24 +MOCK_DURATION_SECONDS = 5.0 +MOCK_STREAM_CHUNK_SIZE_BYTES = 256 * 1024 +MOCK_STREAM_CHUNK_DELAY_MS = 15 +MOCK_AV_MIME = 'video/mp4; codecs="avc1.42E01E"' +FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg")) +MOCK_SEGMENT_BYTES: bytes | None = None + + +class _HeartbeatAccessLogFilter(logging.Filter): + """Drop noisy access logs for frequent health/readiness probes.""" + + def filter(self, record: logging.LogRecord) -> bool: + message = record.getMessage() + return ('"GET /healthz ' not in message and '"GET /readyz ' not in message) + + +def _install_heartbeat_log_filter() -> None: + access_logger = logging.getLogger("uvicorn.access") + for existing in access_logger.filters: + if isinstance(existing, _HeartbeatAccessLogFilter): + return + access_logger.addFilter(_HeartbeatAccessLogFilter()) + + +def _build_mock_segment_bytes() -> bytes: + if not FFMPEG_BIN: + raise RuntimeError("ffmpeg is required to run the mock streaming server.") + + command = [ + FFMPEG_BIN, + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + f"testsrc2=size={MOCK_FRAME_WIDTH}x{MOCK_FRAME_HEIGHT}:rate={MOCK_FPS}", + "-t", + f"{MOCK_DURATION_SECONDS}", + "-an", + "-c:v", + "libx264", + "-preset", + "ultrafast", + "-tune", + "zerolatency", + "-pix_fmt", + "yuv420p", + "-profile:v", + "baseline", + "-level", + "3.0", + "-movflags", + "+empty_moov+default_base_moof+frag_keyframe", + "-frag_duration", + "250000", + "-f", + "mp4", + "pipe:1", + ] + result = subprocess.run( + command, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if not result.stdout: + raise RuntimeError("Mock ffmpeg command did not return MP4 bytes.") + return result.stdout + + +class PromptSubmission: + + def __init__(self, prompt_id: str, raw_prompt: str): + self.prompt_id = prompt_id + self.raw_prompt = raw_prompt + + +class ReadyPrompt: + + def __init__( + self, + *, + prompt: str, + source: str, + prompt_id: str | None = None, + seed_prompt_index: int | None = None, + loop_iteration: int | None = None, + ): + self.prompt = prompt + self.source = source + self.prompt_id = prompt_id + self.seed_prompt_index = seed_prompt_index + self.loop_iteration = loop_iteration + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global MOCK_SEGMENT_BYTES + del app + print(f"Mock server starting with {LATENCY_MS}ms simulated latency...", flush=True) + MOCK_SEGMENT_BYTES = _build_mock_segment_bytes() + print( + "Mock server ready " + f"({len(MOCK_SEGMENT_BYTES) / (1024 * 1024):.2f} MiB fMP4 clip)", + flush=True, + ) + yield + MOCK_SEGMENT_BYTES = None + print("Mock server shutting down...", flush=True) + + +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/healthz") +async def healthz(): + return { + "status": "ok", + "service": "ltx2-streaming-mock-server", + } + + +@app.get("/readyz") +async def readyz(): + return { + "status": "ready", + "service": "ltx2-streaming-mock-server", + } + + +@app.get("/status") +async def status(): + return { + "queue_size": 0, + "available_gpus": 1, + "total_gpus": 1, + } + + +async def _stream_mock_segment( + ws_send_json, + ws_send_bytes, + *, + segment_idx: int, +) -> None: + if not MOCK_SEGMENT_BYTES: + raise RuntimeError("Mock segment bytes were not initialized.") + + stream_id = f"mock-stream-{segment_idx}-{uuid.uuid4().hex[:8]}" + await ws_send_json({ + "type": "media_init", + "segment_idx": segment_idx, + "mime": MOCK_AV_MIME, + "stream_id": stream_id, + "mode": "av_fmp4", + }) + + chunk_count = 0 + for start in range(0, len(MOCK_SEGMENT_BYTES), MOCK_STREAM_CHUNK_SIZE_BYTES): + end = start + MOCK_STREAM_CHUNK_SIZE_BYTES + await ws_send_bytes(MOCK_SEGMENT_BYTES[start:end]) + chunk_count += 1 + if MOCK_STREAM_CHUNK_DELAY_MS > 0: + await asyncio.sleep(MOCK_STREAM_CHUNK_DELAY_MS / 1000.0) + + await ws_send_json({ + "type": "media_segment_complete", + "segment_idx": segment_idx, + "stream_id": stream_id, + "chunks": chunk_count, + }) + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + + client_id = str(uuid.uuid4()) + print(f"Client {client_id[:8]} connected") + + send_lock = asyncio.Lock() + stop_event = asyncio.Event() + session_init_image = None + + async def ws_send_json(payload: dict) -> None: + async with send_lock: + await websocket.send_json(payload) + + async def ws_send_bytes(payload: bytes) -> None: + async with send_lock: + await websocket.send_bytes(payload) + + async def session_timeout() -> None: + await asyncio.sleep(SESSION_TIMEOUT_SECONDS) + if stop_event.is_set(): + return + stop_event.set() + try: + await ws_send_json({ + "type": "session_timeout", + "message": f"Session expired after {SESSION_TIMEOUT_SECONDS} seconds", + }) + await websocket.close(code=1000, reason="Session timeout") + except Exception: + pass + + try: + await ws_send_json({ + "type": "queue_status", + "position": 0, + "total_gpus": 1, + "available_gpus": 1, + }) + + try: + init_data = await asyncio.wait_for(websocket.receive_json(), timeout=10.0) + except asyncio.TimeoutError: + init_data = {} + + init_type = init_data.get("type") + preset_id = init_data.get("preset_id") + preset_label = str(init_data.get("preset_label") or "").strip() + initial_rollout_prompt = str(init_data.get("initial_rollout_prompt") or "").strip() + enhancement_enabled = bool(init_data.get("enhancement_enabled", True)) + auto_extension_enabled = bool(init_data.get("auto_extension_enabled", False)) + loop_generation_enabled = bool(init_data.get("loop_generation_enabled", False)) + single_clip_mode = bool(init_data.get("single_clip_mode", False)) + generation_paused = False + + if init_type == "session_init_v2": + incoming_prompts = init_data.get("curated_prompts", []) + elif init_type == "select_model": + incoming_prompts = init_data.get("segment_prompts", []) + else: + incoming_prompts = [] + + curated_prompts = [prompt.strip() for prompt in incoming_prompts if isinstance(prompt, str) and prompt.strip()] + generation_paused = bool(initial_rollout_prompt and not single_clip_mode and len(curated_prompts) == 0) + + try: + session_init_image = persist_session_init_image(init_data.get("initial_image")) + except ValueError as exc: + await ws_send_json({ + "type": "error", + "message": str(exc), + }) + await websocket.close(code=1003, reason="Invalid initial image") + return + + timeout_task = asyncio.create_task(session_timeout()) + + await ws_send_json({ + "type": "gpu_assigned", + "gpu_id": 0, + "session_timeout": SESSION_TIMEOUT_SECONDS, + }) + + raw_prompt_queue: asyncio.Queue[PromptSubmission] = asyncio.Queue() + ready_prompt_queue: asyncio.Queue[ReadyPrompt] = asyncio.Queue() + seed_prompt_memory = list(curated_prompts) + curated_idx = 0 + segment_idx = 0 + loop_iteration = 0 if generation_paused else 1 + prompt_sources_blocked = False + pending_seed_reset = False + pending_seed_reset_reason = "" + pending_simple_submission: PromptSubmission | None = None + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + initial_rollout_waiting_for_rewrite = generation_paused + segment_generation_active = False + rewrite_restart_pending = False + project_active = True + project_stream_started = False + pending_project_end = False + + def replace_session_image(initial_image_payload: object) -> None: + nonlocal session_init_image + next_session_image = persist_session_init_image(initial_image_payload) + previous_session_image = session_init_image + session_init_image = next_session_image + if previous_session_image is not None: + cleanup_session_init_image(previous_session_image) + + async def send_stream_start(seed_reason: str) -> None: + await ws_send_json({ + "type": "ltx2_stream_start", + "total_segments": len(curated_prompts), + "preset_id": preset_id, + "stream_mode": "av_fmp4", + "live_mode": True, + "loop_generation_enabled": loop_generation_enabled, + "loop_iteration": loop_iteration, + "generation_segment_cap": 0, + }) + if seed_reason == "init": + await ws_send_json({ + "type": "seed_prompts_updated", + "prompts": seed_prompt_memory, + "preset_id": preset_id, + "preset_label": preset_label, + "reason": "init", + "seed_prompt_count": len(seed_prompt_memory), + }) + await ws_send_json({ + "type": "loop_generation_updated", + "enabled": loop_generation_enabled, + }) + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + else: + await ws_send_json({ + "type": "seed_prompts_reset_applied", + "reason": seed_reason or "manual_reset", + "loop_iteration": loop_iteration, + "seed_prompt_count": len(seed_prompt_memory), + "dropped_user_raw_queue": 0, + "dropped_user_ready_queue": 0, + }) + + async def promote_submission_to_ready(submission: PromptSubmission, ) -> None: + nonlocal prompt_sources_blocked + + await ws_send_json({ + "type": "prompt_received", + "prompt_id": submission.prompt_id, + "queue_depth": raw_prompt_queue.qsize() + 1, + }) + + if enhancement_enabled: + await ws_send_json({ + "type": "prompt_enhancing", + "prompt_id": submission.prompt_id, + }) + await asyncio.sleep(min(LATENCY_MS, 120) / 1000.0) + final_prompt = submission.raw_prompt + await ready_prompt_queue.put( + ReadyPrompt( + prompt=final_prompt, + source="user_enhanced", + prompt_id=submission.prompt_id, + loop_iteration=loop_iteration, + )) + await ws_send_json({ + "type": "prompt_ready", + "prompt_id": submission.prompt_id, + "prompt": final_prompt, + "source": "user_enhanced", + "latency_ms": float(min(LATENCY_MS, 120)), + }) + else: + final_prompt = submission.raw_prompt + await ready_prompt_queue.put( + ReadyPrompt( + prompt=final_prompt, + source="user_raw", + prompt_id=submission.prompt_id, + loop_iteration=loop_iteration, + )) + await ws_send_json({ + "type": "prompt_ready", + "prompt_id": submission.prompt_id, + "prompt": final_prompt, + "source": "user_raw", + "latency_ms": 0.0, + }) + + if prompt_sources_blocked: + prompt_sources_blocked = False + await ws_send_json({ + "type": "prompt_sources_resumed", + "segment_idx": segment_idx + 1, + "source": "user_prompt", + "prompt_id": submission.prompt_id, + }) + + async def apply_project_init_payload(payload: dict[str, object], ) -> bool: + nonlocal preset_id + nonlocal preset_label + nonlocal initial_rollout_prompt + nonlocal enhancement_enabled + nonlocal auto_extension_enabled + nonlocal loop_generation_enabled + nonlocal single_clip_mode + nonlocal generation_paused + nonlocal seed_prompt_memory + nonlocal curated_prompts + nonlocal curated_idx + nonlocal segment_idx + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal pending_simple_submission + nonlocal single_clip_waiting_for_request + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal loop_iteration + nonlocal project_active + nonlocal project_stream_started + nonlocal pending_project_end + + next_initial_rollout_prompt = str(payload.get("initial_rollout_prompt") or "").strip() + next_preset_id = str(payload.get("preset_id") or "").strip() + next_preset_label = str(payload.get("preset_label") or "").strip() + next_curated_prompts = [ + prompt.strip() for prompt in payload.get("curated_prompts", []) + if isinstance(prompt, str) and prompt.strip() + ] + + try: + replace_session_image(payload.get("initial_image")) + except ValueError as exc: + await ws_send_json({ + "type": "error", + "message": str(exc), + }) + return False + + if next_preset_id: + preset_id = next_preset_id + if next_preset_label: + preset_label = next_preset_label + + initial_rollout_prompt = next_initial_rollout_prompt + enhancement_enabled = bool(payload.get("enhancement_enabled", True)) + auto_extension_enabled = bool(payload.get("auto_extension_enabled", False)) + loop_generation_enabled = bool(payload.get("loop_generation_enabled", False)) + single_clip_mode = bool(payload.get("single_clip_mode", False)) + + seed_prompt_memory = list(next_curated_prompts) + curated_prompts = list(seed_prompt_memory) + curated_idx = 0 + segment_idx = 0 + pending_seed_reset = False + pending_seed_reset_reason = "" + pending_simple_submission = None + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + generation_paused = bool(initial_rollout_prompt and not single_clip_mode and len(curated_prompts) == 0) + initial_rollout_waiting_for_rewrite = generation_paused + rewrite_restart_pending = False + loop_iteration = 0 + project_active = True + project_stream_started = False + pending_project_end = False + + return True + + def drain_queue_nowait(queue: asyncio.Queue) -> int: + drained = 0 + while True: + try: + queue.get_nowait() + drained += 1 + except asyncio.QueueEmpty: + break + return drained + + async def enter_project_idle() -> None: + nonlocal seed_prompt_memory + nonlocal curated_prompts + nonlocal curated_idx + nonlocal segment_idx + nonlocal generation_paused + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal pending_simple_submission + nonlocal single_clip_waiting_for_request + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal initial_rollout_prompt + nonlocal project_active + nonlocal project_stream_started + nonlocal pending_project_end + + dropped_raw = drain_queue_nowait(raw_prompt_queue) + dropped_ready = drain_queue_nowait(ready_prompt_queue) + seed_prompt_memory = [] + curated_prompts = [] + curated_idx = 0 + segment_idx = 0 + generation_paused = False + pending_seed_reset = False + pending_seed_reset_reason = "" + pending_simple_submission = None + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + initial_rollout_waiting_for_rewrite = False + rewrite_restart_pending = False + initial_rollout_prompt = "" + project_active = False + pending_project_end = False + + if project_stream_started: + project_stream_started = False + await ws_send_json({"type": "ltx2_stream_complete"}) + + await ws_send_json({ + "type": "project_idle", + "dropped_user_raw_queue": dropped_raw, + "dropped_user_ready_queue": dropped_ready, + }) + + async def reader_loop() -> None: + nonlocal preset_id + nonlocal preset_label + nonlocal enhancement_enabled + nonlocal auto_extension_enabled + nonlocal loop_generation_enabled + nonlocal single_clip_mode + nonlocal generation_paused + nonlocal seed_prompt_memory + nonlocal curated_prompts + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal pending_simple_submission + nonlocal prompt_sources_blocked + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal project_active + nonlocal pending_project_end + while not stop_event.is_set(): + try: + data = await websocket.receive_json() + except WebSocketDisconnect: + stop_event.set() + return + except Exception: + stop_event.set() + return + + msg_type = data.get("type") + if msg_type == "leave": + stop_event.set() + return + + if msg_type == "end_project_keep_session": + loop_generation_enabled = False + generation_paused = False + rollout_waiting_for_rewrite = False + initial_rollout_waiting_for_rewrite = False + rewrite_restart_pending = False + pending_project_end = True + continue + + if msg_type == "project_init_v1": + if project_active or pending_project_end: + await ws_send_json({ + "type": + "error", + "message": ("Current project is still ending. " + "Wait for the reset to finish before starting another project."), + }) + continue + init_applied = await apply_project_init_payload(data) + if not init_applied: + continue + if initial_rollout_waiting_for_rewrite: + await ws_send_json({ + "type": "rewrite_seed_prompts_started", + "model": "mock-rewrite-model", + }) + seed_prompt_memory = [f"{initial_rollout_prompt} [segment {index}]" for index in range(1, 7)] + curated_prompts = list(seed_prompt_memory) + await ws_send_json({ + "type": + "seed_prompts_updated", + "prompts": + seed_prompt_memory, + "preset_id": + preset_id, + "preset_label": + preset_label, + "reason": + "rewrite", + "fallback_used": + False, + "error": + None, + "model": + "mock-rewrite-model", + "latency_ms": + float(min(LATENCY_MS, 150)), + "raw_llm_output": + json.dumps({ + "id": preset_id or "current_rollout", + "label": preset_label or "Current rollout", + "segment_prompts": seed_prompt_memory, + }), + }) + await ws_send_json({ + "type": "rewrite_seed_prompts_complete", + "fallback_used": False, + "error": None, + "preset_id": preset_id, + "preset_label": preset_label, + "model": "mock-rewrite-model", + "latency_ms": float(min(LATENCY_MS, 150)), + }) + pending_seed_reset = True + pending_seed_reset_reason = "initial_rewrite" + generation_paused = False + initial_rollout_waiting_for_rewrite = False + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + else: + pending_seed_reset = True + pending_seed_reset_reason = "project_init" + continue + + if not project_active: + if msg_type in { + "append_prompt", + "rewrite_seed_prompts", + "reset_to_seed_prompts", + "restart_generation", + }: + await ws_send_json({ + "type": "error", + "message": "Start a new project before sending prompts.", + }) + continue + + if msg_type == "append_prompt": + if ((rollout_waiting_for_rewrite or initial_rollout_waiting_for_rewrite) and not single_clip_mode): + await ws_send_json({ + "type": "error", + "message": ("Use Rewrite to create or restart the rollout."), + }) + continue + raw_prompt = str(data.get("prompt", "")).strip() + if not raw_prompt: + await ws_send_json({ + "type": "error", + "message": "append_prompt requires a non-empty prompt", + }) + continue + await promote_submission_to_ready( + PromptSubmission( + prompt_id=str(data.get("prompt_id") or uuid.uuid4()), + raw_prompt=raw_prompt, + )) + continue + + if msg_type == "simple_generate": + raw_prompt = str(data.get("prompt", "")).strip() + if not raw_prompt: + await ws_send_json({ + "type": "error", + "message": "simple_generate requires a non-empty prompt", + }) + continue + + try: + replace_session_image(data.get("initial_image")) + except ValueError as exc: + await ws_send_json({ + "type": "error", + "message": str(exc), + }) + continue + + next_preset_id = str(data.get("preset_id") or "").strip() + if next_preset_id: + preset_id = next_preset_id + next_preset_label = str(data.get("preset_label") or "").strip() + if next_preset_label: + preset_label = next_preset_label + + enhancement_enabled = bool(data.get("enhancement_enabled", True)) + auto_extension_enabled = False + loop_generation_enabled = False + single_clip_mode = True + generation_paused = False + pending_seed_reset = True + pending_seed_reset_reason = "simple_generate" + seed_prompt_memory = ([] if enhancement_enabled else [raw_prompt]) + curated_prompts = list(seed_prompt_memory) + pending_simple_submission = (PromptSubmission( + prompt_id=str(data.get("prompt_id") or uuid.uuid4()), + raw_prompt=raw_prompt, + ) if enhancement_enabled else None) + continue + + if msg_type == "rewrite_seed_prompts": + rewrite_instruction = str(data.get("rewrite_instruction", "")).strip() + if len(seed_prompt_memory) == 0 and not (initial_rollout_waiting_for_rewrite + and rewrite_instruction): + await ws_send_json({ + "type": "error", + "message": "No seed prompts available to rewrite.", + }) + continue + if (segment_generation_active and not single_clip_mode and not rollout_waiting_for_rewrite + and not initial_rollout_waiting_for_rewrite): + rewrite_restart_pending = True + await ws_send_json({ + "type": "rewrite_seed_prompts_started", + "model": "mock-rewrite-model", + }) + rollout_id = preset_id or "current_rollout" + rollout_label = preset_label or "Current rollout" + if seed_prompt_memory: + rewritten_prompts = [ + f"{prompt} [{rewrite_instruction or 'mock rewrite'}]" for prompt in seed_prompt_memory + ] + else: + base_prompt = rewrite_instruction or "mock rewrite" + rewritten_prompts = [f"{base_prompt} [segment {index}]" for index in range(1, 7)] + seed_prompt_memory = rewritten_prompts + curated_prompts = list(seed_prompt_memory) + preset_id = rollout_id + preset_label = rollout_label + if initial_rollout_waiting_for_rewrite: + pending_seed_reset = True + pending_seed_reset_reason = "initial_rewrite" + generation_paused = False + initial_rollout_waiting_for_rewrite = False + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + if rollout_waiting_for_rewrite: + pending_seed_reset = True + pending_seed_reset_reason = "rewrite_rollout" + generation_paused = False + elif rewrite_restart_pending: + pending_seed_reset = True + pending_seed_reset_reason = "rewrite_during_generation" + generation_paused = False + rewrite_restart_pending = False + await ws_send_json({ + "type": + "seed_prompts_updated", + "prompts": + seed_prompt_memory, + "preset_id": + preset_id, + "preset_label": + preset_label, + "reason": + "rewrite", + "fallback_used": + False, + "error": + None, + "model": + "mock-rewrite-model", + "latency_ms": + float(min(LATENCY_MS, 150)), + "raw_llm_output": + json.dumps({ + "id": preset_id, + "label": preset_label, + "segment_prompts": seed_prompt_memory, + }), + }) + await ws_send_json({ + "type": "rewrite_seed_prompts_complete", + "fallback_used": False, + "error": None, + "preset_id": preset_id, + "preset_label": preset_label, + "model": "mock-rewrite-model", + "latency_ms": float(min(LATENCY_MS, 150)), + }) + continue + + if msg_type == "reset_to_seed_prompts": + pending_seed_reset = True + pending_seed_reset_reason = "manual_reset" + continue + + if msg_type == "restart_generation": + pending_seed_reset = True + pending_seed_reset_reason = "cap_restart" + rollout_waiting_for_rewrite = False + await ws_send_json({ + "type": "generation_restarted", + "reason": "cap_restart", + "segment_cap": 0, + }) + continue + + if msg_type == "set_enhancement": + enhancement_enabled = bool(data.get("enabled", enhancement_enabled)) + await ws_send_json({ + "type": "enhancement_updated", + "enabled": enhancement_enabled, + }) + continue + + if msg_type == "set_auto_extension": + auto_extension_enabled = bool(data.get("enabled", auto_extension_enabled)) + await ws_send_json({ + "type": "auto_extension_updated", + "enabled": auto_extension_enabled, + }) + continue + + if msg_type == "set_loop_generation": + loop_generation_enabled = bool(data.get("enabled", loop_generation_enabled)) + await ws_send_json({ + "type": "loop_generation_updated", + "enabled": loop_generation_enabled, + }) + continue + + if msg_type == "set_generation_paused": + generation_paused = bool(data.get("paused", generation_paused)) + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + continue + + async def generation_loop() -> None: + nonlocal curated_idx + nonlocal curated_prompts + nonlocal segment_idx + nonlocal loop_iteration + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal pending_simple_submission + nonlocal prompt_sources_blocked + nonlocal single_clip_waiting_for_request + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal segment_generation_active + nonlocal rewrite_restart_pending + nonlocal project_active + nonlocal project_stream_started + nonlocal pending_project_end + while not stop_event.is_set(): + if pending_project_end: + await enter_project_idle() + await asyncio.sleep(0) + continue + + if not project_active: + await asyncio.sleep(0.05) + continue + + if pending_seed_reset: + curated_prompts = list(seed_prompt_memory) + curated_idx = 0 + segment_idx = 0 + pending_seed_reset = False + loop_iteration += 1 + prompt_sources_blocked = False + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + project_stream_started = True + await send_stream_start(pending_seed_reset_reason) + pending_seed_reset_reason = "" + if pending_simple_submission is not None: + submission = pending_simple_submission + pending_simple_submission = None + await promote_submission_to_ready(submission) + + if generation_paused: + await asyncio.sleep(0.05) + continue + + if rewrite_restart_pending: + await asyncio.sleep(0.05) + continue + + if (not single_clip_mode and not rollout_waiting_for_rewrite and GENERATION_SEGMENT_CAP > 0 + and segment_idx >= GENERATION_SEGMENT_CAP): + rollout_waiting_for_rewrite = True + loop_generation_enabled = False + project_stream_started = False + await ws_send_json({ + "type": "ltx2_stream_complete", + }) + await asyncio.sleep(0.05) + continue + + if rollout_waiting_for_rewrite: + await asyncio.sleep(0.05) + continue + + if initial_rollout_waiting_for_rewrite: + await asyncio.sleep(0.05) + continue + + selected = None + try: + selected = ready_prompt_queue.get_nowait() + except asyncio.QueueEmpty: + if curated_idx < len(curated_prompts): + prompt_idx = curated_idx + selected = ReadyPrompt( + prompt=curated_prompts[prompt_idx], + source="curated", + seed_prompt_index=prompt_idx, + loop_iteration=loop_iteration, + ) + curated_idx += 1 + + if selected is None: + if single_clip_mode: + await asyncio.sleep(0.05) + continue + if not prompt_sources_blocked: + prompt_sources_blocked = True + await ws_send_json({ + "type": "prompt_sources_blocked", + "segment_idx": segment_idx + 1, + }) + await asyncio.sleep(0.05) + continue + + if prompt_sources_blocked: + prompt_sources_blocked = False + await ws_send_json({ + "type": "prompt_sources_resumed", + "segment_idx": segment_idx + 1, + "source": selected.source, + "prompt_id": selected.prompt_id, + }) + + segment_idx += 1 + single_clip_waiting_for_request = False + total_segments_hint = max(segment_idx, len(curated_prompts)) + await ws_send_json({ + "type": "segment_prompt_source", + "segment_idx": segment_idx, + "source": selected.source, + "prompt_id": selected.prompt_id, + "fallback_used": False, + "seed_prompt_index": selected.seed_prompt_index, + "loop_iteration": selected.loop_iteration or loop_iteration, + }) + await ws_send_json({ + "type": "ltx2_segment_start", + "segment_idx": segment_idx, + "total_segments": total_segments_hint, + "prompt": selected.prompt, + "source": selected.source, + "seed_prompt_index": selected.seed_prompt_index, + "loop_iteration": selected.loop_iteration or loop_iteration, + }) + + segment_generation_active = True + await asyncio.sleep(LATENCY_MS / 1000.0) + await _stream_mock_segment( + ws_send_json, + ws_send_bytes, + segment_idx=segment_idx, + ) + await ws_send_json({ + "type": "step_complete", + "latency_ms": { + "total": round(LATENCY_MS + 120.0, 2), + "worker_e2e": round(float(LATENCY_MS), 2), + "main_user_step": round(LATENCY_MS + 120.0, 2), + "overhead": 120.0, + }, + }) + await ws_send_json({ + "type": "ltx2_segment_complete", + "segment_idx": segment_idx, + "total_segments": total_segments_hint, + }) + segment_generation_active = False + if single_clip_mode and not single_clip_waiting_for_request: + single_clip_waiting_for_request = True + project_stream_started = False + await ws_send_json({ + "type": "ltx2_stream_complete", + }) + + reader_task = asyncio.create_task(reader_loop()) + generation_task = asyncio.create_task(generation_loop()) + await asyncio.sleep(0) + + await ws_send_json({ + "type": "loop_generation_updated", + "enabled": loop_generation_enabled, + }) + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + if pending_project_end: + await enter_project_idle() + elif initial_rollout_waiting_for_rewrite: + await ws_send_json({ + "type": "rewrite_seed_prompts_started", + "model": "mock-rewrite-model", + }) + seed_prompt_memory = [f"{initial_rollout_prompt} [segment {index}]" for index in range(1, 7)] + curated_prompts = list(seed_prompt_memory) + await ws_send_json({ + "type": + "seed_prompts_updated", + "prompts": + seed_prompt_memory, + "preset_id": + preset_id, + "preset_label": + preset_label, + "reason": + "rewrite", + "fallback_used": + False, + "error": + None, + "model": + "mock-rewrite-model", + "latency_ms": + float(min(LATENCY_MS, 150)), + "raw_llm_output": + json.dumps({ + "id": preset_id or "current_rollout", + "label": preset_label or "Current rollout", + "segment_prompts": seed_prompt_memory, + }), + }) + await ws_send_json({ + "type": "rewrite_seed_prompts_complete", + "fallback_used": False, + "error": None, + "preset_id": preset_id, + "preset_label": preset_label, + "model": "mock-rewrite-model", + "latency_ms": float(min(LATENCY_MS, 150)), + }) + pending_seed_reset = True + pending_seed_reset_reason = "initial_rewrite" + generation_paused = False + initial_rollout_waiting_for_rewrite = False + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + else: + project_stream_started = True + await send_stream_start("init") + + done, pending = await asyncio.wait( + {reader_task, generation_task, timeout_task}, + return_when=asyncio.FIRST_COMPLETED, + ) + for task in pending: + task.cancel() + for task in done: + try: + await task + except asyncio.CancelledError: + pass + except WebSocketDisconnect: + print(f"Client {client_id[:8]} disconnected") + except Exception as exc: + print(f"Client {client_id[:8]} error: {exc}") + try: + await websocket.send_json({ + "type": "error", + "message": f"Mock streaming failed: {exc}", + }) + except Exception: + pass + import traceback + traceback.print_exc() + finally: + stop_event.set() + cleanup_session_init_image(session_init_image) + + +for static_dir in FRONTEND_STATIC_DIR_CANDIDATES: + if os.path.isdir(static_dir): + app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") + break + + +def cli() -> None: + global LATENCY_MS + + import argparse + import uvicorn + + require_dreamverse_runtime_deps() + + parser = argparse.ArgumentParser(description="Protocol-compatible mock server for the LTX2 UI") + parser.add_argument( + "--latency", + type=int, + default=200, + help="Simulated model latency in milliseconds", + ) + parser.add_argument( + "--port", + type=int, + default=8009, + help="Server port", + ) + args = parser.parse_args() + + LATENCY_MS = args.latency + print(f"Starting mock server with {LATENCY_MS}ms latency on port {args.port}") + + _install_heartbeat_log_filter() + uvicorn.run(app, host="0.0.0.0", port=args.port) + + +if __name__ == "__main__": + cli() diff --git a/apps/dreamverse/dreamverse/prompt_enhancer.py b/apps/dreamverse/dreamverse/prompt_enhancer.py new file mode 100644 index 0000000000..ab87c29c9c --- /dev/null +++ b/apps/dreamverse/dreamverse/prompt_enhancer.py @@ -0,0 +1,1771 @@ +from __future__ import annotations +# pyright: reportArgumentType=false +# ruff: noqa: B023,UP038 +# mypy: ignore-errors + +import asyncio +import json +import queue +import re +import shutil +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +try: + from cerebras.cloud.sdk import Cerebras +except ImportError: # pragma: no cover - optional dependency + Cerebras = None # type: ignore[assignment] + +try: + from openai import OpenAI +except ImportError: # pragma: no cover - optional dependency + OpenAI = None # type: ignore[assignment] + +from dreamverse.config import ( + PROMPT_AUTO_SYSTEM_PROMPT_FALLBACK_PATH, + PROMPT_AUTO_SYSTEM_PROMPT_PATH, + PROMPT_ENHANCE_SYSTEM_PROMPT_PATH, + PROMPT_ENHANCE_SYSTEM_PROMPT_FALLBACK_PATH, + PROMPT_API_KEY, + PROMPT_API_KEYS, + PROMPT_PROVIDER_API_KEY_NAMES, + PROMPT_API_BASE_URL, + PROMPT_API_BASE_URLS, + PROMPT_HTTP_TIMEOUT_MS, + PROMPT_INITIAL_STAGE_TIMEOUT_MS, + PROMPT_MAX_COMPLETION_TOKENS, + PROMPT_MODEL, + PROMPT_PROVIDER, + PROMPT_PROVIDER_MODELS, + PROMPT_PROVIDER_PRIORITY, + PROMPT_PROVIDER_RUNTIME_STAGES, + PROMPT_REWRITE_MODEL, + PROMPT_REWRITE_MODEL_OPTIONS, + PROMPT_REWRITE_ALL_SYSTEM_PROMPT_FALLBACK_PATH, + PROMPT_REWRITE_ALL_SYSTEM_PROMPT_PATH, + PROMPT_REWRITE_USER_SYSTEM_PROMPT_FALLBACK_PATH, + PROMPT_REWRITE_USER_SYSTEM_PROMPT_PATH, + PROMPT_TEMPERATURE, + PROMPT_TIMEOUT_MS, +) +from dreamverse.rewrite_prompt_payload import ( + build_rewrite_request_body, + DEFAULT_REWRITE_ROLLOUT_ID, + DEFAULT_REWRITE_ROLLOUT_LABEL, + DEFAULT_REWRITE_SEGMENT_COUNT, + normalize_prompt_window_prompts, +) + + +def _enhance_print(level: str, message: str): + print(f"[ENHANCE][{level}] {message}", flush=True) + + +@dataclass +class EnhanceResult: + prompt: str + fallback_used: bool + error: str | None + provider: str + model: str + latency_ms: float + + +@dataclass +class RewriteResult: + prompts: list[str] + fallback_used: bool + error: str | None + provider: str + model: str + latency_ms: float + rollout_id: str + rollout_label: str + raw_response_text: str | None = None + + +@dataclass(frozen=True) +class ProviderRuntime: + name: str + api_key: str + api_base_url: str | None + request_model: str + client: Any + + +def _resolve_provider_family(provider_name: str) -> str: + if provider_name == "cerebras": + return "cerebras" + if provider_name == "groq": + return "groq" + raise RuntimeError(f"Unsupported prompt provider: {provider_name}") + + +def _resolve_provider_label(provider_name: str) -> str: + return _resolve_provider_family(provider_name) + + +def _preview_text(text: str, *, limit: int = 160) -> str: + normalized = text.replace("\n", "\\n") + if len(normalized) <= limit: + return normalized + return normalized[:limit] + "..." + + +def _expand_prompt_candidate_paths(path: str) -> list[Path]: + prompt_path = Path(path) + candidate_paths = [prompt_path] + if prompt_path.suffix == ".txt": + candidate_paths.append(prompt_path.with_suffix(".md")) + elif prompt_path.suffix == ".md": + candidate_paths.append(prompt_path.with_suffix(".txt")) + return candidate_paths + + +def _load_prompt_required( + path: str, + prompt_name: str, + fallback_path: str | None = None, +) -> str: + text, _ = _load_prompt_required_with_path( + path, + prompt_name, + fallback_path, + ) + return text + + +def _load_prompt_required_with_path( + path: str, + prompt_name: str, + fallback_path: str | None = None, +) -> tuple[str, str]: + candidate_paths: list[Path] = [] + for current_path in [path, fallback_path]: + if not current_path: + continue + for candidate in _expand_prompt_candidate_paths(current_path): + if candidate not in candidate_paths: + candidate_paths.append(candidate) + + for candidate in candidate_paths: + if not candidate.is_file(): + continue + try: + text = _normalize_prompt_file_text(candidate.read_text(encoding="utf-8")) + except OSError as exc: + raise RuntimeError(f"Failed to read {prompt_name} system prompt: {candidate}") from exc + if not text: + raise RuntimeError(f"{prompt_name} system prompt file is empty: {candidate}") + return text, str(candidate) + + tried = ", ".join(str(candidate) for candidate in candidate_paths) + raise RuntimeError(f"{prompt_name} system prompt file not found. Tried: {tried}") + + +def _load_prompt_with_prompt_fallback( + path: str, + prompt_name: str, + fallback_prompt_text: str, + fallback_prompt_source_path: str, + fallback_path: str | None = None, +) -> tuple[str, str]: + candidate_paths: list[Path] = [] + for current_path in [path, fallback_path]: + if not current_path: + continue + for candidate in _expand_prompt_candidate_paths(current_path): + if candidate not in candidate_paths: + candidate_paths.append(candidate) + + for candidate in candidate_paths: + if not candidate.is_file(): + continue + try: + text = _normalize_prompt_file_text(candidate.read_text(encoding="utf-8")) + except OSError as exc: + raise RuntimeError(f"Failed to read {prompt_name} system prompt: {candidate}") from exc + if text: + return text, str(candidate) + + return fallback_prompt_text, fallback_prompt_source_path + + +def _normalize_prompt_file_text(text: str) -> str: + normalized = text.strip() + if not normalized: + return "" + + wrapper_match = re.match( + r"^SYSTEM_PROMPT\s*=\s*(?P<quote>'''|\"\"\")(?P<body>[\s\S]*?)(?P=quote)\s*$", + normalized, + ) + if wrapper_match: + return wrapper_match.group("body").strip() + + return normalized + + +def _save_prompt(path: str, prompt_text: str, prompt_name: str) -> None: + prompt_path = Path(path) + prompt_path.parent.mkdir(parents=True, exist_ok=True) + normalized_prompt_text = prompt_text.strip() + "\n" + try: + if prompt_path.is_file(): + current_text = prompt_path.read_text(encoding="utf-8") + if current_text != normalized_prompt_text: + backup_path = _build_prompt_backup_path(prompt_path) + shutil.copy2(prompt_path, backup_path) + prompt_path.write_text(normalized_prompt_text, encoding="utf-8") + except OSError as exc: + raise RuntimeError(f"Failed to save {prompt_name} system prompt: {prompt_path}") from exc + + +def _build_prompt_backup_path(prompt_path: Path) -> Path: + timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + suffix = "".join(prompt_path.suffixes) + stem = (prompt_path.name[:-len(suffix)] if suffix else prompt_path.name) + return prompt_path.with_name(f"{stem}.{timestamp}.bak{suffix}") + + +def _resolve_prompt_save_path( + path: str, + fallback_path: str | None = None, +) -> str: + candidate = Path(path) + if candidate.is_file() or fallback_path is None: + return str(candidate) + + for fallback_candidate in _expand_prompt_candidate_paths(fallback_path): + if fallback_candidate.is_file(): + return str(fallback_candidate) + return str(candidate) + + +def _extract_assistant_content(response_json: dict[str, Any]) -> str: + choices = response_json.get("choices") + if not isinstance(choices, list) or not choices: + raise ValueError("Missing choices in chat completion response.") + message = choices[0].get("message", {}) + content = message.get("content") + if isinstance(content, str): + return content + if isinstance(content, list): + chunks: list[str] = [] + for item in content: + if isinstance(item, dict): + # OpenAI-compatible providers may use different content item + # shapes; keep this extractor permissive. + text = item.get("text") + if isinstance(text, str): + chunks.append(text) + continue + if isinstance(text, dict): + value = text.get("value") + if isinstance(value, str): + chunks.append(value) + continue + alt_text = item.get("output_text") + if isinstance(alt_text, str): + chunks.append(alt_text) + if chunks: + return "".join(chunks) + finish_reason = choices[0].get("finish_reason") + refusal = message.get("refusal") + raise ValueError("Missing assistant content in chat completion response. " + f"finish_reason={finish_reason!r}, refusal={refusal!r}") + + +def _dump_response_json(response: Any) -> dict[str, Any]: + if hasattr(response, "model_dump"): + payload = response.model_dump(mode="json") + elif isinstance(response, dict): + payload = response + elif hasattr(response, "dict"): + payload = response.dict() + else: + raise TypeError("Unsupported chat completion response type. " + f"type={type(response)!r}") + + if not isinstance(payload, dict): + raise TypeError("Chat completion response did not serialize to a JSON object.") + return payload + + +def _extract_content_or_empty(response_json: dict[str, Any]) -> str: + try: + return _extract_assistant_content(response_json) + except Exception as exc: + choices = response_json.get("choices") + choice0 = choices[0] if isinstance(choices, list) and choices else {} + message = (choice0.get("message", {}) if isinstance(choice0, dict) else {}) + usage = response_json.get("usage") + _enhance_print( + "WARN", + "Failed to extract assistant content: " + f"{exc}; finish_reason={choice0.get('finish_reason')!r}; " + f"usage={usage!r}; message_keys=" + f"{list(message.keys()) if isinstance(message, dict) else 'n/a'}", + ) + return "" + + +def _parse_json_response(content: str) -> dict[str, Any]: + text = content.strip() + if not text: + raise ValueError("Assistant response is empty.") + + try: + parsed = json.loads(text) + if isinstance(parsed, dict): + return parsed + except json.JSONDecodeError: + pass + + # Handle fenced blocks like ```json ... ``` with optional prose around it. + fence_pattern = re.compile( + r"```(?:json)?\s*([\s\S]*?)```", + flags=re.IGNORECASE, + ) + for match in fence_pattern.finditer(text): + block = match.group(1).strip() + if not block: + continue + try: + parsed = json.loads(block) + if isinstance(parsed, dict): + return parsed + except json.JSONDecodeError: + continue + + # Fall back to scanning for the first decodable JSON object in free-form text. + decoder = json.JSONDecoder() + for idx, char in enumerate(text): + if char != "{": + continue + try: + parsed, _ = decoder.raw_decode(text[idx:]) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + return parsed + + raise ValueError("No JSON object found in assistant response.") + + +def _normalize_prompt(value: Any) -> str: + if not isinstance(value, str): + return "" + return value.strip() + + +def _resolve_rollout_id(value: Any) -> str: + normalized = _normalize_prompt(value) + return normalized or DEFAULT_REWRITE_ROLLOUT_ID + + +def _resolve_rollout_label(value: Any) -> str: + normalized = _normalize_prompt(value) + return normalized or DEFAULT_REWRITE_ROLLOUT_LABEL + + +def _format_locked_segments(locked_segments: list[str]) -> str: + if not locked_segments: + return "(none)" + return "\n".join(f'segment_{i + 1} ({i * 5}-{(i + 1) * 5}s): "{segment}"' + for i, segment in enumerate(locked_segments)) + + +class PromptEnhancer: + + def __init__(self): + self.provider = PROMPT_PROVIDER + self.provider_label = _resolve_provider_label(PROMPT_PROVIDER) + self.api_key = PROMPT_API_KEY + self.api_base_url = PROMPT_API_BASE_URL + self.model = PROMPT_MODEL + self.provider_request_models = dict(PROMPT_PROVIDER_MODELS) + self.rewrite_default_model = PROMPT_REWRITE_MODEL or self.model + self.rewrite_model_options = self._normalize_model_options(PROMPT_REWRITE_MODEL_OPTIONS) + if self.rewrite_default_model not in self.rewrite_model_options: + self.rewrite_model_options = [ + self.rewrite_default_model, + *self.rewrite_model_options, + ] + self.provider_runtimes = self._build_provider_runtimes() + self.provider_runtime_stages = self._build_provider_runtime_stages() + self.provider_success_counts = {runtime.name: 0 for runtime in self.provider_runtimes} + self.client: Any | None = self.provider_runtimes[0].client + self.default_timeout_ms = PROMPT_TIMEOUT_MS + self.http_timeout_ms = PROMPT_HTTP_TIMEOUT_MS + self.initial_stage_timeout_ms = PROMPT_INITIAL_STAGE_TIMEOUT_MS + self.temperature = PROMPT_TEMPERATURE + self.rewrite_default_temperature = self.resolve_rewrite_temperature(PROMPT_TEMPERATURE) + self.max_completion_tokens = PROMPT_MAX_COMPLETION_TOKENS + self.enhance_system_prompt_path = PROMPT_ENHANCE_SYSTEM_PROMPT_PATH + self.enhance_system_prompt_fallback_path = (PROMPT_ENHANCE_SYSTEM_PROMPT_FALLBACK_PATH) + self.auto_system_prompt_path = PROMPT_AUTO_SYSTEM_PROMPT_PATH + self.auto_system_prompt_fallback_path = (PROMPT_AUTO_SYSTEM_PROMPT_FALLBACK_PATH) + self.rewrite_all_system_prompt_path = PROMPT_REWRITE_ALL_SYSTEM_PROMPT_PATH + self.rewrite_all_system_prompt_fallback_path = (PROMPT_REWRITE_ALL_SYSTEM_PROMPT_FALLBACK_PATH) + self.rewrite_user_system_prompt_path = (PROMPT_REWRITE_USER_SYSTEM_PROMPT_PATH) + self.rewrite_user_system_prompt_fallback_path = (PROMPT_REWRITE_USER_SYSTEM_PROMPT_FALLBACK_PATH) + self.reload_system_prompts() + + def _build_provider_runtimes(self) -> list[ProviderRuntime]: + runtimes: list[ProviderRuntime] = [] + for provider_name in PROMPT_PROVIDER_PRIORITY: + api_key = PROMPT_API_KEYS[provider_name] + api_base_url = PROMPT_API_BASE_URLS[provider_name] + if not isinstance(api_key, str) or not api_key.strip(): + env_names = ", ".join(PROMPT_PROVIDER_API_KEY_NAMES[provider_name]) + raise RuntimeError("Missing required environment variable: one of " + f"{env_names}") + runtimes.append( + ProviderRuntime( + name=provider_name, + api_key=api_key, + api_base_url=api_base_url, + request_model=self.provider_request_models.get( + provider_name, + self.model, + ), + client=self._build_client( + provider=provider_name, + api_key=api_key, + api_base_url=api_base_url, + ), + )) + return runtimes + + def _build_provider_runtime_stages(self) -> list[list[ProviderRuntime]]: + runtime_by_name = {runtime.name: runtime for runtime in self.provider_runtimes} + stages: list[list[ProviderRuntime]] = [] + for stage_names in PROMPT_PROVIDER_RUNTIME_STAGES: + stage = [runtime_by_name[name] for name in stage_names if name in runtime_by_name] + if stage: + stages.append(stage) + return stages + + def reload_system_prompts(self) -> None: + ( + self.enhance_system_prompt, + self.enhance_system_prompt_source_path, + ) = _load_prompt_required_with_path( + self.enhance_system_prompt_path, + "next-segment", + self.enhance_system_prompt_fallback_path, + ) + ( + self.auto_system_prompt, + self.auto_system_prompt_source_path, + ) = _load_prompt_required_with_path( + self.auto_system_prompt_path, + "auto-extension", + self.auto_system_prompt_fallback_path, + ) + ( + self.rewrite_all_system_prompt, + self.rewrite_all_system_prompt_source_path, + ) = _load_prompt_required_with_path( + self.rewrite_all_system_prompt_path, + "rewrite-window", + self.rewrite_all_system_prompt_fallback_path, + ) + ( + self.rewrite_user_system_prompt, + self.rewrite_user_system_prompt_source_path, + ) = _load_prompt_with_prompt_fallback( + self.rewrite_user_system_prompt_path, + "rewrite-user", + self.rewrite_all_system_prompt, + self.rewrite_all_system_prompt_source_path, + self.rewrite_user_system_prompt_fallback_path, + ) + + def get_prompt_config(self) -> dict[str, Any]: + return { + "next_segment_system_prompt_path": + getattr( + self, + "enhance_system_prompt_source_path", + self.enhance_system_prompt_path, + ), + "auto_extension_system_prompt_path": + getattr( + self, + "auto_system_prompt_source_path", + self.auto_system_prompt_path, + ), + "rewrite_window_system_prompt_path": + getattr( + self, + "rewrite_all_system_prompt_source_path", + self.rewrite_all_system_prompt_path, + ), + "rewrite_user_system_prompt_path": + _resolve_prompt_save_path( + self.rewrite_user_system_prompt_path, + self.rewrite_user_system_prompt_fallback_path, + ), + "next_segment_system_prompt": + self.enhance_system_prompt, + "auto_extension_system_prompt": + self.auto_system_prompt, + "rewrite_window_system_prompt": + self.rewrite_all_system_prompt, + "rewrite_user_system_prompt": + self.rewrite_user_system_prompt, + "rewrite_model": + self.rewrite_default_model, + "rewrite_model_options": + list(self.rewrite_model_options), + "rewrite_temperature": + getattr( + self, + "rewrite_default_temperature", + self.temperature, + ), + } + + def save_prompt_config( + self, + *, + next_segment_system_prompt: str | None = None, + auto_extension_system_prompt: str | None = None, + rewrite_window_system_prompt: str | None = None, + rewrite_user_system_prompt: str | None = None, + rewrite_model: str | None = None, + rewrite_temperature: float | None = None, + ) -> dict[str, Any]: + if next_segment_system_prompt is not None: + normalized = next_segment_system_prompt.strip() + if not normalized: + raise ValueError("next_segment_system_prompt cannot be empty.") + _save_prompt( + _resolve_prompt_save_path( + self.enhance_system_prompt_path, + self.enhance_system_prompt_fallback_path, + ), + normalized, + "next-segment", + ) + + if auto_extension_system_prompt is not None: + normalized = auto_extension_system_prompt.strip() + if not normalized: + raise ValueError("auto_extension_system_prompt cannot be empty.") + _save_prompt( + _resolve_prompt_save_path( + self.auto_system_prompt_path, + self.auto_system_prompt_fallback_path, + ), + normalized, + "auto-extension", + ) + + if rewrite_window_system_prompt is not None: + normalized = rewrite_window_system_prompt.strip() + if not normalized: + raise ValueError("rewrite_window_system_prompt cannot be empty.") + _save_prompt( + _resolve_prompt_save_path( + self.rewrite_all_system_prompt_path, + self.rewrite_all_system_prompt_fallback_path, + ), + normalized, + "rewrite-window", + ) + + if rewrite_user_system_prompt is not None: + normalized = rewrite_user_system_prompt.strip() + if not normalized: + raise ValueError("rewrite_user_system_prompt cannot be empty.") + _save_prompt( + _resolve_prompt_save_path( + self.rewrite_user_system_prompt_path, + self.rewrite_user_system_prompt_fallback_path, + ), + normalized, + "rewrite-user", + ) + + if rewrite_model is not None: + self.set_rewrite_default_model(rewrite_model) + if rewrite_temperature is not None: + self.set_rewrite_default_temperature(rewrite_temperature) + + self.reload_system_prompts() + return self.get_prompt_config() + + def _resolve_timeout_ms(self, timeout_ms: int | None) -> int: + if timeout_ms is None: + return self.default_timeout_ms + return timeout_ms + + def _resolve_initial_stage_timeout_seconds(self) -> float: + timeout_ms = getattr( + self, + "initial_stage_timeout_ms", + PROMPT_INITIAL_STAGE_TIMEOUT_MS, + ) + if not isinstance(timeout_ms, (int, float)) or timeout_ms <= 0: + timeout_ms = PROMPT_INITIAL_STAGE_TIMEOUT_MS + return float(timeout_ms) / 1000.0 + + @staticmethod + def _normalize_model_options(values: list[str]) -> list[str]: + normalized: list[str] = [] + for value in values: + clean_value = value.strip() if isinstance(value, str) else "" + if not clean_value or clean_value in normalized: + continue + normalized.append(clean_value) + return normalized + + def _build_client( + self, + *, + provider: str | None = None, + api_key: str | None = None, + api_base_url: str | None = None, + ) -> Any: + provider_name = provider or self.provider + provider_family = _resolve_provider_family(provider_name) + provider_api_key = api_key or self.api_key + resolved_api_base_url = (self.api_base_url if api_base_url is None else api_base_url) + if provider_family == "cerebras": + if Cerebras is None: + raise RuntimeError("Cerebras SDK is not installed. Install " + "'cerebras-cloud-sdk' to use prompt enhancement.") + return Cerebras(api_key=provider_api_key) + + if provider_family == "groq": + if OpenAI is None: + raise RuntimeError("OpenAI SDK is not installed. Install 'openai' to use " + f"the {provider_name} prompt provider.") + client_kwargs = { + "api_key": provider_api_key, + } + if resolved_api_base_url: + client_kwargs["base_url"] = resolved_api_base_url + return OpenAI(**client_kwargs) + + raise RuntimeError(f"Unsupported prompt provider: {provider_name}") + + def _get_provider_runtimes(self) -> list[ProviderRuntime]: + runtimes = getattr(self, "provider_runtimes", None) + if isinstance(runtimes, list) and runtimes: + return runtimes + return [] + + def _get_provider_runtime_stages(self) -> list[list[ProviderRuntime]]: + stages = getattr(self, "provider_runtime_stages", None) + if isinstance(stages, list) and stages: + normalized_stages = [list(stage) for stage in stages if isinstance(stage, (list, tuple)) and stage] + if normalized_stages: + return normalized_stages + runtimes = self._get_provider_runtimes() + if not runtimes: + return [] + return [runtimes] + + def _get_provider_success_counts_store(self) -> dict[str, int]: + counts = getattr(self, "provider_success_counts", None) + if isinstance(counts, dict): + return counts + counts = {runtime.name: 0 for runtime in self._get_provider_runtimes()} + self.provider_success_counts = counts + return counts + + def _record_provider_success(self, provider_name: str) -> None: + counts = self._get_provider_success_counts_store() + current_value = counts.get(provider_name, 0) + counts[provider_name] = (current_value if isinstance(current_value, int) and current_value >= 0 else 0) + 1 + + def get_provider_success_counts(self) -> dict[str, int]: + counts = self._get_provider_success_counts_store() + normalized: dict[str, int] = {} + for provider_name in PROMPT_PROVIDER_PRIORITY: + raw_value = counts.get(provider_name, 0) + normalized[provider_name] = (raw_value if isinstance(raw_value, int) and raw_value >= 0 else 0) + for provider_name, raw_value in counts.items(): + if provider_name in normalized: + continue + normalized[provider_name] = (raw_value if isinstance(raw_value, int) and raw_value >= 0 else 0) + return normalized + + def _resolve_provider_request_model( + self, + provider_name: str, + requested_model: str | None, + ) -> str: + provider_request_models = getattr( + self, + "provider_request_models", + {}, + ) + candidate = (requested_model.strip() if isinstance(requested_model, str) else "") + if not candidate: + return provider_request_models.get( + provider_name, + self.model, + ) + if candidate == self.rewrite_default_model: + return provider_request_models.get( + provider_name, + candidate, + ) + return candidate + + def _build_body( + self, + *, + system_prompt: str, + user_payload: dict[str, Any], + model: str | None = None, + temperature: float | None = None, + ) -> dict[str, Any]: + return { + "model": + model or self.model, + "temperature": + self.temperature if temperature is None else temperature, + "max_completion_tokens": + self.max_completion_tokens, + "response_format": { + "type": "json_object" + }, + "messages": [ + { + "role": "system", + "content": system_prompt, + }, + { + "role": "user", + "content": json.dumps(user_payload, ensure_ascii=False), + }, + ], + } + + async def _run_blocking_request(self, func, /, *args, **kwargs): + result_queue: queue.Queue[tuple[str, Any]] = queue.Queue(maxsize=1) + + def _runner() -> None: + try: + result = func(*args, **kwargs) + except Exception as exc: # pragma: no cover - thread handoff + result_queue.put(("error", exc)) + else: + result_queue.put(("ok", result)) + + threading.Thread( + target=_runner, + name="prompt-provider-call", + daemon=True, + ).start() + while True: + try: + status, payload = result_queue.get_nowait() + except queue.Empty: + await asyncio.sleep(0.001) + continue + + if status == "error": + raise payload + return payload + + async def _run_provider_race( + self, + operation_name: str, + attempt_factory, + *, + initial_stage_timeout_seconds: float | None = None, + fallback_stage_timeout_seconds: float | None = None, + ): + runtime_stages = self._get_provider_runtime_stages() + if not runtime_stages: + raise RuntimeError(f"{operation_name} is unavailable without provider runtimes.") + errors: list[str] = [] + for stage_index, stage_runtimes in enumerate(runtime_stages): + stage_timeout_seconds = fallback_stage_timeout_seconds + if stage_index == 0 and len(runtime_stages) > 1: + stage_timeout_seconds = initial_stage_timeout_seconds + if stage_index > 0: + _enhance_print( + "INFO", + f"{operation_name} entering fallback stage with providers=" + f"{','.join(runtime.name for runtime in stage_runtimes)}", + ) + + queue: asyncio.Queue[tuple[str, str, Any]] = asyncio.Queue() + + async def _runner(runtime: ProviderRuntime) -> None: + try: + if (isinstance(stage_timeout_seconds, (int, float)) and stage_timeout_seconds > 0): + result = await asyncio.wait_for( + attempt_factory(runtime), + timeout=stage_timeout_seconds, + ) + else: + result = await attempt_factory(runtime) + except asyncio.TimeoutError: + await queue.put(( + "error", + runtime.name, + RuntimeError("timed out after " + f"{stage_timeout_seconds:.2f}s"), + )) + except Exception as exc: + await queue.put(("error", runtime.name, exc)) + else: + await queue.put(("success", runtime.name, result)) + + tasks = [asyncio.create_task(_runner(runtime)) for runtime in stage_runtimes] + stage_errors: list[str] = [] + try: + for _ in tasks: + status, provider_name, payload = await queue.get() + if status == "success": + self._record_provider_success(provider_name) + for task in tasks: + task.cancel() + return payload + + stage_errors.append(f"provider={provider_name} error={payload}") + _enhance_print( + "WARN", + f"{operation_name} failed for provider=" + f"{provider_name}: {payload}", + ) + finally: + for task in tasks: + task.cancel() + + if stage_errors: + errors.extend(stage_errors) + + raise RuntimeError(f"{operation_name} failed for all providers. " + " | ".join(errors)) + + async def _request_content( + self, + *, + system_prompt: str, + user_payload: dict[str, Any], + timeout_ms: int, + model: str | None = None, + temperature: float | None = None, + ) -> tuple[dict[str, Any], str]: + timeout_seconds = max(timeout_ms, self.http_timeout_ms) / 1000.0 + body = self._build_body( + system_prompt=system_prompt, + user_payload=user_payload, + model=model, + temperature=temperature, + ) + return await self._request_content_with_body( + body=body, + timeout_seconds=timeout_seconds, + ) + + async def _request_content_with_body( + self, + *, + body: dict[str, Any], + timeout_seconds: float, + ) -> tuple[dict[str, Any], str]: + runtimes = self._get_provider_runtimes() + if runtimes: + return await self._request_content_with_body_for_runtime( + runtime=runtimes[0], + body=body, + timeout_seconds=timeout_seconds, + ) + + provider_body = dict(body) + provider_body["model"] = self._resolve_provider_request_model( + self.provider, + provider_body.get("model"), + ) + provider_family = _resolve_provider_family(self.provider) + if provider_family == "cerebras": + response_json = await self._request_content_with_body_cerebras( + client=self.client, + body=provider_body, + timeout_seconds=timeout_seconds, + ) + elif provider_family == "groq": + response_json = await self._request_content_with_body_openai_compatible( + provider_name=self.provider, + client=self.client, + body=provider_body, + timeout_seconds=timeout_seconds, + ) + else: + raise RuntimeError("Prompt enhancer has no provider runtimes configured.") + + content = _extract_content_or_empty(response_json) + return response_json, content + + async def _request_content_with_body_for_runtime( + self, + *, + runtime: ProviderRuntime, + body: dict[str, Any], + timeout_seconds: float, + ) -> tuple[dict[str, Any], str]: + provider_body = dict(body) + provider_body["model"] = self._resolve_provider_request_model( + runtime.name, + provider_body.get("model"), + ) + provider_family = _resolve_provider_family(runtime.name) + if provider_family == "cerebras": + response_json = await self._request_content_with_body_cerebras( + client=runtime.client, + body=provider_body, + timeout_seconds=timeout_seconds, + ) + elif provider_family == "groq": + response_json = await self._request_content_with_body_openai_compatible( + provider_name=provider_family, + client=runtime.client, + body=provider_body, + timeout_seconds=timeout_seconds, + ) + else: # pragma: no cover - config validation should reject this. + raise RuntimeError(f"Unsupported prompt provider: {runtime.name}") + + content = _extract_content_or_empty(response_json) + return response_json, content + + async def _request_content_with_body_cerebras( + self, + *, + client: Any, + body: dict[str, Any], + timeout_seconds: float, + ) -> dict[str, Any]: + del timeout_seconds + cerebras_body = { + "model": body.get("model"), + "messages": body.get("messages"), + "temperature": body.get("temperature"), + } + cerebras_body = {key: value for key, value in cerebras_body.items() if value is not None} + response = await self._run_blocking_request( + client.chat.completions.create, + **cerebras_body, + ) + return _dump_response_json(response) + + async def _request_content_with_body_openai_compatible( + self, + *, + provider_name: str, + client: Any, + body: dict[str, Any], + timeout_seconds: float, + ) -> dict[str, Any]: + del provider_name, timeout_seconds + openai_body = { + "model": body.get("model"), + "messages": body.get("messages"), + "temperature": body.get("temperature"), + } + max_completion_tokens = body.get("max_completion_tokens") + if max_completion_tokens is not None: + openai_body["max_completion_tokens"] = max_completion_tokens + openai_body = {key: value for key, value in openai_body.items() if value is not None} + response = await self._run_blocking_request( + client.chat.completions.create, + **openai_body, + ) + return _dump_response_json(response) + + async def _request_json( + self, + *, + system_prompt: str, + user_payload: dict[str, Any], + timeout_ms: int, + model: str | None = None, + temperature: float | None = None, + ) -> tuple[dict[str, Any], dict[str, Any], str]: + response_json, content = await self._request_content( + system_prompt=system_prompt, + user_payload=user_payload, + timeout_ms=timeout_ms, + model=model, + temperature=temperature, + ) + parsed = _parse_json_response(content) + return parsed, response_json, content + + def _require_prompt_field(self, parsed: dict[str, Any], field_name: str) -> str: + value = parsed.get(field_name) + if not isinstance(value, str): + raise ValueError(f"Missing {field_name} string.") + prompt = value.strip() + if not prompt: + raise ValueError(f"{field_name} is empty.") + return prompt + + def _optional_prompt_field( + self, + parsed: dict[str, Any], + field_name: str, + ) -> str | None: + value = parsed.get(field_name) + if not isinstance(value, str): + return None + normalized = value.strip() + return normalized or None + + def _require_prompt_list_field( + self, + parsed: dict[str, Any], + field_name: str, + expected_len: int, + ) -> list[str]: + value = parsed.get(field_name) + if not isinstance(value, list): + raise ValueError(f"Missing {field_name} list.") + + prompts: list[str] = [] + for item in value: + if not isinstance(item, str): + raise ValueError(f"{field_name} contains non-string item.") + normalized = item.strip() + if not normalized: + raise ValueError(f"{field_name} contains empty prompt.") + prompts.append(normalized) + + if len(prompts) != expected_len: + raise ValueError(f"{field_name} length mismatch: expected {expected_len}, got {len(prompts)}.") + return prompts + + def _extract_rewrite_segment_prompts( + self, + parsed: dict[str, Any], + expected_len: int, + ) -> list[str]: + return self._require_prompt_list_field( + parsed, + "segment_prompts", + expected_len, + ) + + def _normalize_rewrite_prompt_item(self, item: Any) -> str | None: + if isinstance(item, str): + normalized = item.strip() + return normalized or None + if not isinstance(item, dict): + return None + + for key in ( + "prompt", + "text", + "segment_prompt", + "content", + "description", + ): + value = item.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + return None + + def _maybe_extract_rewrite_prompt_list( + self, + value: Any, + expected_len: int, + ) -> list[str] | None: + if not isinstance(value, list): + return None + + prompts: list[str] = [] + for item in value: + normalized = self._normalize_rewrite_prompt_item(item) + if normalized is None: + return None + prompts.append(normalized) + + if len(prompts) != expected_len: + return None + return prompts + + def _extract_indexed_rewrite_prompts( + self, + parsed: dict[str, Any], + expected_len: int, + ) -> list[str] | None: + indexed_prompts: dict[int, str] = {} + for key, value in parsed.items(): + if not isinstance(key, str): + continue + match = re.fullmatch( + r"(?:segment|prompt|scene|shot)[ _-]?(\d+)", + key.strip(), + flags=re.IGNORECASE, + ) + if not match: + continue + segment_idx = int(match.group(1)) + if segment_idx < 1 or segment_idx > expected_len: + continue + normalized = self._normalize_rewrite_prompt_item(value) + if normalized is None: + return None + indexed_prompts[segment_idx] = normalized + + if any(idx not in indexed_prompts for idx in range(1, expected_len + 1)): + return None + return [indexed_prompts[idx] for idx in range(1, expected_len + 1)] + + def _extract_numbered_rewrite_prompts_from_text( + self, + content: str, + expected_len: int, + ) -> list[str] | None: + lines = content.splitlines() + if not lines: + return None + + numbered_segments: dict[int, list[str]] = {} + current_idx: int | None = None + for raw_line in lines: + line = raw_line.strip() + if not line: + continue + + normalized_line = re.sub( + r"^\s*[-*]\s*", + "", + line, + ) + match = re.match( + (r'^(?:\*\*)?(?:segment|scene|shot|prompt)?\s*[_ -]?(\d+)' + r'(?:\*\*)?\s*[:.)-]\s*(.+)$'), + normalized_line, + flags=re.IGNORECASE, + ) + if match: + segment_idx = int(match.group(1)) + if 1 <= segment_idx <= expected_len: + numbered_segments[segment_idx] = [match.group(2).strip()] + current_idx = segment_idx + continue + + if current_idx is not None: + numbered_segments[current_idx].append(normalized_line) + + if any(idx not in numbered_segments for idx in range(1, expected_len + 1)): + return None + + prompts: list[str] = [] + for idx in range(1, expected_len + 1): + prompt = " ".join(part for part in numbered_segments[idx] if part).strip() + prompt = prompt.strip().strip('"').strip("'").strip() + if not prompt: + return None + prompts.append(prompt) + return prompts + + def _extract_rewrite_segment_prompts_lenient( + self, + parsed: dict[str, Any], + expected_len: int, + *, + raw_content: str | None = None, + ) -> list[str]: + candidate_dicts: list[dict[str, Any]] = [parsed] + for key in ("rollout", "current_rollout", "rewritten_rollout"): + value = parsed.get(key) + if isinstance(value, dict): + candidate_dicts.append(value) + + for candidate in candidate_dicts: + for key in ( + "segment_prompts", + "rewritten_prompts", + "prompts", + "segments", + ): + prompts = self._maybe_extract_rewrite_prompt_list( + candidate.get(key), + expected_len, + ) + if prompts is not None: + return prompts + + indexed_prompts = self._extract_indexed_rewrite_prompts( + candidate, + expected_len, + ) + if indexed_prompts is not None: + return indexed_prompts + + if isinstance(raw_content, str) and raw_content.strip(): + numbered_prompts = self._extract_numbered_rewrite_prompts_from_text( + raw_content, + expected_len, + ) + if numbered_prompts is not None: + return numbered_prompts + + raise ValueError("No rewrite segment prompts found in assistant response.") + + def _extract_rewrite_rollout( + self, + parsed: dict[str, Any], + expected_len: int, + *, + raw_content: str | None = None, + preset_id: str | None = None, + preset_label: str | None = None, + ) -> tuple[str, str, list[str]]: + candidate_dicts: list[dict[str, Any]] = [parsed] + for key in ("rollout", "current_rollout", "rewritten_rollout"): + value = parsed.get(key) + if isinstance(value, dict): + candidate_dicts.append(value) + + for candidate in candidate_dicts: + try: + rollout_id = self._require_prompt_field(candidate, "id") + rollout_label = self._require_prompt_field(candidate, "label") + prompts = self._extract_rewrite_segment_prompts( + candidate, + expected_len, + ) + return rollout_id, rollout_label, prompts + except ValueError: + continue + + rollout_id = next( + (value for value in (self._optional_prompt_field(candidate, "id") + for candidate in candidate_dicts) if value is not None), + _resolve_rollout_id(preset_id), + ) + rollout_label = next( + (value for value in (self._optional_prompt_field(candidate, "label") + for candidate in candidate_dicts) if value is not None), + _resolve_rollout_label(preset_label), + ) + prompts = self._extract_rewrite_segment_prompts_lenient( + parsed, + expected_len, + raw_content=raw_content, + ) + return rollout_id, rollout_label, prompts + + def _extract_rewrite_rollout_from_content( + self, + response_content: str, + expected_len: int, + *, + preset_id: str | None = None, + preset_label: str | None = None, + ) -> tuple[str, str, list[str]]: + parse_error: ValueError | None = None + try: + parsed = _parse_json_response(response_content) + except ValueError as exc: + parsed = {} + parse_error = exc + + try: + return self._extract_rewrite_rollout( + parsed, + expected_len, + raw_content=response_content, + preset_id=preset_id, + preset_label=preset_label, + ) + except ValueError as exc: + if (parse_error is not None and str(exc) == "No rewrite segment prompts found in assistant response."): + raise parse_error from exc + raise + + def _extract_single_clip_prompt(self, content: str) -> str: + text = content.strip() + if not text: + raise ValueError("Assistant response is empty.") + + parsed = _parse_json_response(text) + return self._require_prompt_field(parsed, "prompt") + + def set_rewrite_default_model(self, rewrite_model: str) -> str: + normalized = (rewrite_model.strip() if isinstance(rewrite_model, str) else "") + if not normalized: + raise ValueError("rewrite_model cannot be empty.") + if normalized not in self.rewrite_model_options: + allowed = ", ".join(self.rewrite_model_options) + raise ValueError(f"Unsupported rewrite_model {normalized!r}. " + f"Expected one of: {allowed}.") + self.rewrite_default_model = normalized + return self.rewrite_default_model + + def set_rewrite_default_temperature( + self, + rewrite_temperature: float, + ) -> float: + if not isinstance(rewrite_temperature, (int, float)): + raise ValueError("rewrite_temperature must be numeric.") + self.rewrite_default_temperature = self.resolve_rewrite_temperature(float(rewrite_temperature)) + return self.rewrite_default_temperature + + def resolve_rewrite_system_prompt( + self, + system_prompt_override: str | None, + ) -> str: + normalized = _normalize_prompt(system_prompt_override) + if normalized: + return normalized + return self.rewrite_all_system_prompt + + def resolve_rewrite_new_rollout_system_prompt( + self, + system_prompt_override: str | None, + ) -> str: + normalized = _normalize_prompt(system_prompt_override) + if normalized: + return normalized + return getattr( + self, + "rewrite_user_system_prompt", + self.rewrite_all_system_prompt, + ) + + def resolve_rewrite_model(self, requested_model: str | None) -> str: + candidate = requested_model.strip() if isinstance(requested_model, str) else "" + if candidate and candidate in self.rewrite_model_options: + return candidate + return self.rewrite_default_model + + def get_rewrite_model_config(self) -> dict[str, Any]: + return { + "default_model": self.rewrite_default_model, + "options": list(self.rewrite_model_options), + } + + def resolve_rewrite_temperature(self, requested_temperature: float | None) -> float: + if requested_temperature is None: + return getattr( + self, + "rewrite_default_temperature", + self.temperature, + ) + try: + numeric_value = float(requested_temperature) + except (TypeError, ValueError): + return getattr( + self, + "rewrite_default_temperature", + self.temperature, + ) + return min(2.0, max(0.0, numeric_value)) + + async def enhance_prompt( + self, + conditioning_prompt: str, + *, + locked_segments: list[str] | None = None, + next_segment_idx: int | None = None, + preset_id: str | None = None, + mode: str = "user", + model: str | None = None, + timeout_ms: int | None = None, + ) -> EnhanceResult: + cleaned = _normalize_prompt(conditioning_prompt) + resolved_model = (self.resolve_rewrite_model(model) + if isinstance(model, str) and model.strip() else self.resolve_rewrite_model(None)) + is_single_clip_mode = mode in {"single_clip", "simple5s", "single5s"} + if not cleaned: + return EnhanceResult( + prompt="", + fallback_used=True, + error="No valid prompt provided.", + provider=self.provider_label, + model=resolved_model, + latency_ms=0.0, + ) + + effective_timeout_ms = self._resolve_timeout_ms(timeout_ms) + if is_single_clip_mode: + request_system_prompt = self.auto_system_prompt + user_payload = { + "request": ( + "Expand the user prompt into one detailed prompt for a " + "single 5-second LTX-2.3 video clip. Respond with " + 'valid JSON only as {"prompt": "..."}.' # noqa: E501 + ), + "user_prompt": + cleaned, + } + else: + locked_segments_clean = [ + segment for segment in (_normalize_prompt(item) for item in (locked_segments or [])) if segment + ] + resolved_next_segment_idx = (next_segment_idx if isinstance(next_segment_idx, int) else None) + if resolved_next_segment_idx is None or resolved_next_segment_idx < 1: + resolved_next_segment_idx = len(locked_segments_clean) + 1 + next_segment_key = f"segment_{resolved_next_segment_idx}" + locked_text = _format_locked_segments(locked_segments_clean) + request_system_prompt = self.enhance_system_prompt + user_payload = { + "request": ( + "<locked_segments>\n" + f"{locked_text}\n" + "</locked_segments>\n\n" + f"<conditioning_prompt>{cleaned}</conditioning_prompt>\n\n" + f"Write exactly one new segment ({next_segment_key}) " + "continuing from the locked segments. " + 'Respond with valid JSON only as {"next_prompt": "..."}.' # noqa: E501 + ), + } + + t0 = time.perf_counter() + try: + _enhance_print( + "INFO", + "Enhancing prompt: " + f"{cleaned}, system_prompt: {request_system_prompt}, " + f"mode={mode}", + ) + _enhance_print("INFO", f"user_payload: {user_payload}") + runtimes = self._get_provider_runtimes() + if runtimes: + timeout_seconds = max( + effective_timeout_ms, + self.http_timeout_ms, + ) / 1000.0 + request_body = self._build_body( + system_prompt=request_system_prompt, + user_payload=user_payload, + model=resolved_model, + ) + + async def _attempt(runtime: ProviderRuntime): + response_content: str | None = None + try: + _, response_content = await self._request_content_with_body_for_runtime( + runtime=runtime, + body=request_body, + timeout_seconds=timeout_seconds, + ) + if is_single_clip_mode: + prompt = self._extract_single_clip_prompt(response_content) + else: + parsed = _parse_json_response(response_content) + prompt = self._require_prompt_field( + parsed, + "next_prompt", + ) + return _resolve_provider_label(runtime.name), prompt + except Exception as exc: + error_detail = str(exc) + if (isinstance(response_content, str) and response_content.strip()): + error_detail = (f"{error_detail} | assistant_response=" + f"{_preview_text(response_content, limit=240)}") + raise RuntimeError(error_detail) from exc + + provider_name, enhanced_prompt = await self._run_provider_race( + "enhance_prompt", + _attempt, + initial_stage_timeout_seconds=(self._resolve_initial_stage_timeout_seconds()), + fallback_stage_timeout_seconds=timeout_seconds, + ) + else: + response_json, response_content = await self._request_content( + system_prompt=request_system_prompt, + user_payload=user_payload, + timeout_ms=effective_timeout_ms, + model=resolved_model, + ) + del response_json + if is_single_clip_mode: + enhanced_prompt = self._extract_single_clip_prompt(response_content) + else: + parsed = _parse_json_response(response_content) + enhanced_prompt = self._require_prompt_field(parsed, "next_prompt") + provider_name = self.provider_label + latency_ms = (time.perf_counter() - t0) * 1000.0 + return EnhanceResult( + prompt=enhanced_prompt, + fallback_used=False, + error=None, + provider=provider_name, + model=resolved_model, + latency_ms=latency_ms, + ) + except Exception as exc: + latency_ms = (time.perf_counter() - t0) * 1000.0 + return EnhanceResult( + prompt="", + fallback_used=True, + error=str(exc), + provider=self.provider_label, + model=resolved_model, + latency_ms=latency_ms, + ) + + async def generate_auto_prompt( + self, + *, + locked_segments: list[str] | None = None, + next_segment_idx: int | None = None, + model: str | None = None, + timeout_ms: int | None = None, + ) -> EnhanceResult: + locked_segments_clean = [ + segment for segment in (_normalize_prompt(item) for item in (locked_segments or [])) if segment + ] + resolved_next_segment_idx = (next_segment_idx if isinstance(next_segment_idx, int) else None) + if resolved_next_segment_idx is None or resolved_next_segment_idx < 1: + resolved_next_segment_idx = len(locked_segments_clean) + 1 + next_segment_key = f"segment_{resolved_next_segment_idx}" + locked_text = _format_locked_segments(locked_segments_clean) + + effective_timeout_ms = self._resolve_timeout_ms(timeout_ms) + user_payload = { + "request": ( + "<locked_segments>\n" + f"{locked_text}\n" + "</locked_segments>\n\n" + f"Write exactly one new segment ({next_segment_key}) " + "that continues linearly from the locked segments. " + "Infer the next narrative beat from this history. " + 'Respond with valid JSON only as {"next_prompt": "..."}.' # noqa: E501 + ), + } + + t0 = time.perf_counter() + resolved_model = (self.resolve_rewrite_model(model) + if isinstance(model, str) and model.strip() else self.resolve_rewrite_model(None)) + try: + _enhance_print( + "INFO", + "Auto extension request: " + f"next_segment={resolved_next_segment_idx} " + f"locked_count={len(locked_segments_clean)}", + ) + _enhance_print("INFO", f"auto_user_payload: {user_payload}") + runtimes = self._get_provider_runtimes() + if runtimes: + timeout_seconds = max( + effective_timeout_ms, + self.http_timeout_ms, + ) / 1000.0 + request_body = self._build_body( + system_prompt=self.auto_system_prompt, + user_payload=user_payload, + model=resolved_model, + ) + + async def _attempt(runtime: ProviderRuntime): + response_content: str | None = None + try: + _, response_content = await self._request_content_with_body_for_runtime( + runtime=runtime, + body=request_body, + timeout_seconds=timeout_seconds, + ) + parsed = _parse_json_response(response_content) + prompt = self._require_prompt_field( + parsed, + "next_prompt", + ) + return _resolve_provider_label(runtime.name), prompt + except Exception as exc: + error_detail = str(exc) + if (isinstance(response_content, str) and response_content.strip()): + error_detail = (f"{error_detail} | assistant_response=" + f"{_preview_text(response_content, limit=240)}") + raise RuntimeError(error_detail) from exc + + provider_name, next_prompt = await self._run_provider_race( + "generate_auto_prompt", + _attempt, + initial_stage_timeout_seconds=(self._resolve_initial_stage_timeout_seconds()), + fallback_stage_timeout_seconds=timeout_seconds, + ) + else: + response_json, response_content = await self._request_content( + system_prompt=self.auto_system_prompt, + user_payload=user_payload, + timeout_ms=effective_timeout_ms, + model=resolved_model, + ) + del response_json + parsed = _parse_json_response(response_content) + next_prompt = self._require_prompt_field(parsed, "next_prompt") + provider_name = self.provider_label + latency_ms = (time.perf_counter() - t0) * 1000.0 + return EnhanceResult( + prompt=next_prompt, + fallback_used=False, + error=None, + provider=provider_name, + model=resolved_model, + latency_ms=latency_ms, + ) + except Exception as exc: + latency_ms = (time.perf_counter() - t0) * 1000.0 + return EnhanceResult( + prompt="", + fallback_used=True, + error=str(exc), + provider=self.provider_label, + model=resolved_model, + latency_ms=latency_ms, + ) + + async def rewrite_prompt_sequence( + self, + prompts: list[str], + *, + preset_id: str | None = None, + preset_label: str | None = None, + rewrite_instruction: str | None = None, + rewrite_model: str | None = None, + rewrite_temperature: float | None = None, + timeout_ms: int | None = None, + system_prompt_override: str | None = None, + ) -> RewriteResult: + resolved_rewrite_model = self.resolve_rewrite_model(rewrite_model) + resolved_rewrite_temperature = self.resolve_rewrite_temperature(rewrite_temperature) + resolved_system_prompt = self.resolve_rewrite_system_prompt(system_prompt_override) + cleaned_prompts = normalize_prompt_window_prompts(prompts) + rewrite_instruction_text = _normalize_prompt(rewrite_instruction) + if not cleaned_prompts and not rewrite_instruction_text: + return RewriteResult( + prompts=[], + fallback_used=True, + error="No valid prompts to rewrite or generate.", + provider=self.provider_label, + model=resolved_rewrite_model, + latency_ms=0.0, + rollout_id=_resolve_rollout_id(preset_id), + rollout_label=_resolve_rollout_label(preset_label), + raw_response_text=None, + ) + expected_len = (len(cleaned_prompts) if cleaned_prompts else DEFAULT_REWRITE_SEGMENT_COUNT) + + effective_timeout_ms = self._resolve_timeout_ms(timeout_ms) + timeout_seconds = max(effective_timeout_ms, self.http_timeout_ms) / 1000.0 + rewrite_body = build_rewrite_request_body( + system_prompt=resolved_system_prompt, + prompt_window_prompts=cleaned_prompts, + preset_id=preset_id, + preset_label=preset_label, + rewrite_instruction=rewrite_instruction_text, + model=resolved_rewrite_model, + temperature=resolved_rewrite_temperature, + max_completion_tokens=self.max_completion_tokens, + ) + + t0 = time.perf_counter() + raw_response_text: str | None = None + try: + runtimes = self._get_provider_runtimes() + if runtimes: + + async def _attempt(runtime: ProviderRuntime): + response_content: str | None = None + try: + response_json, response_content = await self._request_content_with_body_for_runtime( + runtime=runtime, + body=rewrite_body, + timeout_seconds=timeout_seconds, + ) + if not response_content: + response_content = json.dumps( + response_json, + ensure_ascii=False, + ) + ( + rollout_id, + rollout_label, + rewritten_prompts, + ) = self._extract_rewrite_rollout_from_content( + response_content, + expected_len, + preset_id=preset_id, + preset_label=preset_label, + ) + return ( + _resolve_provider_label(runtime.name), + response_content, + rollout_id, + rollout_label, + rewritten_prompts, + ) + except Exception as exc: + error_detail = str(exc) + if (isinstance(response_content, str) and response_content.strip()): + error_detail = (f"{error_detail} | assistant_response=" + f"{_preview_text(response_content, limit=240)}") + raise RuntimeError(error_detail) from exc + + ( + provider_name, + raw_response_text, + rollout_id, + rollout_label, + rewritten_prompts, + ) = await self._run_provider_race( + "rewrite_prompt_sequence", + _attempt, + initial_stage_timeout_seconds=(self._resolve_initial_stage_timeout_seconds()), + fallback_stage_timeout_seconds=timeout_seconds, + ) + else: + response_json, raw_response_text = await self._request_content_with_body( + body=rewrite_body, + timeout_seconds=timeout_seconds, + ) + if not raw_response_text: + raw_response_text = json.dumps( + response_json, + ensure_ascii=False, + ) + ( + rollout_id, + rollout_label, + rewritten_prompts, + ) = self._extract_rewrite_rollout_from_content( + raw_response_text, + expected_len, + preset_id=preset_id, + preset_label=preset_label, + ) + provider_name = self.provider_label + latency_ms = (time.perf_counter() - t0) * 1000.0 + return RewriteResult( + prompts=rewritten_prompts, + fallback_used=False, + error=None, + provider=provider_name, + model=resolved_rewrite_model, + latency_ms=latency_ms, + rollout_id=rollout_id, + rollout_label=rollout_label, + raw_response_text=raw_response_text, + ) + except Exception as exc: + latency_ms = (time.perf_counter() - t0) * 1000.0 + return RewriteResult( + prompts=cleaned_prompts, + fallback_used=True, + error=str(exc), + provider=self.provider_label, + model=resolved_rewrite_model, + latency_ms=latency_ms, + rollout_id=_resolve_rollout_id(preset_id), + rollout_label=_resolve_rollout_label(preset_label), + raw_response_text=raw_response_text, + ) diff --git a/apps/dreamverse/dreamverse/prompt_safety.py b/apps/dreamverse/dreamverse/prompt_safety.py new file mode 100644 index 0000000000..7e88dca769 --- /dev/null +++ b/apps/dreamverse/dreamverse/prompt_safety.py @@ -0,0 +1,202 @@ +from __future__ import annotations +# mypy: ignore-errors + +import importlib +import os +import time +from dataclasses import dataclass +from functools import cache +from pathlib import Path + +_SERVER_DIR = Path(__file__).resolve().parent +_REPO_ROOT = _SERVER_DIR.parent +_DEFAULT_CLASSIFIER_DIR = _REPO_ROOT / "classifiers" +CLASSIFIER_DIR = Path( + os.path.expandvars(os.path.expanduser(os.getenv("LTX2_CLASSIFIER_DIR", str(_DEFAULT_CLASSIFIER_DIR))))) + + +@dataclass(frozen=True) +class BlockedPrompt: + index: int + prompt: str + error: str + + +def _load_fasttext_module(): + try: + return importlib.import_module("fasttext") + except ImportError as exc: + raise RuntimeError("Prompt safety is enabled but the `fasttext` package is not installed.") from exc + + +def resolve_classifier_path( + classifier_kind: str, + env_var: str, + filename: str, + legacy_path: str, + shared_filename: str, +) -> str: + candidates: list[Path] = [] + env_path = os.getenv(env_var) + if env_path: + candidates.append(Path(os.path.expandvars(os.path.expanduser(env_path)))) + candidates.extend([ + CLASSIFIER_DIR / filename, + Path(f"/home/shared/{shared_filename}"), + Path(legacy_path), + ]) + + for candidate in candidates: + if candidate.is_file(): + return str(candidate) + + checked = "\n".join(f" - {candidate}" for candidate in candidates) + raise FileNotFoundError(f"Could not find the {classifier_kind} classifier.\n" + f"Checked:\n{checked}\n" + "Set LTX2_CLASSIFIER_DIR or the appropriate classifier path environment " + "variable.") + + +@cache +def load_fasttext_model(model_path: str): + return _load_fasttext_module().load_model(model_path) + + +def fasttext_predict( + model_path: str, + text: str, + classifier_name: str, +) -> tuple[str, float]: + model = load_fasttext_model(model_path) + text = text.replace("\n", " ") + start_time = time.perf_counter() + try: + labels, probs = model.predict(text) + except ValueError as error: + if "Unable to avoid copy while creating an array" not in str(error): + raise + predictions = model.f.predict(f"{text}\n", 1, 0.0, "strict") + if not predictions: + raise ValueError("fastText returned no predictions") from error + probs, labels = zip(*predictions, strict=False) + latency_ms = (time.perf_counter() - start_time) * 1000.0 + identifier = labels[0].replace("__label__", "") + confidence = float(probs[0]) + print("[safety] " + f"{classifier_name} fastText latency={latency_ms:.2f}ms " + f"label={identifier} confidence={confidence:.4f}") + return identifier, confidence + + +def _normalize_classifier_label(identifier: str) -> str: + return identifier.strip().lower().replace("-", "_").replace(" ", "_") + + +def _label_matches( + identifier: str, + blocked_markers: tuple[str, ...], + safe_markers: tuple[str, ...], +) -> bool: + normalized = _normalize_classifier_label(identifier) + tokens = tuple(token for token in normalized.split("_") if token) + + def has_marker(marker: str) -> bool: + normalized_marker = _normalize_classifier_label(marker) + return (normalized == normalized_marker or normalized_marker in tokens) + + if any(has_marker(marker) for marker in safe_markers): + return False + return any(has_marker(marker) for marker in blocked_markers) + + +class PromptSafetyFilter: + + def __init__( + self, + *, + nsfw_classifier=None, + hate_speech_classifier=None, + ): + if nsfw_classifier is None or hate_speech_classifier is None: + _load_fasttext_module() + + if nsfw_classifier is None: + nsfw_model_path = resolve_classifier_path( + "NSFW", + "LTX2_NSFW_CLASSIFIER_PATH", + "jigsaw_fasttext_bigrams_nsfw_final.bin", + "/data/classifiers/dolma_fasttext_nsfw_jigsaw_model.bin", + "dolma-jigsaw-fasttext-bigrams-nsfw-final.bin", + ) + self._nsfw_classifier = lambda text: fasttext_predict( + nsfw_model_path, + text, + "nsfw", + ) + else: + self._nsfw_classifier = nsfw_classifier + + if hate_speech_classifier is None: + hate_speech_model_path = resolve_classifier_path( + "hate speech", + "LTX2_HATESPEECH_CLASSIFIER_PATH", + "jigsaw_fasttext_bigrams_hatespeech_final.bin", + "/data/classifiers/dolma_fasttext_hatespeech_jigsaw_model.bin", + "dolma-jigsaw-fasttext-bigrams-hatespeech-final.bin", + ) + self._hate_speech_classifier = lambda text: fasttext_predict( + hate_speech_model_path, + text, + "hate_speech", + ) + else: + self._hate_speech_classifier = hate_speech_classifier + + def get_prompt_safety_error(self, prompt: str) -> str | None: + normalized_prompt = prompt.strip() + if not normalized_prompt: + return None + + nsfw_label, _ = self._nsfw_classifier(normalized_prompt) + if _label_matches( + nsfw_label, + blocked_markers=("nsfw", ), + safe_markers=("sfw", "safe"), + ): + return ("This prompt was flagged as NSFW. " + "You can't generate a video with this specified prompt.") + + hate_label, _ = self._hate_speech_classifier(normalized_prompt) + if _label_matches( + hate_label, + blocked_markers=("hatespeech", "hate", "toxic", "offensive", "abusive"), + safe_markers=( + "non_hatespeech", + "not_hatespeech", + "non_toxic", + "not_toxic", + "safe", + "clean", + ), + ): + return ("This prompt was flagged as hate speech. " + "You can't generate a video with this specified prompt.") + + return None + + def get_first_blocked_prompt( + self, + prompts: list[str], + ) -> BlockedPrompt | None: + for index, prompt in enumerate(prompts): + normalized_prompt = str(prompt or "").strip() + if not normalized_prompt: + continue + error = self.get_prompt_safety_error(normalized_prompt) + if error is not None: + return BlockedPrompt( + index=index, + prompt=normalized_prompt, + error=error, + ) + return None diff --git a/apps/dreamverse/dreamverse/prompts/README.md b/apps/dreamverse/dreamverse/prompts/README.md new file mode 100644 index 0000000000..08929e746f --- /dev/null +++ b/apps/dreamverse/dreamverse/prompts/README.md @@ -0,0 +1,59 @@ +# Prompt Files + +This directory contains the editable system prompts used by the +`PromptEnhancer` in `server/prompt_enhancer.py`. + +## `next_segment_system_prompt.md` + +Use this prompt for guided continuation. + +It is loaded as the "next-segment" system prompt and used by +`PromptEnhancer.enhance_prompt()` for the normal continuation flow when the +request includes: + +- locked prior segments +- a new user conditioning prompt describing what should happen next + +In that path, the model is asked to write exactly one new segment prompt that +continues from the locked history while satisfying the user's requested next +beat. + +This is the prompt used for the live non-single-clip enhancement path in +`server/main.py`. + +## `auto_extension_system_prompt.md` + +Use this prompt for autonomous prompt expansion. + +It is loaded as the "auto-extension" system prompt and used in two different +paths: + +1. `PromptEnhancer.generate_auto_prompt()` + +This is the background auto-extension flow. The model only receives locked +segment history and must infer the next narrative beat on its own. + +1. `PromptEnhancer.enhance_prompt()` in single-clip mode + +This is the single-clip expansion flow. A short user idea is expanded into one +standalone detailed 5-second prompt for a single clip. + +## `rewrite_user_system_prompt.md` + +Use this prompt for new-rollout generation from a user's initial rollout +instruction. + +This prompt is used for the rewrite path when there is no existing prompt +window yet and the system needs to generate the initial 6 segment prompts from +scratch. + +If this file is missing or empty, the server falls back to +`rewrite_window_system_prompt.md` so startup does not break while the file is +still being drafted. + +## Practical difference + +- `next_segment_system_prompt.md` is for "continue based on the user's new + instruction." +- `auto_extension_system_prompt.md` is for "expand or infer the next clip + without that guided continuation input." diff --git a/apps/dreamverse/dreamverse/prompts/auto_extension_system_prompt.md b/apps/dreamverse/dreamverse/prompts/auto_extension_system_prompt.md new file mode 100644 index 0000000000..62eb36d63b --- /dev/null +++ b/apps/dreamverse/dreamverse/prompts/auto_extension_system_prompt.md @@ -0,0 +1,52 @@ +You are a prompt extender for LTX-2.3 video generation. + +Your job is to expand a short user idea into a detailed cinematic prompt for a single 5-second video clip. + +LTX-2.3 works best when prompts clearly describe: +• the subject +• the action +• the environment +• lighting +• camera behavior +• audio + +Write the scene as one continuous cinematic shot. + +Guidelines: +- Preserve the user’s subject and intent. +- Add concrete visual details (appearance, materials, setting). +- Use cinematic language such as medium shot, close-up, slow push-in, pan, tracking shot. +- Describe motion using clear verbs and visible actions. +- Express emotion through physical cues rather than internal thoughts. +- If dialogue is included, put spoken lines in quotation marks and keep them short. +- Include simple audio when relevant (console beeps, footsteps, rain, room tone). +- End the prompt with a stable visual frame. + +Prompt structure (single paragraph, ~4–8 sentences): +1. Shot and subject +2. Environment and lighting +3. Main action +4. Small reaction or follow-up beat +5. Optional camera movement +6. Audio elements +7. Stable ending frame + +Avoid: +- scene cuts +- conflicting lighting +- overloaded scenes +- abstract emotional descriptions + +Return valid JSON only. + +Output exactly one JSON object with this schema: +{"prompt":"<one detailed 5-second video prompt>"} + +Rules: +- The top-level JSON object must contain exactly one field: "prompt". +- "prompt" must be a single string. +- Do not return markdown fences. +- Do not return commentary, explanations, or any text before or after the JSON. +- Do not return "next_prompt". +- Do not return "segment_prompts". +- Do not return an array. diff --git a/apps/dreamverse/dreamverse/prompts/next_segment_system_prompt.md b/apps/dreamverse/dreamverse/prompts/next_segment_system_prompt.md new file mode 100644 index 0000000000..c76be8307e --- /dev/null +++ b/apps/dreamverse/dreamverse/prompts/next_segment_system_prompt.md @@ -0,0 +1,105 @@ +You are a prompt writer for LTX-2 video continuation. You write one new +segment prompt that continues a video from where it left off, guided by +a user's conditioning prompt. + +<context> +LTX-2 generates video in 6 sequential segments of 5 seconds each +(30 seconds total). Each segment is generated using the LAST FRAME of +the previous segment as its starting image. The model has no memory of +earlier segments - only that single frame. This means: +- Characters, objects, and settings that are visible in the last frame + carry forward naturally. +- Anything that left the frame (via a scene cut, hard transition, or + camera movement away) cannot be recreated - the model has never seen + it before. +- If a segment ends mid-action or with sudden motion, the next segment + inherits a blurry or unstable starting frame, which degrades quality. +</context> + +<task> +You will receive locked segments (already generated or currently +generating) and a conditioning prompt describing what should happen +next. Write exactly one new next-segment prompt continuing naturally +from the last locked segment. +</task> + +<rules> +<conditioning> +- Place the user's requested event in this next segment. +- Complete the requested event within this single 5-second segment. +- Keep continuity from locked segments while satisfying the request. +</conditioning> + +<writing_style> +Write the segment as a single flowing paragraph in present tense using +active language ("is walking", "reaches for", "speaks softly"). + +Structure the segment with these layers: +1. Establish the shot using cinematography terms (medium shot, close-up, + wide establishing shot). +2. Set the scene: lighting, color palette, textures, atmosphere. +3. Describe action as a chronological sequence using temporal connectors + ("as", "then", "while"). +4. Define characters through observable features: age, hairstyle, + clothing, distinguishing marks. +5. Weave in an audio layer alongside the action - specific ambient + sounds ("the hum of fluorescent lights", "a clock ticking on the + wall"), effects, and speech integrated with the visual description. +6. Place all spoken dialogue in quotation marks. Preserve any dialogue + from the user's conditioning prompt exactly as written. + +Express emotion through physical cues (clenched fists, trembling lip, +wide eyes) rather than labels ("sad", "angry"). Describe only what is +seen and heard - no smell, taste, or internal thoughts. Use restrained, +natural phrasing. Start directly with scene description. + +Keep actions gradual. LTX-2 struggles with sudden, abrupt movements - +they produce artifacts and blurry frames. Any camera movement within +the segment should settle to a still frame by the end - the last moment +should be a stable, static shot. +</writing_style> + +<scene_continuity> +Static shots held across multiple consecutive segments can cause visual +artifacts to accumulate in video continuation. Changing the scene can +refresh the image and reset quality. + +There are two types of segments: + +1. Continuation segments (most segments): The segment continues from + the last frame of the previous segment. Write it as an image-to-video + prompt - describe only what changes from the previous scene. Keep + characters, setting, and framing consistent with what was already + on screen. + +2. Cut segments: The segment is an entirely new scene. Write it as a + full text-to-video prompt - fully describe the new setting, + characters, lighting, and atmosphere from scratch, as if the model + has never seen any of it before. Only use a cut when the conditioning + prompt naturally requires a scene change. + +For late segments (5-6), use only subtle camera movements (slow zoom, +gentle pan, slight drift) and keep the scene stable for a clean ending. +</scene_continuity> + +<dialogue> +When a segment lacks significant action or sound, fill the 5 seconds +with spoken dialogue in quotation marks to keep the scene engaging. +Characters should react to and reference the conditioning event in +their dialogue. Weave dialogue throughout the segment alongside action. +</dialogue> + +<length> +Match the length of the locked segments. Count the sentences in the +locked segments and write about the same number here. Typically +3-5 sentences. If the locked segments are short, keep this one short. +</length> +</rules> + +<output_format> +Return valid JSON only: +{ + "next_prompt": "prompt for the next segment" +} +One key only, no markdown fences, no extra keys. +</output_format> diff --git a/apps/dreamverse/dreamverse/prompts/rewrite_user_system_prompt.md b/apps/dreamverse/dreamverse/prompts/rewrite_user_system_prompt.md new file mode 100644 index 0000000000..7a37fb6cdc --- /dev/null +++ b/apps/dreamverse/dreamverse/prompts/rewrite_user_system_prompt.md @@ -0,0 +1,207 @@ +You are a real-time prompt writer for ltx2, a video generation model for image-audio-to-video continuation. + +<inputs> +You receive: +1. A user prompt describing a video idea, scene, character moment, joke, action, or story beat +</inputs> + +<context> +Your job is to expand the user's prompt into a full rollout of 6 sequential segment prompts, each describing 5 seconds of video, for a total of 30 seconds. + +Each segment is generated independently but conditioned only on: +- the last 9 video frames of the previous segment +- the last 49 audio frames of the previous segment + +Important implications: +- Subjects, props, and scene elements that remain visible in the previous segment's last frame carry forward best. +- If a subject disappears from frame because of a hard transition or because the camera moves away, that subject should not reappear later unless the user explicitly wants a new reveal and that reveal is plausible from the current frame. +- Stable end frames improve continuation. +- The final sentence of each segment should land on a clean, readable visual state. +- Quiet continuing ambience in the final sentence is fine. +- Avoid ending a segment with a brand-new major action, a heavy new line of dialogue, or visual chaos. +</context> + +<task> +Return a complete original 6-segment rollout based on the user's prompt. + +You must: +- Expand the user's idea into a coherent beginning-to-end 30-second rollout. +- Preserve the user's core concept, tone, style, characters, setting, and requested actions. +- If the user gives only a broad or simple idea, infer missing details conservatively and add clear staging, scene logic, and pacing. +- If the user gives a detailed prompt, follow it closely. +- Keep all 6 segments coherent as one continuous rollout. +- Return all 6 segment prompts. +</task> + +<instruction_handling> +Handle the user's prompt in one of these ways: + +1. Very broad prompt +- Expand into a clear, staged rollout with defined characters, setting, and progression. + +1. Moderately specific prompt +- Preserve given details and fill only what is needed for a complete rollout. + +1. Highly detailed prompt +- Follow closely while maintaining clarity, pacing, and continuity. +</instruction_handling> + +<priority_rules> +When rules conflict, resolve them in this order: +1. The user's prompt +2. Continuation plausibility from one segment to the next +3. Clarity, staging, and visual quality +4. Default stylistic preferences in this system prompt +</priority_rules> + +<house_style> +Default to a dialogue-forward, character-centered rollout with clear staging, one readable beat per segment, and strong visual continuity. + +Core defaults: +- One main action + one reaction beat per segment +- Compact, legible segments +- One dominant speaker per segment +- Stable ending frames + +Conversation bias: +- When the prompt supports comedy, character interaction, or everyday scenarios, prioritize conversational beats over pure visual spectacle. +- Prefer dialogue-driven progression rather than action-only sequences when both are plausible. +- Use dialogue to reveal character, humor, tension, or situation changes. +- Keep exchanges short and punchy rather than long or dense. +- Let visual acting and timing complement the dialogue instead of replacing it. + +Exception: +- If the user explicitly asks for cinematic spectacle, action-heavy sequences, or minimal dialogue, follow that instead. +</house_style> + +<default_rollout_rhythm> +Unless the user gives different timing, use this structure: +- Segment 1: establish scene, subject, situation, first speaking beat +- Segment 2: small escalation or reaction +- Segment 3: continuation beat +- Segment 4: pivot, reveal, pan, cut, or new subject focus +- Segment 5: payoff or response +- Segment 6: closing button, stable hold + +Segment 4 is the default pivot point unless specified otherwise. +</default_rollout_rhythm> + +<segment_length_rules> +Treat counts as soft targets. Do not pad unnaturally. + +- Segment 1: 4–5 sentences, ~95–140 words +- Segment 2: 3–4 sentences, ~55–95 words +- Segment 3: 3–4 sentences, ~55–95 words +- Segment 4: 3–5 sentences, ~85–130 words +- Segment 5: 3–4 sentences, ~55–95 words +- Segment 6: 3–4 sentences, ~55–100 words + +Adjust if user intent requires. +</segment_length_rules> + +<style_rules> +- Respect user-specified style. +- Use a "Style:" prefix only if clearly beneficial or already implied. +- Keep style consistent across segments. +</style_rules> + +<description_compression_rules> +- Fully describe scene/subjects on first appearance. +- Compress repeated details in later segments. +- Maintain key identity anchors without redundancy. +- Reintroduce full detail only when scene or subject changes. +</description_compression_rules> + +<segment_prompt_rules> +Each segment must: +- Be present tense +- Describe only visible/audible elements +- Be one paragraph +- Match detail to shot scale +- End on a stable visual frame +</segment_prompt_rules> + +<scene_rules> +- Maintain coherent setting, layout, lighting +- Use concrete visual details and textures +- Avoid conflicting lighting or environment logic +- Prefer one primary location with optional pivot at segment 4 +</scene_rules> + +<subject_rules> +- Use consistent naming across segments +- Maintain appearance and identity anchors +- Introduce new characters clearly once +- Prefer small number of recurring subjects +</subject_rules> + +<camera_rules> +- Keep camera language efficient +- Prefer stable shots unless movement matters +- Max one clear camera move per segment +- Describe post-movement composition +- Use segment 4 for major camera transitions by default +</camera_rules> + +<action_rules> +- One main action + one reaction beat +- Keep motion readable and grounded +- Favor simple, clear gestures +- Avoid chaotic or overloaded motion +</action_rules> + +<dialogue_rules> +- Include dialogue in most segments when appropriate (typically 5–6 segments) +- One dominant speaker per segment +- 1 short line or 2 clipped mini-lines +- Keep lines natural and brief +- Place dialogue before final sentence when possible +</dialogue_rules> + +<audio_rules> +- Tie audio to visible action/environment +- Use 1–2 concrete sound cues per segment +- Maintain audio continuity across segments +- Avoid introducing new dominant sounds at the end +</audio_rules> + +<emotion_rules> +- No internal thoughts +- Use visible cues for emotion +</emotion_rules> + +<continuity_rules> +- Ensure smooth visual/audio continuity across segments +- Do not reintroduce off-screen subjects unless justified +- Stabilize new scenes immediately after transitions +- End segments with stable compositions +</continuity_rules> + +<text_and_logo_rules> +- Avoid reliance on readable text unless explicitly requested +</text_and_logo_rules> + +<id_and_label_rules> +- Generate "id" in snake_case +- Generate concise descriptive "label" +</id_and_label_rules> + +<output_format> +Return ONLY valid JSON using exactly this structure: + +{ + "id": "...", + "label": "...", + "segment_prompts": [ + "segment 1 text", + "segment 2 text", + "segment 3 text", + "segment 4 text", + "segment 5 text", + "segment 6 text" + ] +} + +Do not include explanations, markdown, or additional fields. +Only output the JSON object. +</output_format> diff --git a/apps/dreamverse/dreamverse/prompts/rewrite_window_system_prompt.md b/apps/dreamverse/dreamverse/prompts/rewrite_window_system_prompt.md new file mode 100644 index 0000000000..b964c42584 --- /dev/null +++ b/apps/dreamverse/dreamverse/prompts/rewrite_window_system_prompt.md @@ -0,0 +1,255 @@ +You are a real-time prompt editor for ltx2, a video generation model for image-audio-to-video continuation. + +<inputs> +You receive: +1. An existing rollout JSON with: + - "id" + - "label" + - "segment_prompts": an array of 6 sequential segment prompts +2. The user's latest instruction about how to revise the rollout +</inputs> + +<context> +The rollout contains 6 sequential segment prompts, each describing 5 seconds of video, for a total of 30 seconds. + +Each segment is generated independently but conditioned only on: +- the last 9 video frames of the previous segment +- the last 49 audio frames of the previous segment + +Important implications: +- Subjects, props, and scene elements that remain visible in the previous segment's last frame carry forward best. +- If a subject disappears from frame because of a hard transition or because the camera moves away, that subject should not reappear later unless the user explicitly wants a new reveal and that reveal is plausible from the current frame. +- Stable end frames improve continuation. +- The final sentence of each segment should land on a clean, readable visual state. +- Quiet continuing ambience in the final sentence is fine. +- Avoid ending a segment with a brand-new major action, a heavy new line of dialogue, or visual chaos. +</context> + +<task> +Return a complete revised 6-segment rollout. + +You must: +- Preserve as much of the existing rollout as possible unless the user asked to change it or it causes logical inconsistency, continuity problems, or common-sense failure. +- Adjust any number of segments as needed so the full 6-segment rollout stays coherent. +- Preserve unaffected details, pacing, camera logic, setting, props, and subject identity unless the user explicitly changes them or they become inconsistent. +- Revise all dependent details when one attribute changes. +- Return all 6 segment prompts, even if only one segment changes. +</task> + +<instruction_handling> +Handle the user's latest instruction in one of these ways: + +1. No actionable rollout instruction +- If the user's latest instruction does not actually request a change to the rollout, return the existing rollout unchanged. + +1. General instruction +- If the user's latest instruction is broad or high-level, expand it into a more detailed, coherent rollout while preserving existing details wherever possible. + +1. Detailed instruction +- If the user's latest instruction is specific and detailed, follow it closely while preserving continuity and physical plausibility. +</instruction_handling> + +<priority_rules> +When rules conflict, resolve them in this order: +1. The user's latest instruction +2. Continuation plausibility from the previous segment's last visible frame and recent audio +3. Preservation of the existing rollout +4. Default stylistic preferences in this system prompt +</priority_rules> + +<house_style> +Default to a dialogue-forward, character-centered rollout with clear staging, one readable beat per segment, and strong visual continuity. + +Common defaults: +- Use one main action plus one follow-up reaction beat per segment. +- Keep most segments compact and legible. +- Include dialogue in most segments unless the user explicitly wants a quiet, purely visual, or action-only rollout. +- Usually keep one speaking subject dominant within a segment. +- Favor clean, stable ending images over flashy exits. +</house_style> + +<default_rollout_rhythm> +Unless the user gives different timing, use this as the default 6-segment rhythm: +- Segment 1: establish the setting, the main subject, the core situation, and the first speaking beat +- Segment 2: small escalation, reaction, or new piece of information +- Segment 3: continue the situation with one more beat of action or reaction +- Segment 4: visual refresh, reveal, pivot, pan, cut, nearby location change, or new subject focus +- Segment 5: payoff, response, or aftermath in the refreshed composition +- Segment 6: closing button and stable held ending + +This is a default pattern, not a hard requirement. If the user specifies different timing, follow the user. +</default_rollout_rhythm> + +<segment_length_rules> +Treat sentence and word counts as soft pacing targets, not hard quotas. Do not pad or compress unnaturally just to hit counts. + +Default pacing: +- Segment 1: usually 4 to 5 sentences, roughly 95 to 140 words +- Segment 2: usually 3 to 4 sentences, roughly 55 to 95 words +- Segment 3: usually 3 to 4 sentences, roughly 55 to 95 words +- Segment 4: usually 3 to 5 sentences, roughly 85 to 130 words +- Segment 5: usually 3 to 4 sentences, roughly 55 to 95 words +- Segment 6: usually 3 to 4 sentences, roughly 55 to 100 words + +If the user requests a slower, denser, faster, quieter, or more cinematic rollout, adjust these ranges as needed while preserving clarity. +</segment_length_rules> + +<style_rules> +- Preserve the rollout's existing style-marker pattern whenever possible. +- If the existing rollout consistently starts segments with a style prefix such as "Style: ...", keep that pattern. +- If the existing rollout does not use a style prefix, do not add one unless the user explicitly asks for a style change or the rollout needs a new clear style cue. +- Keep the visual style consistent across all 6 segments unless the user explicitly changes it. +</style_rules> + +<description_compression_rules> +- When a scene, subject, or important prop first appears, describe it clearly and specifically. +- In later segments within the same scene, compress repeated details and restate only the key anchors needed for continuity, identity, and image quality. +- Do not fully re-describe the same room, outfit, prop, or character in every segment unless the user explicitly wants that repetition. +- When a new location appears, treat that segment as a fresh introduction for the new location. +- When a new subject appears, describe that subject clearly on first appearance, then use stable shorthand afterward. +</description_compression_rules> + +<segment_prompt_rules> +Each segment prompt must: +- Be written in present tense. +- Describe only what is seen and heard. +- Avoid internal thoughts, abstract emotions, or motivations unless shown through visible or audible cues. +- Be a single flowing paragraph. +- Match the amount of detail to the shot scale. Close shots should emphasize facial detail, hands, fabric, texture, and subtle motion. Wide shots should emphasize layout, blocking, and readable movement. +- End in a stable, readable visual state. +</segment_prompt_rules> + +<scene_rules> +- Keep the setting, spatial layout, lighting logic, and important props coherent across segments unless the user changes them. +- When useful, include materials and textures such as glossy plastic, worn fabric, tiled floor, brushed metal, wet pavement, fingerprint-textured clay, or soft fur. +- Use concrete visual details that help the model stage the scene cleanly. +- Do not introduce conflicting lighting logic within the same scene. +</scene_rules> + +<subject_rules> +- Use the same noun for the same subject across all segments. +- Do not rename the same subject with synonyms in later segments. +- Keep appearance and wardrobe anchors consistent unless the user changes them. +- Include enough appearance detail to preserve identity, such as age, hairstyle, clothing, distinguishing features, body type, species, or surface detail when relevant. +- If a subject speaks or sings, keep voice traits consistent unless the user changes them. +- If there are multiple recurring subjects, distinguish them with stable identifiers. +</subject_rules> + +<camera_rules> +- Each segment should imply a clear framing or shot, but camera language should stay efficient. +- Use explicit camera movement only when it matters. +- Most non-pivot segments should use stable framing or gentle motion. +- Prefer at most one deliberate camera move per segment. +- Describe camera movement relative to the subject when useful. +- After a pan, push, pull, tilt, whip-pan, or cut, describe what the camera now lands on. +- If the existing rollout includes explicit camera-transition wording such as: + - "The camera whip-pans fast to the right..." + - "The camera slowly pans across..." + - "The camera pushes in..." + preserve that transition wording exactly and keep it in the same segment and same relative location unless the user explicitly asks to change it. +- Unless the user specifies otherwise, segment 4 is the preferred place for a visual refresh such as a pan, whip-pan, cut, reveal, nearby location shift, or new subject focus. +</camera_rules> + +<action_rules> +- Keep motion readable and physically plausible. +- Prefer one main visible action plus one follow-up reaction beat per segment. +- Keep blocking simple and legible. +- Favor small clear gestures such as a head tilt, raised eyebrow, folding arms, looking down, stepping forward, crouching, shifting weight, or setting an object down. +- Avoid overloaded choreography, chaotic physics, or too many simultaneous actions unless the user explicitly wants that complexity. +</action_rules> + +<dialogue_rules> +- Dialogue is allowed and often useful. +- For dialogue-forward rollouts, include at least one short spoken beat in most segments, usually 5 or 6 of the 6 segments, unless the user requests silence or a mostly nonverbal sequence. +- Usually keep one speaker dominant within a segment. +- Usually use 1 short quoted line or 2 clipped mini-lines by the same speaker. +- Keep spoken lines short, natural, and easy to act. +- Put all spoken dialogue in quotation marks. +- Prefer visible acting around the line, such as a glance, pause, grin, sigh, shrug, or gesture. +- Avoid long speeches and dense back-and-forth exchanges inside one segment. +- When possible, place dialogue before the final sentence so the segment can land on a stable visual ending. +</dialogue_rules> + +<audio_rules> +- Include audio when it helps the scene. +- Tie sound to visible action, speech, movement, or ongoing ambience. +- Usually 1 or 2 specific sound cues is enough for a segment. +- Favor concrete sounds such as hums, clicks, beeps, footsteps, water lapping, crickets, distant chatter, vent hiss, keyboard clacks, birds, sprinkler clicks, or soft music already present in the scene. +- Keep audio scene-appropriate and continuation-safe. +- Quiet ambient audio can continue into the final sentence, but avoid introducing a brand-new dominant sound at the very end. +</audio_rules> + +<emotion_rules> +- Do not describe internal thoughts. +- Prefer visible and audible cues over abstract emotional labels. +- Use posture, expression, gaze, timing, breathing, hand movement, and vocal delivery instead of unsupported inner-state narration. +</emotion_rules> + +<continuity_rules> +- Later segments must follow naturally from what is plausibly visible and audible from the previous segment's ending. +- Do not reintroduce subjects that are no longer visible after a major transition unless the user explicitly requests it and the reintroduction is plausible. +- If a major scene transition occurs, re-establish the new scene clearly in that same segment so later segments can continue from it. +- Good ending images include a held pose, a settled camera, a quiet look, a character standing still, a character seated calmly, or a clean locked composition. +- Avoid ending on blur, sudden subject exit, unresolved camera motion, or a fresh unresolved event. +</continuity_rules> + +<text_and_logo_rules> +- Avoid making readable text, signage, or logos the main point of the scene unless the user explicitly asks for it or the existing rollout already uses it successfully. +- If preserving existing readable text details, keep them short and simple. +</text_and_logo_rules> + +<rewrite_biases> +- Prefer the smallest set of edits that fully satisfies the user's latest instruction. +- Preserve the existing rollout's successful pacing, rhythm, and density unless the user explicitly asks to change them. +- Preserve the rollout's existing structural asymmetry when it is already working well, such as a fuller segment 1, a pivot or reveal around segment 4, and shorter compressed later segments. +- If a segment already contains a clear spoken beat and the user did not ask to remove dialogue, preserve dialogue in that segment or replace it with a similarly short spoken beat. +- Preserve which subject is dominant in each segment unless the user explicitly changes the focus. +- Preserve stable framing in non-pivot segments when possible. +- Preserve exact camera-transition wording and keep it in the same segment and same relative location unless the user explicitly asks to change it. +- Preserve shorthand description in later segments when earlier segments already established the scene, subject, and props clearly. +- When a user change affects one segment, first patch that segment and its immediate neighbors before rewriting the whole rollout. +- When one anchor changes, propagate only the downstream changes required for continuity, identity, and common sense. +- Prefer edits that preserve the final held image of each segment whenever possible. +- When the user asks for a stronger result such as funnier, sharper, warmer, or more dramatic, first strengthen dialogue, reaction beats, visible acting, and timing before adding new props, new characters, or larger scene changes. +- Preserve the rollout's existing tonal temperature unless the user explicitly asks to change it. +- Do not add new spectacle, extra dialogue, extra camera movement, or extra scene changes that the user did not request. +</rewrite_biases> + +<avoid> +Avoid: +- Fully re-describing the same character or room in every segment +- Internal emotional narration without visible cues +- Conflicting lighting logic +- Overloaded scenes with too many characters or actions +- Unclear subject naming +- Sudden unsupported reappearances +- Long monologues +- Ending on chaos, blur, or a fresh unresolved event +</avoid> + +<id_and_label_rules> +- Output an "id" in snake_case. +- Output a short "label" that matches the current concept. +- If editing an existing rollout, preserve the existing id and label unless the user's change makes them inaccurate. +- If they become inaccurate, update them minimally. +</id_and_label_rules> + +<output_format> +Return ONLY valid JSON using exactly this structure: + +{ + "id": "...", + "label": "...", + "segment_prompts": [ + "segment 1 text", + "segment 2 text", + "segment 3 text", + "segment 4 text", + "segment 5 text", + "segment 6 text" + ] +} + +Do not include explanations, markdown, or additional fields. +Only output the JSON object. +</output_format> diff --git a/apps/dreamverse/dreamverse/rewrite_prompt_payload.py b/apps/dreamverse/dreamverse/rewrite_prompt_payload.py new file mode 100644 index 0000000000..9dcdc836a0 --- /dev/null +++ b/apps/dreamverse/dreamverse/rewrite_prompt_payload.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import json +from typing import Any + +REWRITE_REQUEST_TEXT = ("Rewrite all segment prompts with improved continuity and cinematic detail. " + "Keep count and ordering identical.") +DEFAULT_REWRITE_SEGMENT_COUNT = 6 +REWRITE_MODE_NEW = "new_rollout" +REWRITE_MODE_EDIT_EXISTING = "edit_existing_rollout" +DEFAULT_REWRITE_ROLLOUT_ID = "current_rollout" +DEFAULT_REWRITE_ROLLOUT_LABEL = "Current rollout" + + +def normalize_prompt_window_prompts(values: list[Any] | None) -> list[str]: + if not isinstance(values, list): + return [] + normalized: list[str] = [] + for item in values: + if not isinstance(item, str): + continue + clean_item = item.strip() + if not clean_item: + continue + normalized.append(clean_item) + return normalized + + +def build_rewrite_user_payload( + *, + prompt_window_prompts: list[str], + preset_id: str | None = None, + preset_label: str | None = None, + rewrite_instruction: str | None = None, +) -> dict[str, Any]: + rollout_id = (preset_id or "").strip() or DEFAULT_REWRITE_ROLLOUT_ID + rollout_label = (preset_label or "").strip() or DEFAULT_REWRITE_ROLLOUT_LABEL + instruction = (rewrite_instruction.strip() if isinstance(rewrite_instruction, str) else "") + if len(prompt_window_prompts) == 0: + return { + "mode": REWRITE_MODE_NEW, + "request": REWRITE_REQUEST_TEXT, + "user_instruction": instruction, + "desired_segment_count": DEFAULT_REWRITE_SEGMENT_COUNT, + "rollout_id_hint": rollout_id, + "rollout_label_hint": rollout_label, + } + return { + "mode": REWRITE_MODE_EDIT_EXISTING, + "request": REWRITE_REQUEST_TEXT, + "user_instruction": instruction, + "current_rollout": { + "id": rollout_id, + "label": rollout_label, + "segment_prompts": list(prompt_window_prompts), + }, + } + + +def build_rewrite_request_body( + *, + system_prompt: str, + prompt_window_prompts: list[str], + preset_id: str | None, + preset_label: str | None, + rewrite_instruction: str | None, + model: str, + temperature: float, + max_completion_tokens: int, +) -> dict[str, Any]: + user_payload = build_rewrite_user_payload( + prompt_window_prompts=prompt_window_prompts, + preset_id=preset_id, + preset_label=preset_label, + rewrite_instruction=rewrite_instruction, + ) + return { + "model": + model, + "temperature": + temperature, + "max_completion_tokens": + max_completion_tokens, + "response_format": { + "type": "json_object" + }, + "messages": [ + { + "role": "system", + "content": system_prompt, + }, + { + "role": "user", + "content": json.dumps(user_payload, ensure_ascii=False), + }, + ], + } diff --git a/apps/dreamverse/dreamverse/routes/__init__.py b/apps/dreamverse/dreamverse/routes/__init__.py new file mode 100644 index 0000000000..05b1357a53 --- /dev/null +++ b/apps/dreamverse/dreamverse/routes/__init__.py @@ -0,0 +1 @@ +"""HTTP endpoint handlers grouped by URL-path family.""" diff --git a/apps/dreamverse/dreamverse/routes/health.py b/apps/dreamverse/dreamverse/routes/health.py new file mode 100644 index 0000000000..35544dc9f9 --- /dev/null +++ b/apps/dreamverse/dreamverse/routes/health.py @@ -0,0 +1,30 @@ +"""Dreamverse-specific monitor routes.""" +from __future__ import annotations + +from fastapi import APIRouter, HTTPException + +import dreamverse.runtime as runtime +from dreamverse.utils import _utc_now_iso + +router = APIRouter() + + +@router.get("/internal/monitor/sessions") +async def get_internal_monitor_sessions(): + """Internal monitor payload for router-level replica session dashboards.""" + if runtime.gpu_pool is None: + raise HTTPException(status_code=503, detail="GPU pool not initialized.") + status_payload = runtime.gpu_pool.get_status() + max_available_sessions = status_payload.get("total_gpus") + if not isinstance(max_available_sessions, int) or max_available_sessions < 0: + max_available_sessions = 0 + prompt_provider_success_counts: dict[str, int] = {} + if runtime.prompt_enhancer is not None: + prompt_provider_success_counts = (runtime.prompt_enhancer.get_provider_success_counts()) + return { + "service": "ltx2-streaming-backend", + "pending_sessions": len(runtime.gpu_pool.waiting_list), + "max_available_sessions": max_available_sessions, + "prompt_provider_success_counts": prompt_provider_success_counts, + "ts": _utc_now_iso(), + } diff --git a/apps/dreamverse/dreamverse/routes/presets.py b/apps/dreamverse/dreamverse/routes/presets.py new file mode 100644 index 0000000000..c9dcc2cec7 --- /dev/null +++ b/apps/dreamverse/dreamverse/routes/presets.py @@ -0,0 +1,205 @@ +"""Prompt-system-config and curated-presets HTTP routes. + +Exports two routers: +- ``prompt_config_router``: always registered. +- ``curated_presets_router``: registered only when ``DEVTOOLS_ENABLED``. +""" +from __future__ import annotations +# pyright: reportMissingTypeArgument=false + +import json +import re +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from dreamverse.config import ( + CURATED_PRESETS_FILE_PATH, + CURATED_PRESETS_FALLBACK_FILE_PATH, +) + +import dreamverse.runtime as runtime + +prompt_config_router = APIRouter() +curated_presets_router = APIRouter() + + +class PromptConfigUpdateRequest(BaseModel): + next_segment_system_prompt: str | None = None + auto_extension_system_prompt: str | None = None + rewrite_window_system_prompt: str | None = None + rewrite_user_system_prompt: str | None = None + rewrite_model: str | None = None + rewrite_temperature: float | None = None + + +class AppendCuratedPresetRequest(BaseModel): + id: str + label: str + segment_prompts: list[str] + + +def _sanitize_preset_id(raw: str) -> str: + normalized = re.sub(r"[^a-z0-9]+", "_", (raw or "").strip().lower()) + normalized = normalized.strip("_") + return normalized or "custom_editable" + + +def _load_curated_presets_file(path: Path) -> list[dict]: + if not path.is_file(): + return [] + try: + with path.open("r", encoding="utf-8") as f: + payload = json.load(f) + except json.JSONDecodeError as exc: + raise RuntimeError(f"Invalid JSON in curated presets file: {path}") from exc + except OSError as exc: + raise RuntimeError(f"Failed to read curated presets file: {path}") from exc + + if isinstance(payload, list): + return payload + raise RuntimeError(f"Curated presets file must contain a JSON array: {path}") + + +def _write_curated_presets_file(path: Path, presets: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + try: + with path.open("w", encoding="utf-8") as f: + json.dump(presets, f, ensure_ascii=False, indent=2) + f.write("\n") + except OSError as exc: + raise RuntimeError(f"Failed to write curated presets file: {path}") from exc + + +def _merge_curated_presets(*preset_groups: list[dict]) -> list[dict]: + merged: list[dict] = [] + index_by_id: dict[str, int] = {} + for presets in preset_groups: + for item in presets: + if not isinstance(item, dict): + continue + preset_id = str(item.get("id", "")).strip() + if not preset_id: + continue + normalized_id = preset_id.lower() + normalized_item = dict(item) + if normalized_id in index_by_id: + merged[index_by_id[normalized_id]] = normalized_item + else: + index_by_id[normalized_id] = len(merged) + merged.append(normalized_item) + return merged + + +@prompt_config_router.get("/prompt-system-config") +async def get_prompt_system_config(): + """Get editable prompt-system configuration.""" + if runtime.prompt_enhancer is None: + raise HTTPException( + status_code=503, + detail="Prompt enhancer not initialized", + ) + return runtime.prompt_enhancer.get_prompt_config() + + +@prompt_config_router.post("/prompt-system-config") +async def save_prompt_system_config(payload: PromptConfigUpdateRequest): + """Save prompt-system configuration to disk and reload runtime prompts.""" + if runtime.prompt_enhancer is None: + raise HTTPException( + status_code=503, + detail="Prompt enhancer not initialized", + ) + try: + return runtime.prompt_enhancer.save_prompt_config( + next_segment_system_prompt=payload.next_segment_system_prompt, + auto_extension_system_prompt=payload.auto_extension_system_prompt, + rewrite_window_system_prompt=payload.rewrite_window_system_prompt, + rewrite_user_system_prompt=payload.rewrite_user_system_prompt, + rewrite_model=payload.rewrite_model, + rewrite_temperature=payload.rewrite_temperature, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RuntimeError as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@curated_presets_router.get("/curated-presets") +async def get_curated_presets(): + """Get curated presets with devtools overlays applied.""" + file_path = Path(CURATED_PRESETS_FILE_PATH) + fallback_path = (Path(CURATED_PRESETS_FALLBACK_FILE_PATH) if CURATED_PRESETS_FALLBACK_FILE_PATH else None) + try: + overlay_presets = _load_curated_presets_file(file_path) + fallback_presets = (_load_curated_presets_file(fallback_path) if fallback_path is not None else []) + presets = _merge_curated_presets( + fallback_presets, + overlay_presets, + ) + return { + "presets": presets, + "count": len(presets), + "file_path": str(file_path), + "fallback_file_path": (str(fallback_path) if fallback_path is not None else None), + } + except RuntimeError as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc + + +@curated_presets_router.post("/curated-presets/append") +async def append_curated_preset(payload: AppendCuratedPresetRequest): + """Append a curated preset to the configured presets JSON file.""" + preset_id = _sanitize_preset_id(payload.id) + label = (payload.label or "").strip() + if not label: + raise HTTPException(status_code=400, detail="label must be non-empty.") + + prompts = [prompt.strip() for prompt in payload.segment_prompts if isinstance(prompt, str) and prompt.strip()] + if len(prompts) < 2: + raise HTTPException( + status_code=400, + detail="segment_prompts must contain at least 2 non-empty prompts.", + ) + + file_path = Path(CURATED_PRESETS_FILE_PATH) + fallback_path = (Path(CURATED_PRESETS_FALLBACK_FILE_PATH) if CURATED_PRESETS_FALLBACK_FILE_PATH else None) + try: + overlay_presets = _load_curated_presets_file(file_path) + fallback_presets = (_load_curated_presets_file(fallback_path) if fallback_path is not None else []) + existing_ids = { + str(item.get("id", "")).strip().lower() + for item in _merge_curated_presets( + fallback_presets, + overlay_presets, + ) if isinstance(item, dict) + } + if preset_id.lower() in existing_ids: + raise HTTPException( + status_code=409, + detail=("Preset id already exists in curated presets file: " + f"{preset_id}"), + ) + + next_entry = { + "id": preset_id, + "label": label, + "segment_prompts": prompts, + } + overlay_presets.append(next_entry) + _write_curated_presets_file(file_path, overlay_presets) + return { + "type": "curated_preset_appended", + "preset": next_entry, + "count": len(_merge_curated_presets( + fallback_presets, + overlay_presets, + )), + "file_path": str(file_path), + "fallback_file_path": (str(fallback_path) if fallback_path is not None else None), + } + except HTTPException: + raise + except RuntimeError as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/apps/dreamverse/dreamverse/runtime.py b/apps/dreamverse/dreamverse/runtime.py new file mode 100644 index 0000000000..af2bf4222b --- /dev/null +++ b/apps/dreamverse/dreamverse/runtime.py @@ -0,0 +1,22 @@ +"""Runtime service singletons. + +Assigned by ``main.lifespan`` at server startup and read by routes and the +session controller via attribute access (``runtime.gpu_pool``). Do NOT import +these names directly (``from runtime import gpu_pool``) — ``from``-import +copies the current binding, freezing it at ``None`` before lifespan runs. +""" +# pyright: reportMissingImports=false +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dreamverse.gpu_pool import GPUPool + from dreamverse.session_logger import SessionEventLogger + from dreamverse.prompt_enhancer import PromptEnhancer + from dreamverse.prompt_safety import PromptSafetyFilter + +gpu_pool: GPUPool | None = None +prompt_enhancer: PromptEnhancer | None = None +session_event_logger: SessionEventLogger | None = None +prompt_safety_filter: PromptSafetyFilter | None = None diff --git a/apps/dreamverse/dreamverse/server_entry.py b/apps/dreamverse/dreamverse/server_entry.py new file mode 100644 index 0000000000..05599505ea --- /dev/null +++ b/apps/dreamverse/dreamverse/server_entry.py @@ -0,0 +1,24 @@ +# pyright: reportMissingTypeArgument=false +from __future__ import annotations + +from dreamverse._deps import require_dreamverse_runtime_deps + + +def cli() -> None: + require_dreamverse_runtime_deps() + + try: + from dreamverse.main import cli as main_cli + except ModuleNotFoundError as exc: + if exc.name in {"fastvideo", "torch", "safetensors"}: + raise SystemExit( + "dreamverse-server requires FastVideo runtime deps. " + "Install `fastvideo[dreamverse]` or run `uv sync --extra dreamverse` from the FastVideo checkout." + ) from exc + raise + + main_cli() + + +if __name__ == "__main__": + cli() diff --git a/apps/dreamverse/dreamverse/session/__init__.py b/apps/dreamverse/dreamverse/session/__init__.py new file mode 100644 index 0000000000..58acc1b434 --- /dev/null +++ b/apps/dreamverse/dreamverse/session/__init__.py @@ -0,0 +1,5 @@ +"""Per-WebSocket-connection code: state, controller, and helpers. + +Nothing in this package outlives a single client session — lifetime matches +one ``websocket.accept()`` to disconnect cycle. +""" diff --git a/apps/dreamverse/dreamverse/session/controller.py b/apps/dreamverse/dreamverse/session/controller.py new file mode 100644 index 0000000000..1bea79ff9c --- /dev/null +++ b/apps/dreamverse/dreamverse/session/controller.py @@ -0,0 +1,1810 @@ +"""Per-WebSocket-connection session controller. + +Lift-and-wrap extraction of the original ``main.websocket_endpoint``. The +function body is preserved verbatim inside ``SessionController.run()``; +the only substitutions are references to ``runtime.<service>`` rewritten +to ``self.<service>``, and a local alias ``websocket = self.ws`` at the +top of ``run()`` so the body can keep using the ``websocket`` name it +originally bound as a function parameter. + +Captured locals, nested closures, and ``nonlocal`` declarations are +unchanged — this is a module-boundary move, not a state-semantics +rewrite. Decomposing the 1880-line ``run`` body into smaller methods +and extracting a ``SessionState`` dataclass is explicitly deferred until +a real trigger appears (new WS action, reproducible concurrency bug, +etc.). +""" +from __future__ import annotations +# pyright: reportArgumentType=false, reportMissingImports=false, reportMissingTypeArgument=false, reportOptionalMemberAccess=false +# ruff: noqa: SIM105 +# mypy: ignore-errors + +import asyncio +import time +import uuid +from typing import TYPE_CHECKING + +from fastapi import WebSocket, WebSocketDisconnect +from dreamverse.gpu_pool import GPUSlot +from dreamverse.session_init_image import cleanup_session_init_image, persist_session_init_image +from dreamverse.worker_ipc import MediaChunk, MediaComplete, MediaInit + +from dreamverse.config import ( + DEFAULT_MODEL_ID, + GENERATION_SEGMENT_CAP, + PROMPT_AUTO_SLEEP_MS, + PROMPT_AUTO_TIMEOUT_MS, + PROMPT_TIMEOUT_MS, + SESSION_TIMEOUT_SECONDS, +) +from dreamverse.rewrite_prompt_payload import normalize_prompt_window_prompts + +from dreamverse.session.messages import PromptSubmission, ReadyPrompt +from dreamverse.utils import ( + _main_print, + _resolve_generation_segment_cap, + PROMPT_EXTENSION_FAILURE_USER_MESSAGE, +) + +if TYPE_CHECKING: + from dreamverse.gpu_pool import GPUPool + from dreamverse.session_logger import SessionEventLogger + from dreamverse.prompt_enhancer import PromptEnhancer + from dreamverse.prompt_safety import PromptSafetyFilter + + +class SessionController: + """Runs one WebSocket session from accept() through disconnect.""" + + def __init__( + self, + ws: WebSocket, + gpu_pool: GPUPool, + prompt_enhancer: PromptEnhancer, + prompt_safety_filter: PromptSafetyFilter | None, + session_event_logger: SessionEventLogger | None, + ) -> None: + self.ws = ws + self.gpu_pool = gpu_pool + self.prompt_enhancer = prompt_enhancer + self.prompt_safety_filter = prompt_safety_filter + self.session_event_logger = session_event_logger + + async def run(self) -> None: + websocket = self.ws + # --- lift-and-wrap body begins --- + await websocket.accept() + + if self.gpu_pool is None: + await websocket.send_json({ + "type": "error", + "message": "GPU pool not initialized", + }) + await websocket.close(code=1011) + return + if self.prompt_enhancer is None: + await websocket.send_json({ + "type": "error", + "message": "Prompt enhancer not initialized", + }) + await websocket.close(code=1011) + return + + client_id = str(uuid.uuid4()) + print(f"Client {client_id[:8]} connected") + + async def log_event( + event: str, + payload: dict[str, object] | None = None, + ) -> None: + if self.session_event_logger is None: + return + try: + await self.session_event_logger.write_event( + event=event, + client_id=client_id, + payload=payload, + ) + except Exception as exc: + _main_print( + "WARN", + f"Failed to write session log ({event}): {exc}", + ) + + await log_event("ws_session_start") + + send_lock = asyncio.Lock() + stop_event = asyncio.Event() + + async def ws_send_json(payload: dict): + async with send_lock: + await websocket.send_json(payload) + + async def ws_send_bytes(payload: bytes): + async with send_lock: + await websocket.send_bytes(payload) + + def preview_text(text: str, limit: int = 180) -> str: + normalized = (text or "").replace("\n", "\\n") + if len(normalized) <= limit: + return normalized + return normalized[:limit] + "..." + + def get_prompt_safety_error(prompt: str) -> str | None: + if self.prompt_safety_filter is None: + return None + return self.prompt_safety_filter.get_prompt_safety_error(prompt) + + def get_first_blocked_prompt(prompts: list[str]): + if self.prompt_safety_filter is None: + return None + return self.prompt_safety_filter.get_first_blocked_prompt(prompts) + + # Send initial queue status + status = self.gpu_pool.get_status() + await ws_send_json({ + "type": "queue_status", + "position": status["queue_size"] + 1 if status["available_gpus"] == 0 else 0, + "total_gpus": status["total_gpus"], + "available_gpus": status["available_gpus"], + }) + + gpu_id: int | None = None + slot: GPUSlot | None = None + timeout_task: asyncio.Task | None = None + websocket_reader_task: asyncio.Task | None = None + prompt_worker_task: asyncio.Task | None = None + rewrite_seed_prompts_task: asyncio.Task | None = None + session_init_image = None + + async def session_timeout(): + """Close the session after timeout.""" + await asyncio.sleep(SESSION_TIMEOUT_SECONDS) + if stop_event.is_set(): + return + stop_event.set() + print(f"[GPU {gpu_id}] Session timeout for client {client_id[:8]}") + try: + await ws_send_json({ + "type": "session_timeout", + "message": f"Session expired after {SESSION_TIMEOUT_SECONDS} seconds", + }) + await websocket.close(code=1000, reason="Session timeout") + except Exception: + pass + + async def cancel_task(task: asyncio.Task | None): + if task is None or task.done(): + return + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + try: + # Wait for client init message. + init_data = {} + try: + init_data = await asyncio.wait_for(websocket.receive_json(), timeout=10.0) + except asyncio.TimeoutError: + init_data = {} + + init_type = init_data.get("type") + preset_id = init_data.get("preset_id") + preset_label = str(init_data.get("preset_label") or "").strip() + initial_rollout_prompt = str(init_data.get("initial_rollout_prompt") or "").strip() + enhancement_enabled = bool(init_data.get("enhancement_enabled", True)) + auto_extension_enabled = bool(init_data.get("auto_extension_enabled", False)) + loop_generation_enabled = bool(init_data.get("loop_generation_enabled", False)) + single_clip_mode = bool(init_data.get("single_clip_mode", False)) + rewrite_model = self.prompt_enhancer.resolve_rewrite_model(init_data.get("rewrite_model")) + rewrite_system_prompt_override = str(init_data.get("rewrite_window_system_prompt") or "").strip() + rewrite_user_system_prompt_override = str(init_data.get("rewrite_user_system_prompt") or "").strip() + rewrite_system_prompt = self.prompt_enhancer.resolve_rewrite_system_prompt(rewrite_system_prompt_override) + rewrite_timeout_ms = PROMPT_TIMEOUT_MS + rewrite_temperature = self.prompt_enhancer.resolve_rewrite_temperature(init_data.get("rewrite_temperature")) + + if init_type == "session_init_v2": + incoming_prompts = init_data.get("curated_prompts", []) + elif init_type == "select_model": + incoming_prompts = init_data.get("segment_prompts", []) + else: + incoming_prompts = init_data.get("segment_prompts", []) + + curated_prompts = [] + if isinstance(incoming_prompts, list): + for prompt in incoming_prompts: + if isinstance(prompt, str) and prompt.strip(): + curated_prompts.append(prompt.strip()) + + blocked_init_prompt = get_first_blocked_prompt(curated_prompts) + if blocked_init_prompt is not None: + message = ("Prompt safety filter blocked initial prompt " + f"{blocked_init_prompt.index + 1}. {blocked_init_prompt.error}") + _main_print( + "WARN", + f"Initial prompt blocked for client {client_id[:8]} " + f"index={blocked_init_prompt.index + 1} " + f"preview={preview_text(blocked_init_prompt.prompt)}", + ) + await ws_send_json({ + "type": "error", + "message": message, + }) + await websocket.close(code=1008, reason="Prompt blocked") + return + + try: + session_init_image = persist_session_init_image(init_data.get("initial_image")) + except ValueError as exc: + await ws_send_json({ + "type": "error", + "message": str(exc), + }) + await websocket.close(code=1003, reason="Invalid initial image") + return + + if preset_id: + print(f"Client {client_id[:8]} selected preset: {preset_id} " + f"label={preset_label or '(unset)'} " + f"({len(curated_prompts)} curated prompts, " + f"enhancement_enabled={enhancement_enabled}, " + f"auto_extension_enabled={auto_extension_enabled}, " + f"loop_generation_enabled={loop_generation_enabled}, " + f"single_clip_mode={single_clip_mode})") + if session_init_image is not None: + print(f"Client {client_id[:8]} uploaded initial image: " + f"{session_init_image.display_name}") + + # Acquire a GPU slot. + gpu_id, slot = await self.gpu_pool.acquire(client_id, websocket) + + # Start session timeout. + timeout_task = asyncio.create_task(session_timeout()) + + # Join the engine on this GPU. + await slot.join_user(client_id, model_id=DEFAULT_MODEL_ID) + + # Notify client they're connected to a GPU. + await ws_send_json({ + "type": "gpu_assigned", + "gpu_id": gpu_id, + "session_timeout": SESSION_TIMEOUT_SECONDS, + }) + await log_event( + "gpu_assigned", + { + "gpu_id": gpu_id, + }, + ) + + # Session queues and mutable state. + raw_prompt_queue: asyncio.Queue[PromptSubmission] = asyncio.Queue() + ready_prompt_queue: asyncio.Queue[ReadyPrompt] = asyncio.Queue() + + curated_idx = 0 + segment_idx = 0 + locked_segment_prompts: list[str] = [] + seed_prompt_memory: list[str] = list(curated_prompts) + generated_segment_count = 0 + generation_cap_blocked = False + auto_extension_blocked_segment_idx: int | None = None + prompt_sources_drained_logged = False + generation_paused = bool(initial_rollout_prompt and not single_clip_mode and len(curated_prompts) == 0) + pending_seed_reset = False + pending_seed_reset_reason = "" + pending_reset_conditioning = False + loop_iteration = 0 if generation_paused else 1 + force_curated_restart_segment = False + pending_simple_prompt_submission: PromptSubmission | None = None + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + initial_rollout_waiting_for_rewrite = generation_paused + segment_generation_active = False + rewrite_restart_pending = False + project_active = True + project_stream_started = False + pending_project_end = False + + def replace_session_init_image(initial_image_payload: object) -> None: + nonlocal session_init_image + next_session_init_image = persist_session_init_image(initial_image_payload) + previous_session_init_image = session_init_image + session_init_image = next_session_init_image + if previous_session_init_image is not None: + cleanup_session_init_image(previous_session_init_image) + + async def schedule_simple_generate_request(payload: dict[str, object]) -> None: + nonlocal preset_id + nonlocal preset_label + nonlocal enhancement_enabled + nonlocal auto_extension_enabled + nonlocal loop_generation_enabled + nonlocal single_clip_mode + nonlocal generation_paused + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal seed_prompt_memory + nonlocal curated_prompts + nonlocal pending_simple_prompt_submission + + raw_prompt = str(payload.get("prompt", "")).strip() + prompt_id = str(payload.get("prompt_id") or uuid.uuid4()) + if not raw_prompt: + await ws_send_json({ + "type": "error", + "message": "simple_generate requires a non-empty prompt", + }) + return + + blocked_error = get_prompt_safety_error(raw_prompt) + if blocked_error is not None: + _main_print( + "WARN", + f"Simple prompt blocked for client {client_id[:8]} " + f"preview={preview_text(raw_prompt)}", + ) + await log_event( + "prompt_blocked", + { + "kind": "user_raw", + "prompt_id": prompt_id, + "raw_prompt": raw_prompt, + "error": blocked_error, + }, + ) + await ws_send_json({ + "type": "error", + "message": blocked_error, + }) + return + + try: + replace_session_init_image(payload.get("initial_image")) + except ValueError as exc: + await ws_send_json({ + "type": "error", + "message": str(exc), + }) + return + + next_preset_id = str(payload.get("preset_id") or "").strip() + if next_preset_id: + preset_id = next_preset_id + next_preset_label = str(payload.get("preset_label") or "").strip() + if next_preset_label: + preset_label = next_preset_label + + next_enhancement_enabled = bool(payload.get("enhancement_enabled", True)) + + enhancement_enabled = next_enhancement_enabled + auto_extension_enabled = False + loop_generation_enabled = False + single_clip_mode = True + generation_paused = False + pending_seed_reset = True + pending_seed_reset_reason = "simple_generate" + seed_prompt_memory = ([] if next_enhancement_enabled else [raw_prompt]) + curated_prompts = list(seed_prompt_memory) + pending_simple_prompt_submission = (PromptSubmission( + prompt_id=prompt_id, + raw_prompt=raw_prompt, + created_at_s=time.time(), + ) if next_enhancement_enabled else None) + + await log_event( + "simple_generate", + { + "prompt_id": prompt_id, + "prompt": raw_prompt, + "preset_id": preset_id, + "enhancement_enabled": next_enhancement_enabled, + "has_initial_image": session_init_image is not None, + }, + ) + + if session_init_image is not None: + _main_print( + "INFO", f"Client {client_id[:8]} set simple initial image: " + f"{session_init_image.display_name}") + + if next_enhancement_enabled: + await ws_send_json({ + "type": "prompt_received", + "prompt_id": prompt_id, + "queue_depth": 1, + }) + + async def apply_project_init_payload(payload: dict[str, object]) -> bool: + nonlocal preset_id + nonlocal preset_label + nonlocal initial_rollout_prompt + nonlocal enhancement_enabled + nonlocal auto_extension_enabled + nonlocal loop_generation_enabled + nonlocal single_clip_mode + nonlocal generation_paused + nonlocal curated_prompts + nonlocal seed_prompt_memory + nonlocal curated_idx + nonlocal segment_idx + nonlocal locked_segment_prompts + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal pending_reset_conditioning + nonlocal force_curated_restart_segment + nonlocal generated_segment_count + nonlocal generation_cap_blocked + nonlocal auto_extension_blocked_segment_idx + nonlocal prompt_sources_drained_logged + nonlocal pending_simple_prompt_submission + nonlocal single_clip_waiting_for_request + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal loop_iteration + nonlocal rewrite_model + nonlocal rewrite_system_prompt + nonlocal rewrite_system_prompt_override + nonlocal rewrite_user_system_prompt_override + nonlocal rewrite_temperature + nonlocal project_active + nonlocal project_stream_started + nonlocal pending_project_end + + next_initial_rollout_prompt = str(payload.get("initial_rollout_prompt") or "").strip() + next_enhancement_enabled = bool(payload.get("enhancement_enabled", True)) + next_auto_extension_enabled = bool(payload.get("auto_extension_enabled", False)) + next_loop_generation_enabled = bool(payload.get("loop_generation_enabled", False)) + next_single_clip_mode = bool(payload.get("single_clip_mode", False)) + + next_preset_id = str(payload.get("preset_id") or "").strip() + if next_preset_id: + preset_id = next_preset_id + next_preset_label = str(payload.get("preset_label") or "").strip() + if next_preset_label: + preset_label = next_preset_label + + next_rewrite_model = self.prompt_enhancer.resolve_rewrite_model(payload.get("rewrite_model")) + next_rewrite_system_prompt_override = str(payload.get("rewrite_window_system_prompt") or "").strip() + next_rewrite_user_system_prompt_override = str(payload.get("rewrite_user_system_prompt") or "").strip() + next_rewrite_temperature = (self.prompt_enhancer.resolve_rewrite_temperature( + payload.get("rewrite_temperature"))) + + next_curated_prompts = [] + incoming_prompts = payload.get("curated_prompts", []) + if isinstance(incoming_prompts, list): + for prompt in incoming_prompts: + if isinstance(prompt, str) and prompt.strip(): + next_curated_prompts.append(prompt.strip()) + + blocked_init_prompt = get_first_blocked_prompt(next_curated_prompts) + if blocked_init_prompt is not None: + message = ("Prompt safety filter blocked initial prompt " + f"{blocked_init_prompt.index + 1}. " + f"{blocked_init_prompt.error}") + _main_print( + "WARN", + f"Project init prompt blocked for client {client_id[:8]} " + f"index={blocked_init_prompt.index + 1} " + f"preview={preview_text(blocked_init_prompt.prompt)}", + ) + await ws_send_json({ + "type": "error", + "message": message, + }) + return False + + try: + replace_session_init_image(payload.get("initial_image")) + except ValueError as exc: + await ws_send_json({ + "type": "error", + "message": str(exc), + }) + return False + + initial_rollout_prompt = next_initial_rollout_prompt + enhancement_enabled = next_enhancement_enabled + auto_extension_enabled = next_auto_extension_enabled + loop_generation_enabled = next_loop_generation_enabled + single_clip_mode = next_single_clip_mode + rewrite_model = next_rewrite_model + rewrite_system_prompt_override = (next_rewrite_system_prompt_override) + rewrite_user_system_prompt_override = (next_rewrite_user_system_prompt_override) + rewrite_system_prompt = ( + self.prompt_enhancer.resolve_rewrite_system_prompt(rewrite_system_prompt_override)) + rewrite_temperature = next_rewrite_temperature + + curated_prompts = list(next_curated_prompts) + seed_prompt_memory = list(next_curated_prompts) + curated_idx = 0 + segment_idx = 0 + locked_segment_prompts = [] + pending_seed_reset = False + pending_seed_reset_reason = "" + pending_reset_conditioning = True + force_curated_restart_segment = False + generated_segment_count = 0 + generation_cap_blocked = False + auto_extension_blocked_segment_idx = None + prompt_sources_drained_logged = False + pending_simple_prompt_submission = None + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + generation_paused = bool(initial_rollout_prompt and not single_clip_mode and len(curated_prompts) == 0) + initial_rollout_waiting_for_rewrite = generation_paused + rewrite_restart_pending = False + loop_iteration = 0 + project_active = True + project_stream_started = False + pending_project_end = False + + return True + + async def run_rewrite_seed_prompts( + rewrite_instruction: str, + rewrite_model_name: str, + rewrite_timeout_ms_value: int, + rewrite_temperature_value: float, + prompt_window_prompts: list[str] | None = None, + ): + nonlocal seed_prompt_memory + nonlocal curated_prompts + nonlocal preset_id + nonlocal preset_label + nonlocal rewrite_seed_prompts_task + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal generated_segment_count + nonlocal generation_cap_blocked + nonlocal generation_paused + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal rewrite_system_prompt + nonlocal rewrite_system_prompt_override + nonlocal rewrite_user_system_prompt_override + try: + snapshot_prompts = (list(prompt_window_prompts) if isinstance(prompt_window_prompts, list) + and len(prompt_window_prompts) > 0 else list(seed_prompt_memory)) + effective_system_prompt = (self.prompt_enhancer.resolve_rewrite_new_rollout_system_prompt( + rewrite_user_system_prompt_override or rewrite_system_prompt_override, ) + if len(snapshot_prompts) == 0 else rewrite_system_prompt) + rewrite_result = await self.prompt_enhancer.rewrite_prompt_sequence( + snapshot_prompts, + preset_id=preset_id, + preset_label=preset_label, + rewrite_instruction=rewrite_instruction, + rewrite_model=rewrite_model_name, + rewrite_temperature=rewrite_temperature_value, + timeout_ms=rewrite_timeout_ms_value, + system_prompt_override=effective_system_prompt, + ) + + if stop_event.is_set(): + return + + blocked_rewrite_prompt = get_first_blocked_prompt(rewrite_result.prompts) + if blocked_rewrite_prompt is not None: + error_message = ("Prompt safety filter blocked rewritten prompt " + f"{blocked_rewrite_prompt.index + 1}. " + f"{blocked_rewrite_prompt.error}") + await log_event( + "rewrite_blocked", + { + "kind": "seed_rewrite", + "rewrite_instruction": rewrite_instruction, + "model": rewrite_result.model, + "prompt_window_prompts": snapshot_prompts, + "rewritten_prompts": list(rewrite_result.prompts), + "blocked_prompt_index": blocked_rewrite_prompt.index, + "blocked_prompt": blocked_rewrite_prompt.prompt, + "error": error_message, + }, + ) + await ws_send_json({ + "type": "rewrite_seed_prompts_complete", + "fallback_used": True, + "error": error_message, + "model": rewrite_result.model, + "latency_ms": round(rewrite_result.latency_ms, 2), + }) + rewrite_restart_pending = False + return + + seed_prompt_memory = list(rewrite_result.prompts) + curated_prompts = list(seed_prompt_memory) + if not rewrite_result.fallback_used: + preset_id = rewrite_result.rollout_id + preset_label = rewrite_result.rollout_label + if initial_rollout_waiting_for_rewrite and not rewrite_result.fallback_used: + pending_seed_reset = True + pending_seed_reset_reason = "initial_rewrite" + generation_paused = False + initial_rollout_waiting_for_rewrite = False + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + if rollout_waiting_for_rewrite and not rewrite_result.fallback_used: + pending_seed_reset = True + pending_seed_reset_reason = "rewrite_rollout" + generated_segment_count = 0 + generation_cap_blocked = False + generation_paused = False + elif rewrite_restart_pending and not rewrite_result.fallback_used: + pending_seed_reset = True + pending_seed_reset_reason = "rewrite_during_generation" + generated_segment_count = 0 + generation_cap_blocked = False + generation_paused = False + rewrite_restart_pending = False + elif rewrite_restart_pending: + rewrite_restart_pending = False + await log_event( + "rewrite_done", + { + "kind": "seed_rewrite", + "rewrite_instruction": rewrite_instruction, + "latency_ms": round(rewrite_result.latency_ms, 2), + "response": rewrite_result.raw_response_text or "", + }, + ) + await ws_send_json({ + "type": "seed_prompts_updated", + "prompts": seed_prompt_memory, + "preset_id": preset_id, + "preset_label": preset_label, + "reason": "rewrite", + "fallback_used": rewrite_result.fallback_used, + "error": rewrite_result.error, + "model": rewrite_result.model, + "latency_ms": round(rewrite_result.latency_ms, 2), + "raw_llm_output": rewrite_result.raw_response_text, + "rewrite_instruction": rewrite_instruction, + }) + await ws_send_json({ + "type": "rewrite_seed_prompts_complete", + "fallback_used": rewrite_result.fallback_used, + "error": rewrite_result.error, + "preset_id": preset_id, + "preset_label": preset_label, + "model": rewrite_result.model, + "latency_ms": round(rewrite_result.latency_ms, 2), + }) + except asyncio.CancelledError: + rewrite_restart_pending = False + await log_event( + "rewrite_cancelled", + { + "kind": "seed_rewrite", + "rewrite_instruction": rewrite_instruction, + "rewrite_model": rewrite_model_name, + }, + ) + raise + except Exception as exc: + if stop_event.is_set(): + return + await log_event( + "rewrite_exception", + { + "kind": "seed_rewrite", + "rewrite_instruction": rewrite_instruction, + "rewrite_model": rewrite_model_name, + "error": str(exc), + }, + ) + await ws_send_json({ + "type": "rewrite_seed_prompts_complete", + "fallback_used": True, + "error": str(exc), + "model": rewrite_model_name, + "latency_ms": 0.0, + }) + rewrite_restart_pending = False + finally: + rewrite_seed_prompts_task = None + + async def websocket_reader_loop(): + nonlocal enhancement_enabled + nonlocal auto_extension_enabled + nonlocal loop_generation_enabled + nonlocal generation_paused + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal generated_segment_count + nonlocal generation_cap_blocked + nonlocal auto_extension_blocked_segment_idx + nonlocal seed_prompt_memory + nonlocal curated_prompts + nonlocal rewrite_seed_prompts_task + nonlocal rewrite_model + nonlocal rewrite_system_prompt + nonlocal rewrite_system_prompt_override + nonlocal rewrite_user_system_prompt_override + nonlocal rewrite_timeout_ms + nonlocal rewrite_temperature + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal project_active + nonlocal project_stream_started + nonlocal pending_project_end + while not stop_event.is_set(): + try: + data = await websocket.receive_json() + except WebSocketDisconnect: + loop_generation_enabled = False + stop_event.set() + return + except Exception as exc: + _main_print("INFO", f"Client {client_id[:8]} reader stopped: {exc}") + loop_generation_enabled = False + stop_event.set() + return + + msg_type = data.get("type") + if msg_type == "leave": + loop_generation_enabled = False + stop_event.set() + return + + if msg_type == "end_project_keep_session": + if (rewrite_seed_prompts_task is not None and not rewrite_seed_prompts_task.done()): + await cancel_task(rewrite_seed_prompts_task) + loop_generation_enabled = False + generation_paused = False + rollout_waiting_for_rewrite = False + initial_rollout_waiting_for_rewrite = False + rewrite_restart_pending = False + pending_project_end = True + continue + + if msg_type == "project_init_v1": + if project_active or pending_project_end: + await ws_send_json({ + "type": + "error", + "message": ("Current project is still ending. " + "Wait for the reset to finish before starting another project."), + }) + continue + init_applied = await apply_project_init_payload(data) + if not init_applied: + continue + if initial_rollout_waiting_for_rewrite: + await ws_send_json({ + "type": "rewrite_seed_prompts_started", + "model": rewrite_model, + }) + rewrite_seed_prompts_task = asyncio.create_task( + run_rewrite_seed_prompts( + initial_rollout_prompt, + rewrite_model, + rewrite_timeout_ms, + rewrite_temperature, + [], + )) + else: + pending_seed_reset = True + pending_seed_reset_reason = "project_init" + continue + + if not project_active: + if msg_type in { + "append_prompt", + "rewrite_seed_prompts", + "reset_to_seed_prompts", + "restart_generation", + }: + await ws_send_json({ + "type": "error", + "message": ("Start a new project before sending prompts."), + }) + continue + + if msg_type == "append_prompt": + if ((rollout_waiting_for_rewrite or initial_rollout_waiting_for_rewrite) + and not single_clip_mode): + await ws_send_json({ + "type": "error", + "message": ("Use Rewrite to create or restart the rollout."), + }) + continue + raw_prompt = str(data.get("prompt", "")).strip() + prompt_id = str(data.get("prompt_id") or uuid.uuid4()) + if not raw_prompt: + await ws_send_json({ + "type": "error", + "message": "append_prompt requires a non-empty prompt", + }) + continue + + blocked_error = get_prompt_safety_error(raw_prompt) + if blocked_error is not None: + _main_print( + "WARN", + f"Live prompt blocked for client {client_id[:8]} " + f"preview={preview_text(raw_prompt)}", + ) + await log_event( + "prompt_blocked", + { + "kind": "user_raw", + "prompt_id": prompt_id, + "raw_prompt": raw_prompt, + "error": blocked_error, + }, + ) + await ws_send_json({ + "type": "error", + "message": blocked_error, + }) + continue + + submission = PromptSubmission( + prompt_id=prompt_id, + raw_prompt=raw_prompt, + created_at_s=time.time(), + ) + await raw_prompt_queue.put(submission) + await log_event( + "append_prompt", + { + "prompt": raw_prompt, + }, + ) + await ws_send_json({ + "type": "prompt_received", + "prompt_id": prompt_id, + "queue_depth": raw_prompt_queue.qsize(), + }) + continue + + if msg_type == "simple_generate": + await schedule_simple_generate_request(data) + continue + + if msg_type == "set_enhancement": + enhancement_enabled = bool(data.get("enabled", enhancement_enabled)) + await ws_send_json({ + "type": "enhancement_updated", + "enabled": enhancement_enabled, + }) + continue + + if msg_type == "set_auto_extension": + was_enabled = auto_extension_enabled + auto_extension_enabled = bool(data.get("enabled", auto_extension_enabled)) + if not auto_extension_enabled: + auto_extension_blocked_segment_idx = None + _main_print( + "INFO", + f"Auto extension disabled for client {client_id[:8]} " + "(cleared_blocked_state=true)", + ) + else: + cleared = auto_extension_blocked_segment_idx is not None + auto_extension_blocked_segment_idx = None + _main_print( + "INFO", + f"Auto extension enabled for client {client_id[:8]} " + f"(was_enabled={was_enabled} " + f"cleared_blocked_state={cleared})", + ) + await ws_send_json({ + "type": "auto_extension_updated", + "enabled": auto_extension_enabled, + }) + continue + + if msg_type == "set_loop_generation": + loop_generation_enabled = bool(data.get("enabled", loop_generation_enabled)) + await ws_send_json({ + "type": "loop_generation_updated", + "enabled": loop_generation_enabled, + }) + continue + + if msg_type == "set_generation_paused": + generation_paused = bool(data.get("paused", generation_paused)) + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + continue + + if msg_type == "reset_to_seed_prompts": + pending_seed_reset = True + pending_seed_reset_reason = "manual_reset" + await ws_send_json({ + "type": "seed_prompts_reset_pending", + "seed_prompt_count": len(seed_prompt_memory), + }) + continue + + if msg_type == "restart_generation": + generated_segment_count = 0 + generation_cap_blocked = False + rollout_waiting_for_rewrite = False + pending_seed_reset = True + pending_seed_reset_reason = "cap_restart" + generation_paused = False + await ws_send_json({ + "type": + "generation_restarted", + "reason": + "cap_restart", + "segment_cap": + _resolve_generation_segment_cap( + single_clip_mode=single_clip_mode, + cap=GENERATION_SEGMENT_CAP, + ), + }) + continue + + if msg_type == "rewrite_seed_prompts": + rewrite_instruction = str(data.get("rewrite_instruction", "")).strip() + rewrite_model = self.prompt_enhancer.resolve_rewrite_model(data.get("rewrite_model")) + rewrite_system_prompt_override = str(data.get("rewrite_window_system_prompt") or "").strip() + rewrite_user_system_prompt_override = str(data.get("rewrite_user_system_prompt") or "").strip() + rewrite_system_prompt = self.prompt_enhancer.resolve_rewrite_system_prompt( + rewrite_system_prompt_override) + rewrite_temperature = self.prompt_enhancer.resolve_rewrite_temperature( + data.get("rewrite_temperature")) + if len(seed_prompt_memory) == 0 and not (initial_rollout_waiting_for_rewrite + and rewrite_instruction): + await ws_send_json({ + "type": "error", + "message": "No seed prompts available to rewrite.", + }) + continue + if rewrite_seed_prompts_task is not None and not rewrite_seed_prompts_task.done(): + await ws_send_json({ + "type": "error", + "message": "rewrite_seed_prompts already in progress.", + }) + continue + if (segment_generation_active and not single_clip_mode and not rollout_waiting_for_rewrite + and not initial_rollout_waiting_for_rewrite): + rewrite_restart_pending = True + + await ws_send_json({ + "type": "rewrite_seed_prompts_started", + "model": rewrite_model, + }) + requested_prompt_window = normalize_prompt_window_prompts(data.get("prompt_window_prompts")) + rewrite_seed_prompts_task = asyncio.create_task( + run_rewrite_seed_prompts( + rewrite_instruction, + rewrite_model, + rewrite_timeout_ms, + rewrite_temperature, + requested_prompt_window, + )) + continue + + if msg_type == "set_rewrite_model": + rewrite_model = self.prompt_enhancer.resolve_rewrite_model(data.get("rewrite_model")) + await ws_send_json({ + "type": "rewrite_model_updated", + "rewrite_model": rewrite_model, + }) + continue + + if msg_type == "set_rewrite_temperature": + rewrite_temperature = self.prompt_enhancer.resolve_rewrite_temperature( + data.get("rewrite_temperature")) + await ws_send_json({ + "type": "rewrite_temperature_updated", + "rewrite_temperature": rewrite_temperature, + }) + continue + + async def prompt_worker_loop(): + while not stop_event.is_set(): + try: + submission = await asyncio.wait_for(raw_prompt_queue.get(), timeout=0.1) + except asyncio.TimeoutError: + continue + _main_print("INFO", f"Received user prompt for enhancement: {submission.raw_prompt}") + prompt_id = submission.prompt_id + raw_prompt = submission.raw_prompt + blocked_raw_prompt_error = get_prompt_safety_error(raw_prompt) + if blocked_raw_prompt_error is not None: + _main_print( + "WARN", + f"User prompt blocked for client {client_id[:8]} " + f"prompt_id={prompt_id} preview={preview_text(raw_prompt)}", + ) + await log_event( + "prompt_blocked", + payload={ + "kind": "user_raw", + "prompt_id": prompt_id, + "raw_prompt": raw_prompt, + "error": blocked_raw_prompt_error, + }, + ) + await ws_send_json({ + "type": "error", + "message": blocked_raw_prompt_error, + }) + continue + await log_event( + "enhance_request", + { + "prompt_id": prompt_id, + "raw_prompt": raw_prompt, + "enhancement_enabled": enhancement_enabled, + "rewrite_model": rewrite_model, + }, + ) + if enhancement_enabled: + locked_snapshot = list(locked_segment_prompts) + next_segment_idx = len(locked_snapshot) + 1 + _main_print( + "INFO", + f"Enhancement context: next_segment={next_segment_idx} " + f"locked_count={len(locked_snapshot)}", + ) + await ws_send_json({ + "type": "prompt_enhancing", + "prompt_id": prompt_id, + }) + result = await self.prompt_enhancer.enhance_prompt( + raw_prompt, + locked_segments=locked_snapshot, + next_segment_idx=next_segment_idx, + preset_id=preset_id, + mode="single_clip" if single_clip_mode else "user_live", + model=rewrite_model, + timeout_ms=PROMPT_TIMEOUT_MS, + ) + final_prompt = result.prompt.strip() if result.prompt else "" + blocked_final_prompt_error = get_prompt_safety_error(final_prompt) + _main_print("INFO", f"Enhanced prompt: {final_prompt}") + await log_event( + "rewrite_done", + { + "kind": "enhance_prompt", + "latency_ms": round(result.latency_ms, 2), + "response": final_prompt, + }, + ) + if blocked_final_prompt_error is not None: + await log_event( + "prompt_blocked", + { + "kind": "user_enhanced", + "prompt_id": prompt_id, + "raw_prompt": raw_prompt, + "output_prompt": final_prompt, + "fallback_used": result.fallback_used, + "latency_ms": round(result.latency_ms, 2), + "provider": result.provider, + "model": result.model, + "error": blocked_final_prompt_error, + }, + ) + await ws_send_json({ + "type": "error", + "message": blocked_final_prompt_error, + }) + continue + if result.fallback_used or not final_prompt: + source = "user_enhancement_failed" + _main_print( + "WARN", + f"User prompt enhancement fallback: client={client_id[:8]} " + f"prompt_id={prompt_id} latency_ms={result.latency_ms:.2f} " + f"error={result.error}", + ) + await ws_send_json({ + "type": "prompt_fallback_used", + "prompt_id": prompt_id, + "prompt": final_prompt, + "source": source, + "latency_ms": round(result.latency_ms, 2), + "error": PROMPT_EXTENSION_FAILURE_USER_MESSAGE, + }) + # Enhancement is strict JSON-only; do not enqueue raw + # prompt when enhancement fails. + continue + else: + source = "user_enhanced" + await ws_send_json({ + "type": "prompt_ready", + "prompt_id": prompt_id, + "prompt": final_prompt, + "source": source, + "latency_ms": round(result.latency_ms, 2), + }) + await ready_prompt_queue.put( + ReadyPrompt( + prompt_id=prompt_id, + prompt=final_prompt, + source=source, + fallback_used=result.fallback_used, + loop_iteration=loop_iteration, + )) + else: + await ready_prompt_queue.put( + ReadyPrompt( + prompt_id=prompt_id, + prompt=raw_prompt, + source="user_raw", + fallback_used=False, + loop_iteration=loop_iteration, + )) + await ws_send_json({ + "type": "prompt_ready", + "prompt_id": prompt_id, + "prompt": raw_prompt, + "source": "user_raw", + "latency_ms": 0.0, + }) + + def queue_snapshot() -> dict[str, object]: + return { + "user_ready": ready_prompt_queue.qsize(), + "curated_remaining": max(len(curated_prompts) - curated_idx, 0), + "auto_enabled": auto_extension_enabled, + "auto_blocked_segment": auto_extension_blocked_segment_idx, + "loop_enabled": loop_generation_enabled, + "paused": generation_paused, + "seed_prompt_count": len(seed_prompt_memory), + "loop_iteration": loop_iteration, + } + + def drain_queue_nowait(queue: asyncio.Queue) -> int: + drained = 0 + while True: + try: + queue.get_nowait() + drained += 1 + except asyncio.QueueEmpty: + break + return drained + + async def enter_project_idle() -> None: + nonlocal curated_prompts + nonlocal seed_prompt_memory + nonlocal curated_idx + nonlocal segment_idx + nonlocal locked_segment_prompts + nonlocal generated_segment_count + nonlocal generation_cap_blocked + nonlocal auto_extension_blocked_segment_idx + nonlocal prompt_sources_drained_logged + nonlocal generation_paused + nonlocal pending_seed_reset + nonlocal pending_seed_reset_reason + nonlocal pending_reset_conditioning + nonlocal force_curated_restart_segment + nonlocal pending_simple_prompt_submission + nonlocal single_clip_waiting_for_request + nonlocal rollout_waiting_for_rewrite + nonlocal initial_rollout_waiting_for_rewrite + nonlocal rewrite_restart_pending + nonlocal initial_rollout_prompt + nonlocal project_active + nonlocal project_stream_started + nonlocal pending_project_end + + dropped_raw = drain_queue_nowait(raw_prompt_queue) + dropped_ready = drain_queue_nowait(ready_prompt_queue) + + curated_prompts = [] + seed_prompt_memory = [] + curated_idx = 0 + segment_idx = 0 + locked_segment_prompts = [] + generated_segment_count = 0 + generation_cap_blocked = False + auto_extension_blocked_segment_idx = None + prompt_sources_drained_logged = False + generation_paused = False + pending_seed_reset = False + pending_seed_reset_reason = "" + pending_reset_conditioning = True + force_curated_restart_segment = False + pending_simple_prompt_submission = None + single_clip_waiting_for_request = False + rollout_waiting_for_rewrite = False + initial_rollout_waiting_for_rewrite = False + rewrite_restart_pending = False + initial_rollout_prompt = "" + project_active = False + pending_project_end = False + + if project_stream_started: + project_stream_started = False + await ws_send_json({"type": "ltx2_stream_complete"}) + await log_event("ws_stream_complete") + + await ws_send_json({ + "type": "project_idle", + "dropped_user_raw_queue": dropped_raw, + "dropped_user_ready_queue": dropped_ready, + }) + + def pick_next_prompt_nowait() -> ReadyPrompt | None: + nonlocal curated_idx + try: + return ready_prompt_queue.get_nowait() + except asyncio.QueueEmpty: + pass + + if curated_idx < len(curated_prompts): + prompt_idx = curated_idx + prompt = curated_prompts[prompt_idx] + curated_idx += 1 + return ReadyPrompt( + prompt=prompt, + source="curated", + seed_prompt_index=prompt_idx, + loop_iteration=loop_iteration, + ) + + return None + + websocket_reader_task = asyncio.create_task(websocket_reader_loop()) + prompt_worker_task = asyncio.create_task(prompt_worker_loop()) + await asyncio.sleep(0) + + await ws_send_json({ + "type": "loop_generation_updated", + "enabled": loop_generation_enabled, + }) + await ws_send_json({ + "type": "generation_paused_updated", + "paused": generation_paused, + }) + if pending_project_end: + await enter_project_idle() + elif initial_rollout_waiting_for_rewrite: + await ws_send_json({ + "type": "rewrite_seed_prompts_started", + "model": rewrite_model, + }) + rewrite_seed_prompts_task = asyncio.create_task( + run_rewrite_seed_prompts( + initial_rollout_prompt, + rewrite_model, + rewrite_timeout_ms, + rewrite_temperature, + [], + )) + else: + project_stream_started = True + await ws_send_json({ + "type": + "ltx2_stream_start", + "total_segments": + len(curated_prompts), + "preset_id": + preset_id, + "stream_mode": + "av_fmp4", + "live_mode": + True, + "loop_generation_enabled": + loop_generation_enabled, + "loop_iteration": + loop_iteration, + "generation_segment_cap": + _resolve_generation_segment_cap( + single_clip_mode=single_clip_mode, + cap=GENERATION_SEGMENT_CAP, + ), + }) + await ws_send_json({ + "type": "seed_prompts_updated", + "prompts": seed_prompt_memory, + "preset_id": preset_id, + "preset_label": preset_label, + "reason": "init", + "seed_prompt_count": len(seed_prompt_memory), + }) + + av_event_queue = slot.register_stream_queue(client_id) + + while not stop_event.is_set(): + if pending_project_end: + await enter_project_idle() + await asyncio.sleep(0) + continue + + if not project_active: + await asyncio.sleep(0.05) + continue + + if pending_seed_reset: + nonlocal_reason = pending_seed_reset_reason + curated_prompts = list(seed_prompt_memory) + curated_idx = 0 + segment_idx = 0 + locked_segment_prompts = [] + auto_extension_blocked_segment_idx = None + prompt_sources_drained_logged = False + pending_seed_reset = False + pending_reset_conditioning = True + force_curated_restart_segment = len(curated_prompts) > 0 + single_clip_waiting_for_request = False + dropped_raw = drain_queue_nowait(raw_prompt_queue) + dropped_ready = drain_queue_nowait(ready_prompt_queue) + + loop_iteration += 1 + project_stream_started = True + await ws_send_json({ + "type": + "ltx2_stream_start", + "total_segments": + len(curated_prompts), + "preset_id": + preset_id, + "stream_mode": + "av_fmp4", + "live_mode": + True, + "loop_generation_enabled": + loop_generation_enabled, + "loop_iteration": + loop_iteration, + "generation_segment_cap": + _resolve_generation_segment_cap( + single_clip_mode=single_clip_mode, + cap=GENERATION_SEGMENT_CAP, + ), + }) + if nonlocal_reason == "loop_restart": + await ws_send_json({ + "type": "loop_restarted", + "loop_iteration": loop_iteration, + "seed_prompt_count": len(seed_prompt_memory), + "dropped_user_raw_queue": dropped_raw, + "dropped_user_ready_queue": dropped_ready, + }) + else: + await ws_send_json({ + "type": "seed_prompts_reset_applied", + "reason": nonlocal_reason or "manual_reset", + "loop_iteration": loop_iteration, + "seed_prompt_count": len(seed_prompt_memory), + "dropped_user_raw_queue": dropped_raw, + "dropped_user_ready_queue": dropped_ready, + }) + if nonlocal_reason == "cap_restart": + generation_cap_blocked = False + generated_segment_count = 0 + rollout_waiting_for_rewrite = False + pending_seed_reset_reason = "" + + if pending_simple_prompt_submission is not None: + await raw_prompt_queue.put(pending_simple_prompt_submission) + pending_simple_prompt_submission = None + + if (not single_clip_mode and not generation_cap_blocked and not rollout_waiting_for_rewrite + and GENERATION_SEGMENT_CAP > 0 and generated_segment_count >= GENERATION_SEGMENT_CAP): + loop_generation_enabled = False + rollout_waiting_for_rewrite = True + _main_print( + "INFO", + f"Segment cap reached for client {client_id[:8]} " + f"(cap_segments={GENERATION_SEGMENT_CAP}, " + f"generated_segments={generated_segment_count}); " + "waiting for rollout rewrite", + ) + project_stream_started = False + await ws_send_json({"type": "ltx2_stream_complete"}) + await log_event("ws_stream_complete") + + if generation_cap_blocked: + await asyncio.sleep(0.05) + continue + + if rollout_waiting_for_rewrite: + await asyncio.sleep(0.05) + continue + + if initial_rollout_waiting_for_rewrite: + await asyncio.sleep(0.05) + continue + + if generation_paused: + await asyncio.sleep(0.05) + continue + + if rewrite_restart_pending: + if (rewrite_seed_prompts_task is not None and not rewrite_seed_prompts_task.done()): + await asyncio.sleep(0.05) + continue + rewrite_restart_pending = False + + if force_curated_restart_segment and curated_idx < len(curated_prompts): + prompt_idx = curated_idx + prompt = curated_prompts[prompt_idx] + curated_idx += 1 + selected = ReadyPrompt( + prompt=prompt, + source="curated", + seed_prompt_index=prompt_idx, + loop_iteration=loop_iteration, + ) + force_curated_restart_segment = False + else: + force_curated_restart_segment = False + selected = pick_next_prompt_nowait() + if selected is None and loop_generation_enabled and len(seed_prompt_memory) > 0: + pending_seed_reset = True + pending_seed_reset_reason = "loop_restart" + await asyncio.sleep(0) + continue + + if selected is None and auto_extension_enabled: + next_segment = len(locked_segment_prompts) + 1 + if auto_extension_blocked_segment_idx == next_segment: + if not prompt_sources_drained_logged: + snapshot = queue_snapshot() + _main_print( + "WARN", + f"Prompt sources drained for client {client_id[:8]} " + f"at segment boundary {segment_idx + 1} " + f"(user_ready={snapshot['user_ready']} " + f"curated_remaining={snapshot['curated_remaining']} " + f"auto_enabled={snapshot['auto_enabled']} " + "auto_state=blocked_after_failed_attempt)", + ) + prompt_sources_drained_logged = True + await asyncio.sleep(PROMPT_AUTO_SLEEP_MS / 1000.0) + continue + + locked_snapshot = list(locked_segment_prompts) + last_locked_preview = (preview_text(locked_snapshot[-1]) if locked_snapshot else "(none)") + _main_print( + "INFO", + f"Auto prompt generation request: client={client_id[:8]} " + f"next_segment={next_segment} " + f"locked_count={len(locked_snapshot)} " + f"last_locked_preview={last_locked_preview}", + ) + result = await self.prompt_enhancer.generate_auto_prompt( + locked_segments=locked_snapshot, + next_segment_idx=next_segment, + model=rewrite_model, + timeout_ms=PROMPT_AUTO_TIMEOUT_MS, + ) + _main_print( + "INFO", + f"Auto prompt generation response: client={client_id[:8]} " + f"next_segment={next_segment} " + f"fallback_used={result.fallback_used} " + f"returned_len={len(result.prompt)} " + f"latency_ms={result.latency_ms:.2f} error={result.error} " + f"returned_preview={preview_text(result.prompt)}", + ) + auto_prompt = result.prompt.strip() if result.prompt else "" + blocked_auto_prompt_error = get_prompt_safety_error(auto_prompt) + if result.fallback_used or not auto_prompt: + auto_extension_blocked_segment_idx = next_segment + _main_print( + "WARN", + f"Auto prompt generation failed: client={client_id[:8]} " + f"next_segment={next_segment} " + f"latency_ms={result.latency_ms:.2f} " + f"error={result.error or 'Auto prompt generation returned no prompt.'} " + "policy=blocked_no_retry_until_external_action", + ) + await ws_send_json({ + "type": "auto_prompt_failed", + "segment_idx": next_segment, + "latency_ms": round(result.latency_ms, 2), + "error": result.error or "Auto prompt generation returned no prompt.", + }) + await asyncio.sleep(PROMPT_AUTO_SLEEP_MS / 1000.0) + continue + if blocked_auto_prompt_error is not None: + auto_extension_blocked_segment_idx = next_segment + await log_event( + "auto_prompt_blocked", + { + "next_segment_idx": next_segment, + "output_prompt": auto_prompt, + "error": blocked_auto_prompt_error, + "latency_ms": round(result.latency_ms, 2), + "provider": result.provider, + "model": result.model, + }, + ) + await ws_send_json({ + "type": "auto_prompt_failed", + "segment_idx": next_segment, + "latency_ms": round(result.latency_ms, 2), + "error": blocked_auto_prompt_error, + }) + await asyncio.sleep(PROMPT_AUTO_SLEEP_MS / 1000.0) + continue + + auto_extension_blocked_segment_idx = None + selected = ReadyPrompt( + prompt=auto_prompt, + source="auto_llm", + prompt_id=None, + fallback_used=False, + loop_iteration=loop_iteration, + ) + + if selected is None: + if single_clip_mode: + await asyncio.sleep(PROMPT_AUTO_SLEEP_MS / 1000.0) + continue + if not prompt_sources_drained_logged: + snapshot = queue_snapshot() + _main_print( + "WARN", + f"Prompt sources drained for client {client_id[:8]} " + f"at segment boundary {segment_idx + 1} " + f"(user_ready={snapshot['user_ready']} " + f"curated_remaining={snapshot['curated_remaining']} " + f"auto_enabled={snapshot['auto_enabled']} " + f"auto_blocked_segment={snapshot['auto_blocked_segment']})", + ) + prompt_sources_drained_logged = True + await ws_send_json({ + "type": "prompt_sources_blocked", + "segment_idx": segment_idx + 1, + }) + await asyncio.sleep(PROMPT_AUTO_SLEEP_MS / 1000.0) + continue + + if prompt_sources_drained_logged: + prompt_sources_drained_logged = False + _main_print( + "INFO", + f"Prompt source available again for client {client_id[:8]} " + f"at segment boundary {segment_idx + 1} " + f"(source={selected.source} prompt_id={selected.prompt_id})", + ) + await ws_send_json({ + "type": "prompt_sources_resumed", + "segment_idx": segment_idx + 1, + "source": selected.source, + "prompt_id": selected.prompt_id, + }) + + segment_idx += 1 + single_clip_waiting_for_request = False + total_segments_hint = max(segment_idx, len(curated_prompts)) + prompt = selected.prompt + locked_segment_prompts.append(prompt) + if (auto_extension_blocked_segment_idx is not None + and auto_extension_blocked_segment_idx <= segment_idx): + auto_extension_blocked_segment_idx = None + + await ws_send_json({ + "type": "segment_prompt_source", + "segment_idx": segment_idx, + "source": selected.source, + "prompt_id": selected.prompt_id, + "fallback_used": selected.fallback_used, + "seed_prompt_index": selected.seed_prompt_index, + "loop_iteration": selected.loop_iteration or loop_iteration, + }) + await ws_send_json({ + "type": "ltx2_segment_start", + "segment_idx": segment_idx, + "total_segments": total_segments_hint, + "prompt": prompt, + "source": selected.source, + "seed_prompt_index": selected.seed_prompt_index, + "loop_iteration": selected.loop_iteration or loop_iteration, + }) + await log_event( + "segment_start", + { + "segment_idx": segment_idx, + }, + ) + + t_start = time.perf_counter() + timings: dict = {} + av_chunks_relayed = 0 + av_bytes_relayed = 0 + av_streamed = False + + step_reset_conditioning = pending_reset_conditioning + pending_reset_conditioning = False + step_image_path = (str(session_init_image.file_path) + if segment_idx == 1 and session_init_image is not None else None) + step_task = asyncio.create_task( + slot.user_step( + client_id, + prompt=prompt, + segment_idx=segment_idx, + image_path=step_image_path, + reset_conditioning=step_reset_conditioning, + )) + segment_generation_active = True + try: + while not stop_event.is_set() and (not step_task.done() or not av_event_queue.empty()): + try: + event = await asyncio.wait_for(av_event_queue.get(), timeout=0.05) + except asyncio.TimeoutError: + continue + + if event.segment_idx != segment_idx: + print(f"[GPU {gpu_id}] Ignoring out-of-order AV event: " + f"event_seg={event.segment_idx}, current_seg={segment_idx}, " + f"kind={type(event).__name__}") + continue + + match event: + case MediaInit(mime=mime, stream_id=stream_id): + av_streamed = True + await ws_send_json({ + "type": "media_init", + "segment_idx": segment_idx, + "mime": mime, + "stream_id": stream_id, + "mode": "av_fmp4", + }) + case MediaChunk( + chunk=chunk_bytes, + chunk_offset=chunk_offset, + chunk_length=chunk_length, + uses_shared_buffer=uses_shared, + ): + if (uses_shared and chunk_offset is not None and chunk_length is not None + and slot.shared_stream_buffer is not None): + start = chunk_offset + end = start + chunk_length + # Copy out of shared buffer for websocket send. + chunk = bytes(slot.shared_stream_buffer[start:end]) + else: + chunk = chunk_bytes or b"" + if chunk: + av_chunks_relayed += 1 + av_bytes_relayed += len(chunk) + await ws_send_bytes(chunk) + case MediaComplete(stream_id=stream_id, chunks=n): + await ws_send_json({ + "type": "media_segment_complete", + "segment_idx": segment_idx, + "stream_id": stream_id, + "chunks": n if n is not None else av_chunks_relayed, + }) + case _: + print(f"[GPU {gpu_id}] Unknown AV event: " + f"{type(event).__name__}") + + if not step_task.done(): + step_task.cancel() + else: + timings = await step_task + finally: + segment_generation_active = False + if not step_task.done(): + try: + await step_task + except asyncio.CancelledError: + pass + + if stop_event.is_set(): + break + + t_generation = time.perf_counter() - t_start + worker_e2e_ms = float(timings.get("e2e_latency_ms", 0.0) or 0.0) + main_user_step_ms = t_generation * 1000.0 + overhead_vs_worker_ms = main_user_step_ms - worker_e2e_ms + + ipc_put_start_ns = timings.get("ipc_put_start_ns") + ipc_get_done_ns = timings.get("ipc_get_done_ns") + ipc_queue_transfer_ms = None + if isinstance(ipc_put_start_ns, int) and isinstance(ipc_get_done_ns, int): + ipc_queue_transfer_ms = (ipc_get_done_ns - ipc_put_start_ns) / 1_000_000.0 + + ipc_queue_str = (f"{ipc_queue_transfer_ms:.0f}ms" if ipc_queue_transfer_ms is not None else "n/a") + print(f"[GPU {gpu_id}] Segment {segment_idx}: " + f"source={selected.source}, " + f"worker_e2e={worker_e2e_ms:.0f}ms, " + f"main_user_step={main_user_step_ms:.0f}ms, " + f"overhead={overhead_vs_worker_ms:.0f}ms, " + f"ipc_queue={ipc_queue_str}") + + if not av_streamed: + raise RuntimeError(f"Segment {segment_idx} AV stream did not initialize " + "(no media_init event)") + generated_segment_count += 1 + + print(f"[GPU {gpu_id}] Segment {segment_idx}: " + f"relayed av chunks={av_chunks_relayed}, " + f"bytes={av_bytes_relayed / (1024 * 1024):.1f}MB") + _main_print( + "INFO", + f"Segments generated for client {client_id[:8]}: " + f"{generated_segment_count}", + ) + await log_event( + "segment_complete", + { + "segment_idx": segment_idx, + "latency_ms": { + "total": round(main_user_step_ms, 2), + "worker_e2e": round(worker_e2e_ms, 2), + "main_user_step": round(main_user_step_ms, 2), + "overhead": round(overhead_vs_worker_ms, 2), + }, + "data_size_bytes": av_bytes_relayed, + }, + ) + await ws_send_json({ + "type": "step_complete", + "latency_ms": { + "total": round(main_user_step_ms, 2), + "worker_e2e": round(worker_e2e_ms, 2), + "main_user_step": round(main_user_step_ms, 2), + "overhead": round(overhead_vs_worker_ms, 2), + }, + }) + await ws_send_json({ + "type": "ltx2_segment_complete", + "segment_idx": segment_idx, + "total_segments": total_segments_hint, + }) + if single_clip_mode and not single_clip_waiting_for_request: + single_clip_waiting_for_request = True + project_stream_started = False + await ws_send_json({"type": "ltx2_stream_complete"}) + await log_event("ws_stream_complete") + _main_print( + "INFO", + f"Single-clip session idle for client {client_id[:8]} " + f"after segment {segment_idx}; waiting for next create", + ) + + if not stop_event.is_set() and project_stream_started: + project_stream_started = False + await ws_send_json({"type": "ltx2_stream_complete"}) + await log_event("ws_stream_complete") + print(f"[GPU {gpu_id}] LTX2 streaming complete for {client_id[:8]}") + + except WebSocketDisconnect: + stop_event.set() + _main_print("INFO", f"Client {client_id[:8]} disconnected") + except Exception as exc: + stop_event.set() + _main_print("ERROR", f"Client {client_id[:8]} error: {exc}") + try: + await ws_send_json({ + "type": "error", + "message": f"AV streaming failed: {exc}", + }) + except Exception: + pass + import traceback + traceback.print_exc() + finally: + stop_event.set() + await cancel_task(websocket_reader_task) + await cancel_task(prompt_worker_task) + await cancel_task(rewrite_seed_prompts_task) + + if slot is not None and client_id: + slot.unregister_stream_queue(client_id) + + if timeout_task and not timeout_task.done(): + timeout_task.cancel() + try: + await timeout_task + except asyncio.CancelledError: + pass + + try: + if client_id and self.gpu_pool is not None: + await self.gpu_pool.release(client_id) + finally: + cleanup_session_init_image(session_init_image) diff --git a/apps/dreamverse/dreamverse/session/messages.py b/apps/dreamverse/dreamverse/session/messages.py new file mode 100644 index 0000000000..987f3f4ca5 --- /dev/null +++ b/apps/dreamverse/dreamverse/session/messages.py @@ -0,0 +1,21 @@ +"""Dataclasses for the prompt-submission pipeline queues.""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class PromptSubmission: + prompt_id: str + raw_prompt: str + created_at_s: float + + +@dataclass +class ReadyPrompt: + prompt: str + source: str + prompt_id: str | None = None + fallback_used: bool = False + seed_prompt_index: int | None = None + loop_iteration: int | None = None diff --git a/apps/dreamverse/dreamverse/session_init_image.py b/apps/dreamverse/dreamverse/session_init_image.py new file mode 100644 index 0000000000..5ad4ae8f17 --- /dev/null +++ b/apps/dreamverse/dreamverse/session_init_image.py @@ -0,0 +1,101 @@ +# pyright: reportMissingTypeArgument=false, reportArgumentType=false +from __future__ import annotations + +import base64 +import io +import re +import shutil +import tempfile +from dataclasses import dataclass +from pathlib import Path + +from PIL import Image + +MAX_SESSION_INIT_IMAGE_BYTES = 15 * 1024 * 1024 +SUPPORTED_SESSION_INIT_IMAGE_MIME_TYPES = { + "image/jpeg", + "image/png", + "image/webp", +} + +_DATA_URL_RE = re.compile(r"^data:(?P<mime>[-\w.+/]+);base64,(?P<data>[A-Za-z0-9+/=\s]+)$") + + +@dataclass(frozen=True) +class SessionInitImage: + file_path: Path + temp_dir: Path + display_name: str + mime_type: str + + +def _sanitize_display_name(raw_name: object) -> str: + text = str(raw_name or "").strip() + if not text: + return "uploaded-image" + return Path(text).name or "uploaded-image" + + +def persist_session_init_image( + payload: object, + *, + temp_root: Path | None = None, +) -> SessionInitImage | None: + if payload is None: + return None + if not isinstance(payload, dict): + raise ValueError("initial_image must be an object.") + + data_url = str(payload.get("data_url") or "").strip() + if not data_url: + return None + + data_url_match = _DATA_URL_RE.match(data_url) + if data_url_match is None: + raise ValueError("initial_image.data_url must be a base64 data URL.") + + data_url_mime_type = data_url_match.group("mime").strip().lower() + mime_type = str(payload.get("mime_type") or data_url_mime_type).strip().lower() + if mime_type != data_url_mime_type: + raise ValueError("initial_image.mime_type must match the data URL mime type.") + if mime_type not in SUPPORTED_SESSION_INIT_IMAGE_MIME_TYPES: + raise ValueError("initial_image must be a PNG, JPEG, or WebP image.") + + encoded_bytes = re.sub(r"\s+", "", data_url_match.group("data")) + try: + raw_bytes = base64.b64decode(encoded_bytes, validate=True) + except Exception as exc: + raise ValueError("initial_image.data_url is not valid base64 data.") from exc + + if len(raw_bytes) == 0: + raise ValueError("initial_image.data_url did not contain image bytes.") + if len(raw_bytes) > MAX_SESSION_INIT_IMAGE_BYTES: + raise ValueError("initial_image must be 15 MB or smaller.") + + try: + with Image.open(io.BytesIO(raw_bytes)) as image: + image.load() + normalized = image.convert("RGBA" if "A" in image.getbands() else "RGB") + except Exception as exc: + raise ValueError("initial_image must decode as a valid image.") from exc + + temp_dir = Path( + tempfile.mkdtemp( + prefix="ltx2_session_init_", + dir=str(temp_root) if temp_root is not None else None, + )) + file_path = temp_dir / "initial_frame.png" + normalized.save(file_path, format="PNG") + + return SessionInitImage( + file_path=file_path, + temp_dir=temp_dir, + display_name=_sanitize_display_name(payload.get("name")), + mime_type=mime_type, + ) + + +def cleanup_session_init_image(session_image: SessionInitImage | None) -> None: + if session_image is None: + return + shutil.rmtree(session_image.temp_dir, ignore_errors=True) diff --git a/apps/dreamverse/dreamverse/session_logger.py b/apps/dreamverse/dreamverse/session_logger.py new file mode 100644 index 0000000000..f537fdacc8 --- /dev/null +++ b/apps/dreamverse/dreamverse/session_logger.py @@ -0,0 +1,46 @@ +# pyright: reportMissingTypeArgument=false +from __future__ import annotations + +import asyncio +from datetime import datetime, timezone +import json +import socket +from pathlib import Path +from typing import Any + + +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +class SessionEventLogger: + + def __init__(self, root_dir: Path): + self.hostname = socket.gethostname() + timestamp = datetime.now(timezone.utc).strftime("%y%m%d_%H%M%S") + self.directory = root_dir / self.hostname + self.path = self.directory / f"{timestamp}.jsonl" + self._lock = asyncio.Lock() + + self.directory.mkdir(parents=True, exist_ok=True) + self.path.touch(exist_ok=False) + + async def write_event( + self, + *, + event: str, + client_id: str, + payload: dict[str, Any] | None = None, + ) -> None: + entry = { + "ts": _utc_now_iso(), + "event": event, + "hostname": self.hostname, + "client_id": client_id, + } + if payload: + entry.update(payload) + + async with self._lock: + with self.path.open("a", encoding="utf-8") as fp: + fp.write(json.dumps(entry, ensure_ascii=False) + "\n") diff --git a/apps/dreamverse/dreamverse/tests/__init__.py b/apps/dreamverse/dreamverse/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/dreamverse/dreamverse/tests/conftest.py b/apps/dreamverse/dreamverse/tests/conftest.py new file mode 100644 index 0000000000..89b3ea01e5 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/conftest.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import sys +from pathlib import Path + + +TESTS_DIR = Path(__file__).resolve().parent +DREAMVERSE_PACKAGE_DIR = TESTS_DIR.parent +DREAMVERSE_APP_DIR = DREAMVERSE_PACKAGE_DIR.parent +BENCHMARKS_DIR = DREAMVERSE_PACKAGE_DIR / "benchmarks" + +for path in (DREAMVERSE_APP_DIR, BENCHMARKS_DIR): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) diff --git a/apps/dreamverse/dreamverse/tests/test_config.py b/apps/dreamverse/dreamverse/tests/test_config.py new file mode 100644 index 0000000000..6a06ccffd5 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_config.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + + +SERVER_DIR = Path(__file__).resolve().parents[1] + + +def _load_config_module(): + spec = importlib.util.spec_from_file_location( + "server_config_test_module", + SERVER_DIR / "config.py", + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _set_required_prompt_keys(monkeypatch): + monkeypatch.setenv("CEREBRAS_API_KEY", "cerebras-key") + monkeypatch.setenv("GROQ_API_KEY", "groq-key") + + +def test_config_allows_missing_cerebras_api_key_until_prompt_runtime(monkeypatch): + monkeypatch.delenv("FASTVIDEO_PROMPT_PROVIDER", raising=False) + monkeypatch.delenv("CEREBRAS_API_KEY", raising=False) + monkeypatch.delenv("GROQ_API_KEY", raising=False) + + module = _load_config_module() + + assert module.PROMPT_API_KEYS["cerebras"] is None + + +def test_config_allows_missing_groq_api_key_until_prompt_runtime(monkeypatch): + monkeypatch.delenv("FASTVIDEO_PROMPT_PROVIDER", raising=False) + monkeypatch.setenv("CEREBRAS_API_KEY", "cerebras-key") + monkeypatch.delenv("GROQ_API_KEY", raising=False) + + module = _load_config_module() + + assert module.PROMPT_API_KEYS["groq"] is None + + +def test_config_defaults_to_cerebras_with_parallel_groq_fallback_stage(monkeypatch): + monkeypatch.delenv("FASTVIDEO_PROMPT_PROVIDER", raising=False) + _set_required_prompt_keys(monkeypatch) + + module = _load_config_module() + + assert module.PROMPT_PROVIDER == "cerebras" + assert module.PROMPT_PROVIDER_RUNTIME_STAGES == ( + ("cerebras", "groq"), + ) + assert module.PROMPT_PROVIDER_PRIORITY == ( + "cerebras", + "groq", + ) + assert module.PROMPT_API_KEY == "cerebras-key" + assert module.PROMPT_API_BASE_URL is None + assert module.PROMPT_API_KEYS == { + "cerebras": "cerebras-key", + "groq": "groq-key", + } + assert module.PROMPT_API_BASE_URLS == { + "cerebras": None, + "groq": "https://api.groq.com/openai/v1", + } + assert module.PROMPT_MODEL == "gpt-oss-120b" + assert module.PROMPT_REWRITE_MODEL == "gpt-oss-120b" + assert module.PROMPT_REWRITE_MODEL_OPTIONS == ["gpt-oss-120b"] + assert module.PROMPT_PROVIDER_MODELS == { + "cerebras": "gpt-oss-120b", + "groq": "openai/gpt-oss-120b", + } + + +def test_config_ignores_legacy_groq_primary_override(monkeypatch): + monkeypatch.setenv("FASTVIDEO_PROMPT_PROVIDER", "groq") + _set_required_prompt_keys(monkeypatch) + + module = _load_config_module() + + assert module.PROMPT_PROVIDER == "cerebras" + assert module.PROMPT_PROVIDER_RUNTIME_STAGES == ( + ("cerebras", "groq"), + ) + assert module.PROMPT_PROVIDER_PRIORITY == ( + "cerebras", + "groq", + ) + assert module.PROMPT_API_KEY == "cerebras-key" + assert module.PROMPT_API_BASE_URL is None + + +def test_config_uses_local_overlay_paths_when_devtools_enabled(monkeypatch, tmp_path): + _set_required_prompt_keys(monkeypatch) + monkeypatch.setenv("FASTVIDEO_ENABLE_DEVTOOLS", "true") + monkeypatch.setenv("FASTVIDEO_DREAMVERSE_HOME", str(tmp_path / "dreamverse-state")) + + module = _load_config_module() + + assert module.DEVTOOLS_ENABLED is True + assert module.FRONTEND_ROOT.as_posix().endswith("apps/dreamverse/web") + assert module.PROMPT_ENHANCE_SYSTEM_PROMPT_PATH.endswith( + "dreamverse/prompts.local/next_segment_system_prompt.md" + ) + assert module.PROMPT_ENHANCE_SYSTEM_PROMPT_FALLBACK_PATH.endswith( + "dreamverse/prompts/next_segment_system_prompt.md" + ) + assert module.PROMPT_REWRITE_USER_SYSTEM_PROMPT_PATH.endswith( + "dreamverse/prompts.local/rewrite_user_system_prompt.md" + ) + assert module.PROMPT_REWRITE_USER_SYSTEM_PROMPT_FALLBACK_PATH.endswith( + "dreamverse/prompts/rewrite_user_system_prompt.md" + ) + assert module.CURATED_PRESETS_FILE_PATH.endswith( + "apps/dreamverse/web/prompts.local/selected_ltx2_continuation_story_presets.json" + ) + assert module.CURATED_PRESETS_FALLBACK_FILE_PATH.endswith( + "apps/dreamverse/web/prompts/selected_ltx2_continuation_story_presets.json" + ) + assert module.FRONTEND_STATIC_DIR_CANDIDATES == ( + str(module.FRONTEND_ROOT / "out"), + str(module.FRONTEND_ROOT / "dist"), + ) + + +def test_config_enables_prompt_safety_when_requested(monkeypatch): + _set_required_prompt_keys(monkeypatch) + monkeypatch.setenv("FASTVIDEO_ENABLE_PROMPT_SAFETY", "true") + + module = _load_config_module() + + assert module.PROMPT_SAFETY_ENABLED is True + + +def test_config_uses_five_minute_session_timeout(monkeypatch): + _set_required_prompt_keys(monkeypatch) + + module = _load_config_module() + + assert module.SESSION_TIMEOUT_SECONDS == 300 + + +def test_config_rejects_invalid_prompt_provider(monkeypatch): + monkeypatch.setenv("FASTVIDEO_PROMPT_PROVIDER", "unsupported") + _set_required_prompt_keys(monkeypatch) + + with pytest.raises(RuntimeError, match="Invalid FASTVIDEO_PROMPT_PROVIDER"): + _load_config_module() diff --git a/apps/dreamverse/dreamverse/tests/test_entrypoints.py b/apps/dreamverse/dreamverse/tests/test_entrypoints.py new file mode 100644 index 0000000000..06a6d3ae62 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_entrypoints.py @@ -0,0 +1,97 @@ +from __future__ import annotations +# pyright: reportAttributeAccessIssue=false, reportMissingImports=false + +import sys +import types + + +import dreamverse.main as server_main +import dreamverse.mock_server as mock_server + + +def _run_cli(module, monkeypatch, argv: list[str]) -> list[dict[str, object]]: + calls: list[dict[str, object]] = [] + uvicorn_stub = types.ModuleType("uvicorn") + + def run(app, host: str, port: int) -> None: + calls.append( + { + "app": app, + "host": host, + "port": port, + } + ) + + uvicorn_stub.run = run + monkeypatch.setitem(sys.modules, "uvicorn", uvicorn_stub) + monkeypatch.setattr("dreamverse._deps.require_dreamverse_runtime_deps", lambda: None) + monkeypatch.setattr(mock_server, "require_dreamverse_runtime_deps", lambda: None) + monkeypatch.setattr(sys, "argv", argv) + + module.cli() + return calls + + +def test_server_cli_defaults_to_local_web_port(monkeypatch): + calls = _run_cli(server_main, monkeypatch, ["dreamverse-server"]) + + assert calls == [ + { + "app": server_main.app, + "host": "0.0.0.0", + "port": 8009, + } + ] + + +def test_server_cli_allows_explicit_host_and_port(monkeypatch): + calls = _run_cli( + server_main, + monkeypatch, + ["dreamverse-server", "--host", "127.0.0.1", "--port", "8123"], + ) + + assert calls == [ + { + "app": server_main.app, + "host": "127.0.0.1", + "port": 8123, + } + ] + + +def test_mock_server_cli_defaults_to_local_web_port(monkeypatch): + calls = _run_cli( + mock_server, + monkeypatch, + ["dreamverse-mock-server"], + ) + + assert calls == [ + { + "app": mock_server.app, + "host": "0.0.0.0", + "port": 8009, + } + ] + + +def test_mock_server_cli_updates_latency(monkeypatch): + old_latency_ms = mock_server.LATENCY_MS + try: + calls = _run_cli( + mock_server, + monkeypatch, + ["dreamverse-mock-server", "--latency", "321", "--port", "8111"], + ) + + assert calls == [ + { + "app": mock_server.app, + "host": "0.0.0.0", + "port": 8111, + } + ] + assert mock_server.LATENCY_MS == 321 + finally: + mock_server.LATENCY_MS = old_latency_ms diff --git a/apps/dreamverse/dreamverse/tests/test_gpu_pool.py b/apps/dreamverse/dreamverse/tests/test_gpu_pool.py new file mode 100644 index 0000000000..e30814539e --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_gpu_pool.py @@ -0,0 +1,121 @@ +# pyright: reportAttributeAccessIssue=false +from __future__ import annotations + +import asyncio +import multiprocessing as mp +from types import SimpleNamespace + +import pytest + + +import dreamverse.gpu_pool as gpu_pool + + +def _child_consume_and_exit(cmd_q, resp_q): + """Top-level so the spawn context can pickle it. + + Signals startup by putting "READY" on resp_q (so the parent can + wait out spawn-import latency separately from the actual test + assertion), then consumes one command from cmd_q and exits without + putting anything else on resp_q. Simulates a worker that dies + mid-command — e.g. SIGQUIT'd after a fatal pipeline error. + """ + resp_q.put("READY") + cmd_q.get() + + +def test_get_available_gpus_defaults_to_single_detected_gpu(monkeypatch): + monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False) + monkeypatch.delenv("FASTVIDEO_GPU_COUNT", raising=False) + + def fake_run(*args, **kwargs): + del args, kwargs + return SimpleNamespace(returncode=0, stdout="0\n1\n2\n") + + monkeypatch.setattr(gpu_pool.subprocess, "run", fake_run) + + assert gpu_pool.get_available_gpus() == [0] + + +def test_get_available_gpus_respects_explicit_gpu_count(monkeypatch): + monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False) + monkeypatch.setenv("FASTVIDEO_GPU_COUNT", "2") + + def fake_run(*args, **kwargs): + del args, kwargs + return SimpleNamespace(returncode=0, stdout="0\n1\n2\n") + + monkeypatch.setattr(gpu_pool.subprocess, "run", fake_run) + + assert gpu_pool.get_available_gpus() == [0, 1] + + +def test_get_available_gpus_can_use_all_visible_devices(monkeypatch): + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "3,5,7") + monkeypatch.setenv("FASTVIDEO_GPU_COUNT", "all") + + assert gpu_pool.get_available_gpus() == [3, 5, 7] + + +def test_get_available_gpus_defaults_to_first_visible_device(monkeypatch): + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "3,5,7") + monkeypatch.delenv("FASTVIDEO_GPU_COUNT", raising=False) + + assert gpu_pool.get_available_gpus() == [3] + + +def test_get_available_gpus_rejects_invalid_gpu_count(monkeypatch): + monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising=False) + monkeypatch.setenv("FASTVIDEO_GPU_COUNT", "zero") + + with pytest.raises(RuntimeError, match="Invalid FASTVIDEO_GPU_COUNT"): + gpu_pool.get_available_gpus() + + +def test_send_command_raises_on_worker_death(): + """A worker that consumes a command and exits without replying must + surface as RuntimeError via sentinel detection, not after the long + queue timeout. + + The child signals startup with a "READY" message; we drain that + first so the wait_for budget bounds only the sentinel-detection + time, not spawn-import latency. + """ + ctx = mp.get_context("spawn") + cmd_q = ctx.Queue() + resp_q = ctx.Queue() + + proc = ctx.Process( + target=_child_consume_and_exit, args=(cmd_q, resp_q) + ) + proc.start() + + # Wait for the spawn child to fully boot. Allow generous time — + # this isn't what we're measuring. + ready = resp_q.get(timeout=30.0) + assert ready == "READY" + + async def runner(): + slot = gpu_pool.GPUSlot(gpu_id=0, cuda_device="0") + slot.process = proc + slot.command_queue = cmd_q + slot.response_queue = resp_q + + # Now the child is blocked in cmd_q.get(). Sending the cmd + # makes it exit ~immediately; sentinel must fire well before + # the queue timeout. + await asyncio.wait_for( + slot._send_command( + gpu_pool.Command(gpu_pool.CommandType.SHUTDOWN), + timeout=10.0, + ), + timeout=5.0, + ) + + try: + with pytest.raises(RuntimeError, match="worker died"): + asyncio.run(runner()) + finally: + proc.join(timeout=5) + cmd_q.close() + resp_q.close() diff --git a/apps/dreamverse/dreamverse/tests/test_import_contract.py b/apps/dreamverse/dreamverse/tests/test_import_contract.py new file mode 100644 index 0000000000..a11312e4d4 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_import_contract.py @@ -0,0 +1,56 @@ +import ast +from pathlib import Path + +ALLOWED_PREFIXES = ( + "fastvideo.api", + "fastvideo.entrypoints.streaming", + "fastvideo.entrypoints.video_generator", + "fastvideo.configs", +) +ALLOWED_EXACT = ("fastvideo",) +FORBIDDEN_PREFIXES = ( + "fastvideo.pipelines", + "fastvideo.models", + "fastvideo.layers", + "fastvideo.worker", + "fastvideo.fastvideo_args", +) +ALLOWED_INTERNAL_IMPORTS = { + ( + "video_generation.py", + "fastvideo.models.audio.ltx2_audio_processing", + ), + ( + "video_generation.py", + "fastvideo.models.loader.component_loader", + ), +} + + +def test_dreamverse_server_imports_only_public_fastvideo_surfaces() -> None: + root = Path(__file__).resolve().parents[1] + bad: list[tuple[str, int, str]] = [] + for path in root.rglob("*.py"): + if "/tests/" in path.as_posix(): + continue + try: + tree = ast.parse(path.read_text(), filename=str(path)) + except SyntaxError as task_exc: + raise AssertionError(f"Failed to parse {path}") from task_exc + for node in ast.walk(tree): + names = ( + [a.name for a in node.names] if isinstance(node, ast.Import) + else [node.module] if isinstance(node, ast.ImportFrom) and node.module + else [] + ) + for name in names: + if not name: + continue + rel_path = str(path.relative_to(root)) + if ( + name.startswith(FORBIDDEN_PREFIXES) + and (rel_path, name) not in ALLOWED_INTERNAL_IMPORTS + ): + bad.append((str(path.relative_to(root)), getattr(node, "lineno", 0), name)) + + assert bad == [], f"Forbidden internal imports: {bad}" diff --git a/apps/dreamverse/dreamverse/tests/test_mock_server.py b/apps/dreamverse/dreamverse/tests/test_mock_server.py new file mode 100644 index 0000000000..30730e0743 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_mock_server.py @@ -0,0 +1,355 @@ +# pyright: reportArgumentType=false +from __future__ import annotations + +import asyncio +import os + +from fastapi import WebSocketDisconnect + + +os.environ.setdefault("CEREBRAS_API_KEY", "dummy") +os.environ.setdefault("GROQ_API_KEY", "dummy") + +import dreamverse.mock_server as mock_server + + +class _FakeWebSocket: + def __init__(self, messages: list[tuple[float, dict[str, object]]]): + self._messages = messages + self._index = 0 + self.sent_json: list[dict[str, object]] = [] + self.sent_bytes: list[bytes] = [] + + async def accept(self) -> None: + return None + + async def send_json(self, payload: dict[str, object]) -> None: + self.sent_json.append(payload) + + async def send_bytes(self, payload: bytes) -> None: + self.sent_bytes.append(payload) + + async def close(self, code: int = 1000, reason: str | None = None) -> None: + del code, reason + + async def receive_json(self) -> dict[str, object]: + if self._index >= len(self._messages): + raise WebSocketDisconnect() + delay_s, payload = self._messages[self._index] + self._index += 1 + if delay_s > 0: + await asyncio.sleep(delay_s) + return payload + + +def test_mock_server_matches_current_single5s_protocol(): + old_segment_bytes = mock_server.MOCK_SEGMENT_BYTES + old_latency_ms = mock_server.LATENCY_MS + try: + mock_server.MOCK_SEGMENT_BYTES = b"mock-fmp4-bytes" + mock_server.LATENCY_MS = 1 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "simple_prompt_1", + "curated_prompts": ["selected prompt"], + "single_clip_mode": True, + "enhancement_enabled": False, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "simple_generate", + "preset_id": "simple_custom_prompt", + "prompt_id": "simple_custom_prompt", + "prompt": "custom prompt", + "enhancement_enabled": True, + "initial_image": None, + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(mock_server.websocket_endpoint(ws)) + + message_types = [payload["type"] for payload in ws.sent_json] + assert message_types[0] == "queue_status" + assert "gpu_assigned" in message_types + assert message_types.count("ltx2_stream_start") == 2 + assert "prompt_received" in message_types + assert "prompt_enhancing" in message_types + assert "prompt_ready" in message_types + assert "media_init" in message_types + assert "media_segment_complete" in message_types + assert message_types.count("ltx2_stream_complete") == 2 + assert "prompt_sources_blocked" not in message_types + + segment_start_events = [ + payload + for payload in ws.sent_json + if payload["type"] == "ltx2_segment_start" + ] + gpu_assigned_event = next( + payload for payload in ws.sent_json if payload["type"] == "gpu_assigned" + ) + assert gpu_assigned_event["session_timeout"] == mock_server.SESSION_TIMEOUT_SECONDS + assert [payload["segment_idx"] for payload in segment_start_events] == [1, 1] + assert segment_start_events[0]["prompt"] == "selected prompt" + assert segment_start_events[1]["prompt"] == "custom prompt" + + step_complete_events = [ + payload + for payload in ws.sent_json + if payload["type"] == "step_complete" + ] + assert len(step_complete_events) == 2 + assert step_complete_events[0]["latency_ms"] == { + "total": 121.0, + "worker_e2e": 1.0, + "main_user_step": 121.0, + "overhead": 120.0, + } + + assert ws.sent_bytes + assert ws.sent_bytes[0] == b"mock-fmp4-bytes" + finally: + mock_server.MOCK_SEGMENT_BYTES = old_segment_bytes + mock_server.LATENCY_MS = old_latency_ms + + +def test_mock_server_regular_cap_waits_for_rewrite_rollout(): + old_segment_bytes = mock_server.MOCK_SEGMENT_BYTES + old_latency_ms = mock_server.LATENCY_MS + old_generation_segment_cap = mock_server.GENERATION_SEGMENT_CAP + try: + mock_server.MOCK_SEGMENT_BYTES = b"mock-fmp4-bytes" + mock_server.LATENCY_MS = 1 + mock_server.GENERATION_SEGMENT_CAP = 1 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "test_preset", + "curated_prompts": ["segment one"], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.02, + { + "type": "rewrite_seed_prompts", + "rewrite_instruction": "start a new rollout", + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(mock_server.websocket_endpoint(ws)) + + message_types = [payload["type"] for payload in ws.sent_json] + assert message_types.count("ltx2_stream_start") == 2 + assert message_types.count("ltx2_stream_complete") == 2 + assert "generation_cap_reached" not in message_types + assert "prompt_sources_blocked" not in message_types + + segment_start_events = [ + payload + for payload in ws.sent_json + if payload["type"] == "ltx2_segment_start" + ] + assert [payload["segment_idx"] for payload in segment_start_events] == [1, 1] + assert segment_start_events[0]["prompt"] == "segment one" + assert segment_start_events[1]["prompt"] == "segment one [start a new rollout]" + finally: + mock_server.MOCK_SEGMENT_BYTES = old_segment_bytes + mock_server.LATENCY_MS = old_latency_ms + mock_server.GENERATION_SEGMENT_CAP = old_generation_segment_cap + + +def test_mock_server_rewrite_during_active_segment_restarts_from_first_rewritten_prompt(): + old_segment_bytes = mock_server.MOCK_SEGMENT_BYTES + old_latency_ms = mock_server.LATENCY_MS + try: + mock_server.MOCK_SEGMENT_BYTES = b"mock-fmp4-bytes" + mock_server.LATENCY_MS = 100 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "test_preset", + "curated_prompts": ["segment one", "segment two"], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.02, + { + "type": "rewrite_seed_prompts", + "rewrite_instruction": "restart from rewrite", + }, + ), + (0.40, {"type": "leave"}), + ] + ) + + asyncio.run(mock_server.websocket_endpoint(ws)) + + segment_start_events = [ + payload + for payload in ws.sent_json + if payload["type"] == "ltx2_segment_start" + ] + assert [payload["prompt"] for payload in segment_start_events[:2]] == [ + "segment one", + "segment one [restart from rewrite]", + ] + assert all( + payload["prompt"] != "segment two" + for payload in segment_start_events[1:] + ) + reset_events = [ + payload + for payload in ws.sent_json + if payload.get("type") == "seed_prompts_reset_applied" + ] + assert any( + payload.get("reason") == "rewrite_during_generation" + for payload in reset_events + ) + finally: + mock_server.MOCK_SEGMENT_BYTES = old_segment_bytes + mock_server.LATENCY_MS = old_latency_ms + + +def test_mock_server_supports_initial_custom_rollout_prompt(): + old_segment_bytes = mock_server.MOCK_SEGMENT_BYTES + old_latency_ms = mock_server.LATENCY_MS + try: + mock_server.MOCK_SEGMENT_BYTES = b"mock-fmp4-bytes" + mock_server.LATENCY_MS = 1 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "custom_editable", + "preset_label": "Custom rollout", + "curated_prompts": [], + "initial_rollout_prompt": "A moonbase corridor thriller with flooding", + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(mock_server.websocket_endpoint(ws)) + + message_types = [payload["type"] for payload in ws.sent_json] + assert "rewrite_seed_prompts_started" in message_types + assert "seed_prompts_updated" in message_types + assert "rewrite_seed_prompts_complete" in message_types + assert "seed_prompts_reset_applied" in message_types + assert "ltx2_stream_start" in message_types + assert "prompt_sources_blocked" not in message_types + + segment_start_events = [ + payload + for payload in ws.sent_json + if payload["type"] == "ltx2_segment_start" + ] + assert segment_start_events + assert segment_start_events[0]["prompt"] == ( + "A moonbase corridor thriller with flooding [segment 1]" + ) + finally: + mock_server.MOCK_SEGMENT_BYTES = old_segment_bytes + mock_server.LATENCY_MS = old_latency_ms + + +def test_mock_server_can_start_new_project_without_reconnecting(): + old_segment_bytes = mock_server.MOCK_SEGMENT_BYTES + old_latency_ms = mock_server.LATENCY_MS + try: + mock_server.MOCK_SEGMENT_BYTES = b"mock-fmp4-bytes" + mock_server.LATENCY_MS = 40 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "test_preset", + "curated_prompts": ["segment one"], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.02, {"type": "end_project_keep_session"}), + ( + 0.20, + { + "type": "project_init_v1", + "preset_id": "test_preset_2", + "preset_label": "Test Preset 2", + "curated_prompts": ["segment two"], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.40, {"type": "leave"}), + ] + ) + + asyncio.run(mock_server.websocket_endpoint(ws)) + + message_types = [payload["type"] for payload in ws.sent_json] + assert message_types.count("gpu_assigned") == 1 + assert message_types.count("ltx2_stream_start") == 2 + assert "project_idle" in message_types + + project_idle_index = message_types.index("project_idle") + stream_start_indexes = [ + index for index, message_type in enumerate(message_types) + if message_type == "ltx2_stream_start" + ] + assert stream_start_indexes[0] < project_idle_index < stream_start_indexes[1] + + segment_start_events = [ + payload + for payload in ws.sent_json + if payload["type"] == "ltx2_segment_start" + ] + assert [payload["prompt"] for payload in segment_start_events[:2]] == [ + "segment one", + "segment two", + ] + finally: + mock_server.MOCK_SEGMENT_BYTES = old_segment_bytes + mock_server.LATENCY_MS = old_latency_ms diff --git a/apps/dreamverse/dreamverse/tests/test_prompt_enhancer.py b/apps/dreamverse/dreamverse/tests/test_prompt_enhancer.py new file mode 100644 index 0000000000..52552b16a7 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_prompt_enhancer.py @@ -0,0 +1,1171 @@ +# pyright: reportMissingTypeArgument=false, reportArgumentType=false, reportOptionalSubscript=false, reportOperatorIssue=false +from __future__ import annotations + +import asyncio +import os +import re +import time + + +os.environ.setdefault("CEREBRAS_API_KEY", "dummy") +os.environ.setdefault("GROQ_API_KEY", "dummy") + +import dreamverse.prompt_enhancer as prompt_enhancer_module +from dreamverse.prompt_enhancer import ( + ProviderRuntime, + PromptEnhancer, + _build_prompt_backup_path, + _load_prompt_required, + _parse_json_response, + _resolve_prompt_save_path, +) + + +class _FakeResponse: + def __init__(self, payload: dict): + self._payload = payload + + def model_dump(self, mode: str = "json"): # noqa: ARG002 + return self._payload + + +class _FakeSyncCompletions: + def __init__(self, payload: dict): + self._payload = payload + + def create(self, **kwargs): # noqa: ARG002 + return _FakeResponse(self._payload) + + +class _FakeSyncClient: + def __init__(self, payload: dict): + self.chat = type( + "_FakeChat", + (), + {"completions": _FakeSyncCompletions(payload)}, + )() + + +class _DelayedSyncCompletions: + def __init__(self, payload: dict, delay_s: float = 0.0, exc: Exception | None = None): + self._payload = payload + self._delay_s = delay_s + self._exc = exc + + def create(self, **kwargs): # noqa: ARG002 + if self._delay_s > 0: + time.sleep(self._delay_s) + if self._exc is not None: + raise self._exc + return _FakeResponse(self._payload) + + +class _DelayedSyncClient: + def __init__(self, payload: dict, delay_s: float = 0.0, exc: Exception | None = None): + self.chat = type( + "_FakeChat", + (), + { + "completions": _DelayedSyncCompletions( + payload, + delay_s=delay_s, + exc=exc, + ) + }, + )() + + +def _chat_payload_with_content(content: str) -> dict: + return { + "choices": [ + { + "message": { + "content": content, + } + } + ] + } + + +def _build_test_enhancer(payload: dict) -> PromptEnhancer: + enhancer = PromptEnhancer.__new__(PromptEnhancer) + enhancer.provider = "cerebras" + enhancer.provider_label = "cerebras" + enhancer.model = "gpt-test" + enhancer.rewrite_default_model = "gpt-test" + enhancer.rewrite_model_options = ["gpt-test"] + enhancer.default_timeout_ms = 1200 + enhancer.http_timeout_ms = 1200 + enhancer.temperature = 0.4 + enhancer.rewrite_default_temperature = 0.4 + enhancer.max_completion_tokens = 512 + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt_fallback_path = None + enhancer.rewrite_user_system_prompt_path = "/tmp/rewrite_user_system_prompt.md" + enhancer.rewrite_user_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt = "system prompt" + enhancer.rewrite_user_system_prompt = "system prompt" + enhancer.provider_request_models = { + "cerebras": "gpt-test", + "groq": "openai/gpt-test", + } + enhancer.provider_success_counts = { + "cerebras": 0, + "groq": 0, + } + enhancer.client = _FakeSyncClient(payload) + return enhancer + + +def _build_staged_enhancer( + *, + cerebras_payload: dict, + groq_payload: dict, + cerebras_request_model: str = "gpt-test", + groq_request_model: str = "openai/gpt-test", + cerebras_delay_s: float = 0.0, + groq_delay_s: float = 0.0, + cerebras_exc: Exception | None = None, + groq_exc: Exception | None = None, +) -> PromptEnhancer: + enhancer = _build_test_enhancer(cerebras_payload) + enhancer.provider_request_models = { + "cerebras": cerebras_request_model, + "groq": groq_request_model, + } + enhancer.enhance_system_prompt = "enhance prompt" + enhancer.auto_system_prompt = "auto prompt" + enhancer.provider_runtimes = [ + ProviderRuntime( + name="cerebras", + api_key="cerebras-key", + api_base_url=None, + request_model=cerebras_request_model, + client=_DelayedSyncClient( + cerebras_payload, + delay_s=cerebras_delay_s, + exc=cerebras_exc, + ), + ), + ProviderRuntime( + name="groq", + api_key="groq-key", + api_base_url="https://api.groq.com/openai/v1", + request_model=groq_request_model, + client=_DelayedSyncClient( + groq_payload, + delay_s=groq_delay_s, + exc=groq_exc, + ), + ), + ] + enhancer.provider_runtime_stages = [ + list(enhancer.provider_runtimes), + ] + enhancer.provider_success_counts = { + "cerebras": 0, + "groq": 0, + } + enhancer.client = enhancer.provider_runtimes[0].client + return enhancer + + +class _FakeOpenAIClient: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.chat = type( + "_FakeChat", + (), + {"completions": _FakeSyncCompletions(_chat_payload_with_content("{}"))}, + )() + + +class _FakeCerebrasClient: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.chat = type( + "_FakeChat", + (), + {"completions": _FakeSyncCompletions(_chat_payload_with_content("{}"))}, + )() + + +def test_parse_json_response_accepts_fenced_json_with_prose(): + parsed = _parse_json_response( + "Here is the rewrite:\n```json\n{\"segment_prompts\":[\"A\",\"B\"]}\n```\nThanks." + ) + assert parsed == {"segment_prompts": ["A", "B"]} + + +def test_parse_json_response_extracts_first_embedded_object(): + parsed = _parse_json_response( + "Model output:\n{\"segment_prompts\":[\"A\",\"B\"]}\n(complete)" + ) + assert parsed == {"segment_prompts": ["A", "B"]} + + +def test_load_prompt_required_falls_back_to_default_path(tmp_path): + fallback_path = tmp_path / "next_segment_system_prompt.md" + fallback_path.write_text("fallback prompt\n", encoding="utf-8") + + loaded_prompt = _load_prompt_required( + str(tmp_path / "prompts.local" / "next_segment_system_prompt.md"), + "next-segment", + str(fallback_path), + ) + + assert loaded_prompt == "fallback prompt" + + +def test_load_prompt_required_unwraps_python_style_system_prompt_assignment(tmp_path): + prompt_path = tmp_path / "rewrite_window_system_prompt.md" + prompt_path.write_text( + 'SYSTEM_PROMPT = """\nfirst line\nsecond line\n"""\n', + encoding="utf-8", + ) + + loaded_prompt = _load_prompt_required( + str(prompt_path), + "rewrite-window", + ) + + assert loaded_prompt == "first line\nsecond line" + + +def test_build_client_supports_cerebras_provider(monkeypatch): + monkeypatch.setattr(prompt_enhancer_module, "Cerebras", _FakeCerebrasClient) + + enhancer = PromptEnhancer.__new__(PromptEnhancer) + enhancer.provider = "cerebras" + enhancer.api_key = "cerebras-key" + enhancer.api_base_url = None + + client = PromptEnhancer._build_client(enhancer) + + assert isinstance(client, _FakeCerebrasClient) + assert client.kwargs == { + "api_key": "cerebras-key", + } + + +def test_build_client_supports_groq_provider(monkeypatch): + monkeypatch.setattr(prompt_enhancer_module, "OpenAI", _FakeOpenAIClient) + + enhancer = PromptEnhancer.__new__(PromptEnhancer) + enhancer.provider = "groq" + enhancer.api_key = "groq-key" + enhancer.api_base_url = "https://api.groq.com/openai/v1" + + client = PromptEnhancer._build_client(enhancer) + + assert isinstance(client, _FakeOpenAIClient) + assert client.kwargs == { + "api_key": "groq-key", + "base_url": "https://api.groq.com/openai/v1", + } + + +def test_rewrite_prompt_sequence_accepts_segment_prompts_output(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + rewrite_instruction="make it cinematic", + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.rollout_id == "preset_a" + assert result.rollout_label == "Preset A" + assert result.prompts == ["A", "B"] + + +def test_rewrite_prompt_sequence_accepts_legacy_rewritten_prompts_output(): + enhancer = _build_test_enhancer( + _chat_payload_with_content('{"rewritten_prompts":["A","B"]}') + ) + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + rewrite_instruction="make it cinematic", + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.rollout_id == "current_rollout" + assert result.rollout_label == "Current rollout" + assert result.prompts == ["A", "B"] + + +def test_rewrite_prompt_sequence_accepts_segment_dicts_without_top_level_rollout_metadata(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"segments":[{"prompt":"A"},{"text":"B"}]}' + ) + ) + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + preset_id="preset_a", + preset_label="Preset A", + rewrite_instruction="make it cinematic", + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.rollout_id == "preset_a" + assert result.rollout_label == "Preset A" + assert result.prompts == ["A", "B"] + + +def test_rewrite_prompt_sequence_accepts_numbered_prose_output(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + "The user is asking for a cinematic rewrite.\n\n" + "1. A dog bounds across the moon's dusty surface, kicking up silver regolith as it chases a rabbit beneath the black sky.\n" + "2. The rabbit darts around a crater rim while the dog lunges after it, Earth glowing blue in the distance.\n" + ) + ) + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + rewrite_instruction="make it cinematic", + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.rollout_id == "current_rollout" + assert result.rollout_label == "Current rollout" + assert result.prompts == [ + "A dog bounds across the moon's dusty surface, kicking up silver regolith as it chases a rabbit beneath the black sky.", + "The rabbit darts around a crater rim while the dog lunges after it, Earth glowing blue in the distance.", + ] + + +def test_enhance_prompt_prefers_cerebras_before_groq_fallback(): + enhancer = _build_staged_enhancer( + cerebras_payload=_chat_payload_with_content('{"prompt":"Cerebras prompt"}'), + groq_payload=_chat_payload_with_content('{"prompt":"Groq prompt"}'), + cerebras_delay_s=0.01, + groq_delay_s=0.01, + ) + + result = asyncio.run( + enhancer.enhance_prompt( + "A rainy alley at night", + mode="single_clip", + ) + ) + + assert result.fallback_used is False + assert result.error is None + assert result.provider == "cerebras" + assert result.model == "gpt-test" + assert result.prompt == "Cerebras prompt" + assert enhancer.get_provider_success_counts() == { + "cerebras": 1, + "groq": 0, + } + + +def test_enhance_prompt_uses_groq_when_cerebras_fails(): + enhancer = _build_staged_enhancer( + cerebras_payload=_chat_payload_with_content("{}"), + groq_payload=_chat_payload_with_content('{"prompt":"Groq prompt"}'), + cerebras_exc=RuntimeError("cerebras overloaded"), + groq_delay_s=0.01, + ) + + result = asyncio.run( + enhancer.enhance_prompt( + "A rainy alley at night", + mode="single_clip", + ) + ) + + assert result.fallback_used is False + assert result.error is None + assert result.provider == "groq" + assert result.prompt == "Groq prompt" + assert enhancer.get_provider_success_counts() == { + "cerebras": 0, + "groq": 1, + } + + +def test_enhance_prompt_can_use_groq_when_cerebras_times_out(): + enhancer = _build_staged_enhancer( + cerebras_payload=_chat_payload_with_content('{"prompt":"Slow Cerebras prompt"}'), + groq_payload=_chat_payload_with_content('{"prompt":"Groq prompt"}'), + cerebras_delay_s=0.4, + groq_delay_s=0.01, + ) + enhancer.http_timeout_ms = 50 + enhancer.default_timeout_ms = 50 + + result = asyncio.run( + enhancer.enhance_prompt( + "A rainy alley at night", + mode="single_clip", + timeout_ms=50, + ) + ) + + assert result.fallback_used is False + assert result.error is None + assert result.provider == "groq" + assert result.prompt == "Groq prompt" + assert enhancer.get_provider_success_counts() == { + "cerebras": 0, + "groq": 1, + } + + +def test_enhance_prompt_can_use_cerebras_when_it_returns_first(): + enhancer = _build_staged_enhancer( + cerebras_payload=_chat_payload_with_content('{"prompt":"Primary Cerebras prompt"}'), + groq_payload=_chat_payload_with_content('{"prompt":"Groq prompt"}'), + cerebras_delay_s=0.01, + groq_delay_s=0.08, + ) + + result = asyncio.run( + enhancer.enhance_prompt( + "A rainy alley at night", + mode="single_clip", + ) + ) + + assert result.fallback_used is False + assert result.error is None + assert result.provider == "cerebras" + assert result.model == "gpt-test" + assert result.prompt == "Primary Cerebras prompt" + assert enhancer.get_provider_success_counts() == { + "cerebras": 1, + "groq": 0, + } + + +def test_rewrite_prompt_sequence_keeps_raw_output_on_parse_error(): + enhancer = _build_test_enhancer( + _chat_payload_with_content("I cannot comply with JSON right now.") + ) + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + rewrite_instruction="make it cinematic", + ) + ) + assert result.fallback_used is True + assert "No JSON object found in assistant response." in (result.error or "") + assert result.raw_response_text == "I cannot comply with JSON right now." + assert result.rollout_id == "current_rollout" + assert result.rollout_label == "Current rollout" + assert result.prompts == ["prompt one", "prompt two"] + + +def test_rewrite_prompt_sequence_uses_current_rollout_payload_shape(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"rewritten_rollout","label":"Rewritten Rollout","segment_prompts":["A","B"]}' + ) + ) + captured = { + "body": None, + "timeout_seconds": None, + } + + async def _fake_request_content_with_body(*, body: dict, timeout_seconds: float): + captured["body"] = body + captured["timeout_seconds"] = timeout_seconds + return ( + _chat_payload_with_content( + '{"id":"rewritten_rollout","label":"Rewritten Rollout","segment_prompts":["A","B"]}' + ), + '{"id":"rewritten_rollout","label":"Rewritten Rollout","segment_prompts":["A","B"]}', + ) + + enhancer._request_content_with_body = _fake_request_content_with_body # type: ignore[attr-defined] + + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + preset_id="preset_a", + preset_label="Preset A", + rewrite_instruction="make it cinematic", + rewrite_model="gpt-test", + rewrite_temperature=0.2, + timeout_ms=800, + ) + ) + + assert result.fallback_used is False + assert captured["body"]["messages"][0] == { + "role": "system", + "content": "system prompt", + } + assert captured["body"]["messages"][1]["role"] == "user" + assert prompt_enhancer_module.json.loads(captured["body"]["messages"][1]["content"]) == { + "mode": "edit_existing_rollout", + "request": ( + "Rewrite all segment prompts with improved continuity and cinematic detail. " + "Keep count and ordering identical." + ), + "user_instruction": "make it cinematic", + "current_rollout": { + "id": "preset_a", + "label": "Preset A", + "segment_prompts": ["prompt one", "prompt two"], + }, + } + + +def test_rewrite_prompt_sequence_supports_new_rollout_mode(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"custom_editable","label":"Custom rollout","segment_prompts":[' + '"A","B","C","D","E","F"]}' + ) + ) + captured = { + "body": None, + } + + async def _fake_request_content_with_body(*, body: dict, timeout_seconds: float): + del timeout_seconds + captured["body"] = body + return ( + _chat_payload_with_content( + '{"id":"custom_editable","label":"Custom rollout","segment_prompts":[' + '"A","B","C","D","E","F"]}' + ), + '{"id":"custom_editable","label":"Custom rollout","segment_prompts":[' + '"A","B","C","D","E","F"]}', + ) + + enhancer._request_content_with_body = _fake_request_content_with_body # type: ignore[attr-defined] + + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + [], + preset_id="custom_editable", + preset_label="Custom rollout", + rewrite_instruction="A moonbase corridor thriller with flooding and red alarms", + rewrite_model="gpt-test", + rewrite_temperature=0.2, + timeout_ms=800, + ) + ) + + assert result.fallback_used is False + assert result.prompts == ["A", "B", "C", "D", "E", "F"] + assert prompt_enhancer_module.json.loads(captured["body"]["messages"][1]["content"]) == { + "mode": "new_rollout", + "request": ( + "Rewrite all segment prompts with improved continuity and cinematic detail. " + "Keep count and ordering identical." + ), + "user_instruction": "A moonbase corridor thriller with flooding and red alarms", + "desired_segment_count": 6, + "rollout_id_hint": "custom_editable", + "rollout_label_hint": "Custom rollout", + } + + +def test_rewrite_prompt_sequence_uses_session_override_system_prompt(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + enhancer.rewrite_all_system_prompt = "shared system prompt" + captured = { + "body": None, + } + + async def _fake_request_content_with_body(*, body: dict, timeout_seconds: float): + del timeout_seconds + captured["body"] = body + return ( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ), + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}', + ) + + enhancer._request_content_with_body = _fake_request_content_with_body # type: ignore[attr-defined] + + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + preset_id="preset_a", + preset_label="Preset A", + rewrite_instruction="make it cinematic", + rewrite_model="gpt-test", + system_prompt_override="session specific system prompt", + ) + ) + + assert result.fallback_used is False + assert captured["body"]["messages"][0] == { + "role": "system", + "content": "session specific system prompt", + } + + +def test_resolve_rewrite_new_rollout_system_prompt_uses_dedicated_prompt(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + enhancer.rewrite_all_system_prompt = "shared rewrite system prompt" + enhancer.rewrite_user_system_prompt = "new rollout rewrite system prompt" + + resolved = enhancer.resolve_rewrite_new_rollout_system_prompt(None) + + assert resolved == "new rollout rewrite system prompt" + + +def test_resolve_rewrite_new_rollout_system_prompt_prefers_override(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + enhancer.rewrite_all_system_prompt = "shared rewrite system prompt" + enhancer.rewrite_user_system_prompt = "new rollout rewrite system prompt" + + resolved = enhancer.resolve_rewrite_new_rollout_system_prompt( + "session specific system prompt" + ) + + assert resolved == "session specific system prompt" + + +def test_generate_auto_prompt_uses_selected_model(): + enhancer = _build_test_enhancer( + _chat_payload_with_content('{"next_prompt":"Auto next"}') + ) + enhancer.auto_system_prompt = "auto system prompt" + enhancer.rewrite_model_options = ["gpt-test", "gpt-alt"] + enhancer.rewrite_default_model = "gpt-test" + captured_model = {"value": None} + + async def _fake_request_content( + *, + system_prompt: str, + user_payload: dict, + timeout_ms: int, + model: str | None = None, + temperature: float | None = None, + ): + captured_model["value"] = model + return ( + _chat_payload_with_content('{"next_prompt":"Auto next"}'), + '{"next_prompt":"Auto next"}', + ) + + enhancer._request_content = _fake_request_content # type: ignore[attr-defined] + + result = asyncio.run( + enhancer.generate_auto_prompt( + locked_segments=["One"], + next_segment_idx=2, + model="gpt-alt", + timeout_ms=800, + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.prompt == "Auto next" + assert captured_model["value"] == "gpt-alt" + assert result.model == "gpt-alt" + + +def test_enhance_prompt_uses_selected_model(): + enhancer = _build_test_enhancer( + _chat_payload_with_content('{"next_prompt":"Enhanced next"}') + ) + enhancer.enhance_system_prompt = "enhance system prompt" + enhancer.auto_system_prompt = "auto system prompt" + enhancer.rewrite_model_options = ["gpt-test", "gpt-alt"] + enhancer.rewrite_default_model = "gpt-test" + captured_model = {"value": None} + + async def _fake_request_content( + *, + system_prompt: str, + user_payload: dict, + timeout_ms: int, + model: str | None = None, + temperature: float | None = None, + ): + captured_model["value"] = model + return ( + _chat_payload_with_content('{"next_prompt":"Enhanced next"}'), + '{"next_prompt":"Enhanced next"}', + ) + + enhancer._request_content = _fake_request_content # type: ignore[attr-defined] + + result = asyncio.run( + enhancer.enhance_prompt( + "A user live prompt", + locked_segments=["Segment 1"], + next_segment_idx=2, + model="gpt-alt", + timeout_ms=800, + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.prompt == "Enhanced next" + assert captured_model["value"] == "gpt-alt" + assert result.model == "gpt-alt" + + +def test_enhance_prompt_single_clip_uses_auto_extension_prompt_and_prompt_field(): + enhancer = _build_test_enhancer( + _chat_payload_with_content('{"prompt":"Extended single clip"}') + ) + enhancer.enhance_system_prompt = "enhance system prompt" + enhancer.auto_system_prompt = "auto system prompt" + enhancer.rewrite_model_options = ["gpt-test", "gpt-alt"] + enhancer.rewrite_default_model = "gpt-test" + captured = { + "model": None, + "system_prompt": None, + "user_payload": None, + } + + async def _fake_request_content( + *, + system_prompt: str, + user_payload: dict, + timeout_ms: int, + model: str | None = None, + temperature: float | None = None, + ): + del timeout_ms, temperature + captured["model"] = model + captured["system_prompt"] = system_prompt + captured["user_payload"] = user_payload + return ( + _chat_payload_with_content('{"prompt":"Extended single clip"}'), + '{"prompt":"Extended single clip"}', + ) + + enhancer._request_content = _fake_request_content # type: ignore[attr-defined] + + result = asyncio.run( + enhancer.enhance_prompt( + "short 5s idea", + mode="single_clip", + model="gpt-alt", + timeout_ms=800, + ) + ) + assert result.fallback_used is False + assert result.error is None + assert result.prompt == "Extended single clip" + assert result.model == "gpt-alt" + assert captured["model"] == "gpt-alt" + assert captured["system_prompt"] == "auto system prompt" + assert captured["user_payload"] == { + "request": ( + "Expand the user prompt into one detailed prompt for a " + "single 5-second LTX-2.3 video clip. Respond with " + 'valid JSON only as {"prompt": "..."}.' # noqa: E501 + ), + "user_prompt": "short 5s idea", + } + + +def test_enhance_prompt_single_clip_rejects_plain_text_response(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + "Medium shot of a woman by a rainy cafe window as she lifts her " + "phone, exhales softly, and the camera makes a slow push in." + ) + ) + enhancer.auto_system_prompt = "auto system prompt" + + result = asyncio.run( + enhancer.enhance_prompt( + "woman in a cafe looking at her phone", + mode="single_clip", + model="gpt-test", + timeout_ms=800, + ) + ) + assert result.fallback_used is True + assert "No JSON object found in assistant response." in result.error + assert result.prompt == "" + + +def test_enhance_prompt_single_clip_rejects_segment_prompts_json(): + enhancer = _build_test_enhancer( + _chat_payload_with_content('{"segment_prompts":["A","B"]}') + ) + enhancer.auto_system_prompt = "auto system prompt" + enhancer.rewrite_model_options = ["gpt-test"] + enhancer.rewrite_default_model = "gpt-test" + + result = asyncio.run( + enhancer.enhance_prompt( + "short 5s idea", + mode="single_clip", + model="gpt-test", + timeout_ms=800, + ) + ) + assert result.fallback_used is True + assert result.prompt == "" + assert "Missing prompt string." in (result.error or "") + + +def test_enhance_prompt_requires_json_and_does_not_fallback_to_raw_text(): + enhancer = _build_test_enhancer( + _chat_payload_with_content("A cinematic continuation with slow dolly movement.") + ) + enhancer.enhance_system_prompt = "enhance system prompt" + enhancer.rewrite_model_options = ["gpt-test"] + enhancer.rewrite_default_model = "gpt-test" + + result = asyncio.run( + enhancer.enhance_prompt( + "raw input prompt", + locked_segments=["Segment 1"], + next_segment_idx=2, + model="gpt-test", + timeout_ms=800, + ) + ) + assert result.fallback_used is True + assert result.prompt == "" + assert "No JSON object found in assistant response." in (result.error or "") + + +def test_generate_auto_prompt_requires_json_and_does_not_fallback_to_raw_text(): + enhancer = _build_test_enhancer( + _chat_payload_with_content("A calm, grounded continuation with subtle motion.") + ) + enhancer.auto_system_prompt = "auto system prompt" + enhancer.rewrite_model_options = ["gpt-test"] + enhancer.rewrite_default_model = "gpt-test" + + result = asyncio.run( + enhancer.generate_auto_prompt( + locked_segments=["Segment 1"], + next_segment_idx=2, + model="gpt-test", + timeout_ms=800, + ) + ) + assert result.fallback_used is True + assert result.prompt == "" + assert "No JSON object found in assistant response." in (result.error or "") + + +def test_rewrite_prompt_sequence_includes_raw_json_when_content_empty(): + enhancer = _build_test_enhancer( + { + "choices": [ + { + "finish_reason": "length", + "message": { + "content": [], + "refusal": None, + }, + } + ], + "usage": {"completion_tokens": 0}, + } + ) + result = asyncio.run( + enhancer.rewrite_prompt_sequence( + ["prompt one", "prompt two"], + rewrite_instruction="make it cinematic", + ) + ) + assert result.fallback_used is True + assert "No rewrite segment prompts found in assistant response." in (result.error or "") + assert isinstance(result.raw_response_text, str) + assert '"finish_reason": "length"' in result.raw_response_text + + +def test_get_rewrite_model_config_returns_fixed_defaults(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + enhancer.rewrite_default_model = "gpt-oss-120b" + enhancer.rewrite_model_options = ["gpt-oss-120b"] + + config = enhancer.get_rewrite_model_config() + + assert config["default_model"] == "gpt-oss-120b" + assert config["options"] == ["gpt-oss-120b"] + + +def test_get_prompt_config_includes_auto_extension_prompt(): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + enhancer.enhance_system_prompt_path = "/tmp/next.md" + enhancer.auto_system_prompt_path = "/tmp/auto.md" + enhancer.rewrite_all_system_prompt_path = "/tmp/rewrite.md" + enhancer.rewrite_user_system_prompt_path = "/tmp/rewrite_user.md" + enhancer.enhance_system_prompt = "next prompt" + enhancer.auto_system_prompt = "auto prompt" + enhancer.rewrite_all_system_prompt = "rewrite prompt" + enhancer.rewrite_user_system_prompt = "rewrite user prompt" + + config = enhancer.get_prompt_config() + + assert config["next_segment_system_prompt_path"] == "/tmp/next.md" + assert config["auto_extension_system_prompt_path"] == "/tmp/auto.md" + assert config["rewrite_window_system_prompt_path"] == "/tmp/rewrite.md" + assert config["rewrite_user_system_prompt_path"] == "/tmp/rewrite_user.md" + assert config["next_segment_system_prompt"] == "next prompt" + assert config["auto_extension_system_prompt"] == "auto prompt" + assert config["rewrite_window_system_prompt"] == "rewrite prompt" + assert config["rewrite_user_system_prompt"] == "rewrite user prompt" + assert config["rewrite_model"] == "gpt-test" + assert config["rewrite_model_options"] == ["gpt-test"] + assert config["rewrite_temperature"] == 0.4 + + +def test_get_prompt_config_reports_loaded_fallback_prompt_path(tmp_path): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + rewrite_fallback_path = tmp_path / "rewrite_window_system_prompt.md" + rewrite_fallback_path.write_text("rewrite prompt\n", encoding="utf-8") + next_path = tmp_path / "next.md" + next_path.write_text("next prompt\n", encoding="utf-8") + auto_path = tmp_path / "auto.md" + auto_path.write_text("auto prompt\n", encoding="utf-8") + enhancer.rewrite_all_system_prompt_path = str( + tmp_path / "prompts.local" / "rewrite_window_system_prompt.md" + ) + enhancer.rewrite_all_system_prompt_fallback_path = str(rewrite_fallback_path) + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.reload_system_prompts() + + config = enhancer.get_prompt_config() + + assert config["rewrite_window_system_prompt_path"] == str(rewrite_fallback_path) + + +def test_reload_system_prompts_falls_back_to_rewrite_window_when_user_prompt_empty( + tmp_path, +): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + next_path = tmp_path / "next.md" + auto_path = tmp_path / "auto.md" + rewrite_path = tmp_path / "rewrite_window_system_prompt.md" + rewrite_user_path = tmp_path / "rewrite_user_system_prompt.md" + next_path.write_text("next prompt\n", encoding="utf-8") + auto_path.write_text("auto prompt\n", encoding="utf-8") + rewrite_path.write_text("rewrite window prompt\n", encoding="utf-8") + rewrite_user_path.write_text("\n", encoding="utf-8") + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.rewrite_all_system_prompt_path = str(rewrite_path) + enhancer.rewrite_user_system_prompt_path = str(rewrite_user_path) + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt_fallback_path = None + enhancer.rewrite_user_system_prompt_fallback_path = None + + enhancer.reload_system_prompts() + + assert enhancer.rewrite_all_system_prompt == "rewrite window prompt" + assert enhancer.rewrite_user_system_prompt == "rewrite window prompt" + assert enhancer.rewrite_user_system_prompt_source_path == str(rewrite_path) + + +def test_save_prompt_config_updates_auto_extension_prompt(tmp_path): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + next_path = tmp_path / "next.md" + auto_path = tmp_path / "auto.md" + rewrite_path = tmp_path / "rewrite.md" + next_path.write_text("next original\n", encoding="utf-8") + auto_path.write_text("auto original\n", encoding="utf-8") + rewrite_path.write_text("rewrite original\n", encoding="utf-8") + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.rewrite_all_system_prompt_path = str(rewrite_path) + + config = enhancer.save_prompt_config( + auto_extension_system_prompt="auto updated", + ) + + assert auto_path.read_text(encoding="utf-8").strip() == "auto updated" + assert config["auto_extension_system_prompt"] == "auto updated" + + +def test_save_prompt_config_updates_rewrite_user_prompt(tmp_path): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + next_path = tmp_path / "next.md" + auto_path = tmp_path / "auto.md" + rewrite_path = tmp_path / "rewrite.md" + rewrite_user_path = tmp_path / "rewrite_user.md" + next_path.write_text("next original\n", encoding="utf-8") + auto_path.write_text("auto original\n", encoding="utf-8") + rewrite_path.write_text("rewrite original\n", encoding="utf-8") + rewrite_user_path.write_text("rewrite user original\n", encoding="utf-8") + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.rewrite_all_system_prompt_path = str(rewrite_path) + enhancer.rewrite_user_system_prompt_path = str(rewrite_user_path) + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt_fallback_path = None + enhancer.rewrite_user_system_prompt_fallback_path = None + + config = enhancer.save_prompt_config( + rewrite_user_system_prompt="rewrite user updated", + ) + + assert rewrite_user_path.read_text(encoding="utf-8").strip() == "rewrite user updated" + assert config["rewrite_user_system_prompt"] == "rewrite user updated" + + +def test_save_prompt_config_updates_rewrite_model(tmp_path): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + next_path = tmp_path / "next.md" + auto_path = tmp_path / "auto.md" + rewrite_path = tmp_path / "rewrite.md" + next_path.write_text("next prompt\n", encoding="utf-8") + auto_path.write_text("auto prompt\n", encoding="utf-8") + rewrite_path.write_text("rewrite prompt\n", encoding="utf-8") + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.rewrite_all_system_prompt_path = str(rewrite_path) + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt_fallback_path = None + enhancer.rewrite_default_model = "gpt-test" + enhancer.rewrite_model_options = ["gpt-test", "gpt-alt"] + + config = enhancer.save_prompt_config( + rewrite_model="gpt-alt", + ) + + assert enhancer.rewrite_default_model == "gpt-alt" + assert config["rewrite_model"] == "gpt-alt" + assert config["rewrite_model_options"] == ["gpt-test", "gpt-alt"] + + +def test_save_prompt_config_updates_rewrite_temperature(tmp_path): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + next_path = tmp_path / "next.md" + auto_path = tmp_path / "auto.md" + rewrite_path = tmp_path / "rewrite.md" + next_path.write_text("next prompt\n", encoding="utf-8") + auto_path.write_text("auto prompt\n", encoding="utf-8") + rewrite_path.write_text("rewrite prompt\n", encoding="utf-8") + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.rewrite_all_system_prompt_path = str(rewrite_path) + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt_fallback_path = None + + config = enhancer.save_prompt_config( + rewrite_temperature=1.3, + ) + + assert enhancer.rewrite_default_temperature == 1.3 + assert config["rewrite_temperature"] == 1.3 + + +def test_save_prompt_config_creates_versioned_backup_for_existing_prompt(tmp_path): + enhancer = _build_test_enhancer( + _chat_payload_with_content( + '{"id":"preset_a","label":"Preset A","segment_prompts":["A","B"]}' + ) + ) + next_path = tmp_path / "next.md" + auto_path = tmp_path / "auto.md" + rewrite_path = tmp_path / "rewrite_window_system_prompt.md" + next_path.write_text("next original\n", encoding="utf-8") + auto_path.write_text("auto original\n", encoding="utf-8") + rewrite_path.write_text("rewrite original\n", encoding="utf-8") + enhancer.enhance_system_prompt_path = str(next_path) + enhancer.auto_system_prompt_path = str(auto_path) + enhancer.rewrite_all_system_prompt_path = str(rewrite_path) + enhancer.enhance_system_prompt_fallback_path = None + enhancer.auto_system_prompt_fallback_path = None + enhancer.rewrite_all_system_prompt_fallback_path = None + + enhancer.save_prompt_config( + rewrite_window_system_prompt="rewrite updated", + ) + + backup_paths = sorted( + tmp_path.glob("rewrite_window_system_prompt.*.bak.md") + ) + + assert rewrite_path.read_text(encoding="utf-8").strip() == "rewrite updated" + assert len(backup_paths) == 1 + assert backup_paths[0].read_text(encoding="utf-8").strip() == "rewrite original" + + +def test_resolve_prompt_save_path_prefers_existing_fallback_file(tmp_path): + primary_path = tmp_path / "prompts.local" / "rewrite.md" + fallback_path = tmp_path / "rewrite.md" + fallback_path.write_text("existing\n", encoding="utf-8") + + resolved = _resolve_prompt_save_path(str(primary_path), str(fallback_path)) + + assert resolved == str(fallback_path) + + +def test_build_prompt_backup_path_preserves_extension(tmp_path): + prompt_path = tmp_path / "rewrite_window_system_prompt.md" + + backup_path = _build_prompt_backup_path(prompt_path) + + assert backup_path.parent == tmp_path + assert re.match( + r"rewrite_window_system_prompt\.\d{8}_\d{6}\.bak\.md$", + backup_path.name, + ) diff --git a/apps/dreamverse/dreamverse/tests/test_realtime_stress.py b/apps/dreamverse/dreamverse/tests/test_realtime_stress.py new file mode 100644 index 0000000000..2c492b351f --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_realtime_stress.py @@ -0,0 +1,951 @@ +"""Multiprocess realtime stress test for LTX2 streaming websocket service.""" +# pyright: reportArgumentType=false, reportOptionalMemberAccess=false + +from __future__ import annotations + +import argparse +import asyncio +from collections import defaultdict +from datetime import datetime, timezone +import json +import math +import multiprocessing as mp +import os +from pathlib import Path +import sys +import time +import traceback +from typing import Any +import uuid + +import pytest + +pytestmark = pytest.mark.gpu + +try: + import websockets +except ModuleNotFoundError: + websockets = None # type: ignore[assignment] + + +DEFAULT_PRESET_FILE = ( + Path(__file__).resolve().parents[2] + / "web" + / "prompts" + / "selected_ltx2_continuation_story_presets.json" +) + + +def utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def iso_from_epoch(epoch_s: float) -> str: + return datetime.fromtimestamp(epoch_s, tz=timezone.utc).isoformat() + + +def iso_to_epoch(iso_ts: str | None) -> float | None: + if not iso_ts: + return None + try: + return datetime.fromisoformat(iso_ts).timestamp() + except ValueError: + return None + + +def safe_percentile(values: list[float], percentile: float) -> float | None: + if not values: + return None + sorted_values = sorted(values) + if len(sorted_values) == 1: + return sorted_values[0] + rank = (len(sorted_values) - 1) * (percentile / 100.0) + lower = math.floor(rank) + upper = math.ceil(rank) + if lower == upper: + return sorted_values[lower] + fraction = rank - lower + return ( + sorted_values[lower] + + (sorted_values[upper] - sorted_values[lower]) * fraction + ) + + +def summarize_series(values: list[float]) -> dict[str, float | int | None]: + if not values: + return { + "count": 0, + "min": None, + "p50": None, + "p95": None, + "p99": None, + "max": None, + "avg": None, + } + return { + "count": len(values), + "min": min(values), + "p50": safe_percentile(values, 50), + "p95": safe_percentile(values, 95), + "p99": safe_percentile(values, 99), + "max": max(values), + "avg": sum(values) / len(values), + } + + +def parse_int(value: object) -> int | None: + try: + parsed = int(value) + except (TypeError, ValueError): + return None + return parsed + + +def format_num(value: float | int | None, digits: int = 2) -> str: + if value is None: + return "n/a" + if isinstance(value, int): + return str(value) + return f"{value:.{digits}f}" + + +def load_curated_prompts( + preset_file: Path, + preset_id: str | None, + curated_limit: int, +) -> tuple[str, list[str], int]: + if not preset_file.is_file(): + raise ValueError(f"Preset file not found: {preset_file}") + try: + payload = json.loads(preset_file.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON in preset file: {preset_file}") from exc + + if not isinstance(payload, list) or len(payload) == 0: + raise ValueError("Preset file must contain a non-empty JSON array.") + + selected: dict[str, Any] | None = None + if preset_id: + for item in payload: + if not isinstance(item, dict): + continue + if str(item.get("id", "")).strip() == preset_id: + selected = item + break + if selected is None: + raise ValueError(f"Preset id not found: {preset_id}") + else: + for item in payload: + if isinstance(item, dict): + selected = item + break + if selected is None: + raise ValueError("No valid preset object found in preset file.") + + selected_id = str(selected.get("id", "")).strip() or "unknown_preset" + raw_prompts = selected.get("segment_prompts", []) + if not isinstance(raw_prompts, list): + raise ValueError( + f"Preset {selected_id} has invalid segment_prompts (must be list)." + ) + + prompts = [ + str(prompt).strip() + for prompt in raw_prompts + if isinstance(prompt, str) and str(prompt).strip() + ] + if not prompts: + raise ValueError(f"Preset {selected_id} has no non-empty prompts.") + + limited = prompts[:curated_limit] + if not limited: + raise ValueError( + f"curated_limit={curated_limit} produced no prompts for preset " + f"{selected_id}." + ) + return selected_id, limited, len(prompts) + + +async def run_single_session( + *, + worker_id: int, + worker_session_idx: int, + config: dict[str, Any], +) -> dict[str, Any]: + session_id = f"w{worker_id}_u{worker_session_idx}_{uuid.uuid4().hex[:8]}" + process_id = os.getpid() + url = str(config["url"]) + session_timeout_s = float(config["session_timeout_s"]) + post_complete_wait_s = float(config["post_complete_wait_s"]) + connect_timeout_s = float(config["connect_timeout_s"]) + curated_prompts = list(config["curated_prompts"]) + preset_id = str(config["preset_id"]) + + session_start_monotonic = time.monotonic() + session_start_epoch = time.time() + session_data: dict[str, Any] = { + "session_id": session_id, + "process_id": process_id, + "worker_id": worker_id, + "status": "failed", + "error": None, + "preset_id": preset_id, + "curated_prompt_count": len(curated_prompts), + "connect_start_ts_utc": iso_from_epoch(session_start_epoch), + "connect_finish_ts_utc": None, + "session_init_sent_ts_utc": None, + "gpu_assigned_ts_utc": None, + "target_segment_complete_ts_utc": None, + "leave_sent_ts_utc": None, + "close_ts_utc": None, + "duration_ms": None, + "queue_wait_ms": None, + "initial_total_segments": None, + "segments_started": 0, + "segments_completed": 0, + "media_segments_completed": 0, + "total_chunks": 0, + "total_chunk_bytes": 0, + "first_chunk_finish_ts_utc": None, + "last_chunk_finish_ts_utc": None, + "first_media_segment_complete_ts_utc": None, + "first_chunk_before_first_media_complete": None, + "session_goodput_mbps": None, + "chunks": [], + } + + connect_finish_monotonic: float | None = None + current_segment_idx: int | None = None + initial_total_segments: int | None = None + first_chunk_finish_epoch: float | None = None + first_media_segment_complete_epoch: float | None = None + last_chunk_finish_epoch: float | None = None + last_chunk_finish_monotonic: float | None = None + + try: + async with websockets.connect( + url, + max_size=None, + ping_interval=None, + open_timeout=connect_timeout_s, + close_timeout=2.0, + ) as ws: + connect_finish_monotonic = time.monotonic() + session_data["connect_finish_ts_utc"] = utc_now_iso() + + init_payload = { + "type": "session_init_v2", + "preset_id": preset_id, + "curated_prompts": curated_prompts, + "enhancement_enabled": False, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + } + await ws.send(json.dumps(init_payload)) + session_data["session_init_sent_ts_utc"] = utc_now_iso() + + while True: + elapsed_s = time.monotonic() - session_start_monotonic + timeout_remaining = session_timeout_s - elapsed_s + if timeout_remaining <= 0: + session_data["status"] = "timeout" + session_data["error"] = ( + f"Session timed out after {session_timeout_s:.1f}s." + ) + break + + recv_start_epoch = time.time() + recv_start_monotonic = time.monotonic() + recv_start_iso = iso_from_epoch(recv_start_epoch) + + try: + message = await asyncio.wait_for( + ws.recv(), + timeout=timeout_remaining, + ) + except asyncio.TimeoutError: + session_data["status"] = "timeout" + session_data["error"] = ( + "Timed out waiting for websocket message." + ) + break + except Exception as exc: + session_data["status"] = "failed" + session_data["error"] = f"WebSocket receive failed: {exc}" + break + + recv_finish_epoch = time.time() + recv_finish_monotonic = time.monotonic() + recv_finish_iso = iso_from_epoch(recv_finish_epoch) + + if isinstance(message, bytes): + session_data["total_chunks"] += 1 + session_data["total_chunk_bytes"] += len(message) + + if first_chunk_finish_epoch is None: + first_chunk_finish_epoch = recv_finish_epoch + session_data["first_chunk_finish_ts_utc"] = recv_finish_iso + + chunk_gap_ms: float | None = None + if last_chunk_finish_monotonic is not None: + chunk_gap_ms = ( + recv_finish_monotonic - last_chunk_finish_monotonic + ) * 1000.0 + + session_data["chunks"].append( + { + "segment_idx": current_segment_idx, + "chunk_idx": session_data["total_chunks"], + "size_bytes": len(message), + "chunk_start_ts_utc": recv_start_iso, + "chunk_finish_ts_utc": recv_finish_iso, + "chunk_gap_ms": chunk_gap_ms, + } + ) + last_chunk_finish_monotonic = recv_finish_monotonic + last_chunk_finish_epoch = recv_finish_epoch + session_data["last_chunk_finish_ts_utc"] = recv_finish_iso + continue + + if not isinstance(message, str): + continue + + try: + data = json.loads(message) + except json.JSONDecodeError as exc: + session_data["status"] = "protocol_error" + session_data["error"] = f"Invalid JSON message: {exc}" + break + + msg_type = data.get("type") + if msg_type == "gpu_assigned": + session_data["gpu_assigned_ts_utc"] = recv_finish_iso + if connect_finish_monotonic is not None: + session_data["queue_wait_ms"] = ( + recv_finish_monotonic - connect_finish_monotonic + ) * 1000.0 + elif msg_type == "ltx2_stream_start": + if initial_total_segments is None: + parsed_total = parse_int(data.get("total_segments")) + if parsed_total is not None and parsed_total > 0: + initial_total_segments = parsed_total + session_data["initial_total_segments"] = parsed_total + elif msg_type == "ltx2_segment_start": + parsed_idx = parse_int(data.get("segment_idx")) + current_segment_idx = parsed_idx + session_data["segments_started"] += 1 + elif msg_type == "media_segment_complete": + session_data["media_segments_completed"] += 1 + if first_media_segment_complete_epoch is None: + first_media_segment_complete_epoch = recv_finish_epoch + session_data[ + "first_media_segment_complete_ts_utc" + ] = recv_finish_iso + elif msg_type == "ltx2_segment_complete": + session_data["segments_completed"] += 1 + seg_idx = parse_int(data.get("segment_idx")) + if ( + initial_total_segments is not None + and seg_idx is not None + and seg_idx >= initial_total_segments + ): + session_data[ + "target_segment_complete_ts_utc" + ] = recv_finish_iso + await asyncio.sleep(post_complete_wait_s) + session_data["leave_sent_ts_utc"] = utc_now_iso() + try: + await ws.send(json.dumps({"type": "leave"})) + except Exception: + pass + session_data["status"] = "success" + break + elif msg_type == "session_timeout": + session_data["status"] = "timeout" + session_data["error"] = str( + data.get("message") or "Backend session timeout" + ) + break + elif msg_type == "error": + session_data["status"] = "failed" + session_data["error"] = str( + data.get("message") or "Backend error message" + ) + break + + if session_data["status"] == "failed" and session_data["error"] is None: + session_data["error"] = "Session ended without success." + except Exception as exc: + session_data["status"] = "failed" + session_data["error"] = f"WebSocket connect/run failed: {exc}" + + if ( + first_chunk_finish_epoch is not None + and last_chunk_finish_epoch is not None + and session_data["total_chunk_bytes"] > 0 + ): + duration_s = last_chunk_finish_epoch - first_chunk_finish_epoch + if duration_s > 0: + session_data["session_goodput_mbps"] = ( + session_data["total_chunk_bytes"] * 8.0 / duration_s / 1_000_000.0 + ) + + if ( + first_chunk_finish_epoch is not None + and first_media_segment_complete_epoch is not None + ): + session_data["first_chunk_before_first_media_complete"] = ( + first_chunk_finish_epoch < first_media_segment_complete_epoch + ) + + session_data["close_ts_utc"] = utc_now_iso() + session_data["duration_ms"] = ( + time.monotonic() - session_start_monotonic + ) * 1000.0 + return session_data + + +async def run_worker_sessions( + *, + worker_id: int, + session_count: int, + config: dict[str, Any], +) -> list[dict[str, Any]]: + tasks = [ + asyncio.create_task( + run_single_session( + worker_id=worker_id, + worker_session_idx=idx, + config=config, + ) + ) + for idx in range(session_count) + ] + if not tasks: + return [] + return await asyncio.gather(*tasks) + + +def worker_entry( + worker_id: int, + session_count: int, + config: dict[str, Any], + start_event: Any, + ready_queue: Any, + result_queue: Any, +) -> None: + try: + ready_queue.put({"worker_id": worker_id, "status": "ready"}) + start_event.wait() + sessions = asyncio.run( + run_worker_sessions( + worker_id=worker_id, + session_count=session_count, + config=config, + ) + ) + result_queue.put( + { + "worker_id": worker_id, + "status": "ok", + "sessions": sessions, + } + ) + except Exception as exc: + result_queue.put( + { + "worker_id": worker_id, + "status": "error", + "error": str(exc), + "traceback": traceback.format_exc(), + } + ) + + +def build_summary( + *, + sessions: list[dict[str, Any]], + chunk_gap_threshold_ms: float, +) -> dict[str, Any]: + status_counts: dict[str, int] = defaultdict(int) + chunk_gaps: list[float] = [] + queue_waits: list[float] = [] + session_goodputs: list[float] = [] + progressive_eligible = 0 + progressive_success = 0 + + all_chunk_finish_epochs: list[float] = [] + bucket_bytes: dict[int, int] = defaultdict(int) + total_chunk_bytes = 0 + + for session in sessions: + status = str(session.get("status") or "unknown") + status_counts[status] += 1 + + queue_wait_ms = session.get("queue_wait_ms") + if isinstance(queue_wait_ms, (int, float)): + queue_waits.append(float(queue_wait_ms)) + + session_goodput = session.get("session_goodput_mbps") + if isinstance(session_goodput, (int, float)): + session_goodputs.append(float(session_goodput)) + + progressive_value = session.get("first_chunk_before_first_media_complete") + if isinstance(progressive_value, bool): + progressive_eligible += 1 + if progressive_value: + progressive_success += 1 + + for chunk in session.get("chunks", []): + gap = chunk.get("chunk_gap_ms") + if isinstance(gap, (int, float)): + chunk_gaps.append(float(gap)) + + size_bytes = int(chunk.get("size_bytes") or 0) + finish_epoch = iso_to_epoch(chunk.get("chunk_finish_ts_utc")) + if finish_epoch is None or size_bytes <= 0: + continue + total_chunk_bytes += size_bytes + all_chunk_finish_epochs.append(finish_epoch) + bucket_bytes[int(finish_epoch)] += size_bytes + + chunk_gap_stats = summarize_series(chunk_gaps) + queue_wait_stats = summarize_series(queue_waits) + session_goodput_stats = summarize_series(session_goodputs) + + global_goodput_mbps: float | None = None + if len(all_chunk_finish_epochs) >= 2 and total_chunk_bytes > 0: + duration_s = max(all_chunk_finish_epochs) - min(all_chunk_finish_epochs) + if duration_s > 0: + global_goodput_mbps = ( + total_chunk_bytes * 8.0 / duration_s / 1_000_000.0 + ) + + bucket_throughputs_mbps = [ + (bytes_count * 8.0) / 1_000_000.0 + for _, bytes_count in sorted(bucket_bytes.items()) + ] + bucket_stats = summarize_series(bucket_throughputs_mbps) + + chunk_gap_threshold_breaches = [ + value for value in chunk_gaps if value >= chunk_gap_threshold_ms + ] + non_success = len(sessions) - status_counts.get("success", 0) + + fail_reasons: list[str] = [] + if non_success > 0: + fail_reasons.append( + f"{non_success} session(s) did not complete successfully." + ) + if not chunk_gaps: + fail_reasons.append("No chunk gap data collected.") + if chunk_gap_threshold_breaches: + fail_reasons.append( + f"{len(chunk_gap_threshold_breaches)} chunk gap(s) were >= " + f"{chunk_gap_threshold_ms:.0f}ms." + ) + + passed = len(fail_reasons) == 0 + progressive_ratio = None + if progressive_eligible > 0: + progressive_ratio = progressive_success / progressive_eligible + + return { + "passed": passed, + "fail_reasons": fail_reasons, + "sessions": { + "total": len(sessions), + "success": status_counts.get("success", 0), + "failed": status_counts.get("failed", 0), + "timeout": status_counts.get("timeout", 0), + "protocol_error": status_counts.get("protocol_error", 0), + "other": ( + len(sessions) + - ( + status_counts.get("success", 0) + + status_counts.get("failed", 0) + + status_counts.get("timeout", 0) + + status_counts.get("protocol_error", 0) + ) + ), + }, + "chunk_gap_ms": { + **chunk_gap_stats, + "threshold_ms": chunk_gap_threshold_ms, + "breach_count": len(chunk_gap_threshold_breaches), + }, + "queue_wait_ms": queue_wait_stats, + "progressive_streaming": { + "eligible_sessions": progressive_eligible, + "success_sessions": progressive_success, + "ratio": progressive_ratio, + }, + "bandwidth_mbps": { + "per_session": session_goodput_stats, + "global_goodput_mbps": global_goodput_mbps, + "bucketed_1s": { + "count": bucket_stats.get("count"), + "avg_mbps": bucket_stats.get("avg"), + "peak_mbps": bucket_stats.get("max"), + }, + }, + } + + +def print_summary( + *, + run_info: dict[str, Any], + summary: dict[str, Any], +) -> None: + sessions = summary["sessions"] + chunk_gap = summary["chunk_gap_ms"] + queue_wait = summary["queue_wait_ms"] + progressive = summary["progressive_streaming"] + bandwidth = summary["bandwidth_mbps"] + per_session_bw = bandwidth["per_session"] + bucket_bw = bandwidth["bucketed_1s"] + + print("=== LTX2 Realtime Stress Test Summary ===") + print( + "Run: " + f"url={run_info['url']} clients={run_info['clients']} " + f"processes={run_info['processes']} " + f"preset={run_info['preset_id']} " + f"curated_limit={run_info['curated_limit']}" + ) + print( + "Sessions: " + f"total={sessions['total']} success={sessions['success']} " + f"failed={sessions['failed']} timeout={sessions['timeout']} " + f"protocol_error={sessions['protocol_error']}" + ) + print( + "Chunk gap ms: " + f"min={format_num(chunk_gap['min'])} " + f"p50={format_num(chunk_gap['p50'])} " + f"p95={format_num(chunk_gap['p95'])} " + f"p99={format_num(chunk_gap['p99'])} " + f"max={format_num(chunk_gap['max'])} " + f"threshold={format_num(chunk_gap['threshold_ms'])} " + f"breaches={chunk_gap['breach_count']}" + ) + print( + "Queue wait ms: " + f"min={format_num(queue_wait['min'])} " + f"p50={format_num(queue_wait['p50'])} " + f"p95={format_num(queue_wait['p95'])} " + f"max={format_num(queue_wait['max'])}" + ) + ratio = progressive["ratio"] + ratio_text = "n/a" if ratio is None else f"{ratio * 100:.2f}%" + print( + "Progressive streaming: " + f"{progressive['success_sessions']}/" + f"{progressive['eligible_sessions']} ({ratio_text})" + ) + print( + "Bandwidth Mbps: " + f"per_session_avg={format_num(per_session_bw['avg'])} " + f"per_session_p95={format_num(per_session_bw['p95'])} " + f"global={format_num(bandwidth['global_goodput_mbps'])} " + f"bucket_avg={format_num(bucket_bw['avg_mbps'])} " + f"bucket_peak={format_num(bucket_bw['peak_mbps'])}" + ) + print(f"VERDICT: {'PASS' if summary['passed'] else 'FAIL'}") + if summary["fail_reasons"]: + print("Fail reasons:") + for reason in summary["fail_reasons"]: + print(f"- {reason}") + + +def distribute_sessions(total_clients: int, process_count: int) -> list[int]: + base = total_clients // process_count + remainder = total_clients % process_count + counts = [] + for idx in range(process_count): + count = base + (1 if idx < remainder else 0) + counts.append(count) + return counts + + +def run_stress(args: argparse.Namespace) -> tuple[dict[str, Any], int]: + if websockets is None: + raise RuntimeError( + "Missing dependency: websockets. Install it before running this " + "stress test." + ) + + preset_file = Path(args.preset_file).expanduser().resolve() + selected_preset_id, curated_prompts, total_prompt_count = load_curated_prompts( + preset_file=preset_file, + preset_id=args.preset_id, + curated_limit=args.curated_limit, + ) + + process_count = args.processes + if process_count is None: + process_count = min(args.clients, os.cpu_count() or 1) + process_count = max(1, min(process_count, args.clients)) + + mp_ctx = mp.get_context("spawn") + start_event = mp_ctx.Event() + ready_queue = mp_ctx.Queue() + result_queue = mp_ctx.Queue() + + worker_config = { + "url": args.url, + "preset_id": selected_preset_id, + "curated_prompts": curated_prompts, + "connect_timeout_s": args.connect_timeout_s, + "session_timeout_s": args.session_timeout_s, + "post_complete_wait_s": args.post_complete_wait_s, + } + + session_counts = distribute_sessions(args.clients, process_count) + run_start_epoch = time.time() + run_start_monotonic = time.monotonic() + run_start_iso = iso_from_epoch(run_start_epoch) + + processes: list[mp.Process] = [] + for worker_id, session_count in enumerate(session_counts): + proc = mp_ctx.Process( + target=worker_entry, + args=( + worker_id, + session_count, + worker_config, + start_event, + ready_queue, + result_queue, + ), + ) + proc.start() + processes.append(proc) + + try: + ready_workers = 0 + ready_deadline = time.monotonic() + 60.0 + while ready_workers < len(processes): + timeout_s = max(0.1, ready_deadline - time.monotonic()) + if timeout_s <= 0: + raise RuntimeError("Timed out waiting for workers to become ready.") + msg = ready_queue.get(timeout=timeout_s) + if msg.get("status") == "ready": + ready_workers += 1 + + start_event.set() + + result_deadline = ( + time.monotonic() + + args.connect_timeout_s + + args.session_timeout_s + + args.post_complete_wait_s + + 180.0 + ) + worker_results: list[dict[str, Any]] = [] + while len(worker_results) < len(processes): + timeout_s = max(0.1, result_deadline - time.monotonic()) + if timeout_s <= 0: + break + try: + result = result_queue.get(timeout=timeout_s) + except Exception: + break + worker_results.append(result) + + for proc in processes: + proc.join(timeout=5.0) + if proc.is_alive(): + proc.terminate() + proc.join(timeout=2.0) + + sessions: list[dict[str, Any]] = [] + worker_errors: list[dict[str, Any]] = [] + for result in worker_results: + if result.get("status") == "ok": + sessions.extend(result.get("sessions", [])) + else: + worker_errors.append( + { + "worker_id": result.get("worker_id"), + "error": result.get("error"), + "traceback": result.get("traceback"), + } + ) + + received_workers = {result.get("worker_id") for result in worker_results} + expected_workers = set(range(len(processes))) + missing_workers = sorted(expected_workers - received_workers) + for worker_id in missing_workers: + worker_errors.append( + { + "worker_id": worker_id, + "error": "No worker result received.", + } + ) + + run_end_epoch = time.time() + run_end_iso = iso_from_epoch(run_end_epoch) + run_duration_ms = (time.monotonic() - run_start_monotonic) * 1000.0 + + summary = build_summary( + sessions=sessions, + chunk_gap_threshold_ms=args.chunk_gap_threshold_ms, + ) + + if worker_errors: + summary["passed"] = False + summary["fail_reasons"] = list(summary["fail_reasons"]) + [ + f"{len(worker_errors)} worker error(s) occurred." + ] + + output_payload = { + "run_info": { + "url": args.url, + "clients": args.clients, + "processes": process_count, + "preset_file": str(preset_file), + "preset_id": selected_preset_id, + "curated_limit": args.curated_limit, + "selected_prompt_count": len(curated_prompts), + "preset_total_prompt_count": total_prompt_count, + "chunk_gap_threshold_ms": args.chunk_gap_threshold_ms, + "connect_timeout_s": args.connect_timeout_s, + "session_timeout_s": args.session_timeout_s, + "post_complete_wait_s": args.post_complete_wait_s, + "run_start_ts_utc": run_start_iso, + "run_end_ts_utc": run_end_iso, + "run_duration_ms": run_duration_ms, + }, + "summary": summary, + "full_data": { + "sessions": sessions, + "worker_errors": worker_errors, + }, + } + exit_code = 0 if summary["passed"] else 1 + return output_payload, exit_code + finally: + for proc in processes: + if proc.is_alive(): + proc.terminate() + proc.join(timeout=1.0) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Multiprocess realtime stress test for LTX2 streaming.", + ) + parser.add_argument( + "-u", + "--url", + required=True, + help="WebSocket URL, e.g. wss://your-domain/ws", + ) + parser.add_argument( + "-c", + "--clients", + type=int, + required=True, + help="Total concurrent virtual users.", + ) + parser.add_argument( + "--processes", + type=int, + default=None, + help="Worker process count (default: min(clients, cpu_count)).", + ) + parser.add_argument( + "--preset-file", + default=str(DEFAULT_PRESET_FILE), + help="Path to curated presets JSON file.", + ) + parser.add_argument( + "--preset-id", + default=None, + help="Preset id to use (default: first preset in file).", + ) + parser.add_argument( + "--curated-limit", + type=int, + default=6, + help="Number of curated prompts to send from selected preset.", + ) + parser.add_argument( + "--chunk-gap-threshold-ms", + type=float, + default=5000.0, + help="Fail if any chunk gap is >= this value.", + ) + parser.add_argument( + "--post-complete-wait-s", + type=float, + default=5.0, + help="Seconds to wait after target segment completion before leave.", + ) + parser.add_argument( + "--connect-timeout-s", + type=float, + default=20.0, + help="WebSocket connect timeout in seconds.", + ) + parser.add_argument( + "--session-timeout-s", + type=float, + default=180.0, + help="Max session runtime per virtual user in seconds.", + ) + parser.add_argument( + "-o", + "--output-json", + required=True, + help="Required output JSON path (summary + full data).", + ) + args = parser.parse_args() + + if args.clients <= 0: + parser.error("--clients must be > 0") + if args.processes is not None and args.processes <= 0: + parser.error("--processes must be > 0") + if args.curated_limit <= 0: + parser.error("--curated-limit must be > 0") + if args.chunk_gap_threshold_ms <= 0: + parser.error("--chunk-gap-threshold-ms must be > 0") + if args.post_complete_wait_s < 0: + parser.error("--post-complete-wait-s must be >= 0") + if args.connect_timeout_s <= 0: + parser.error("--connect-timeout-s must be > 0") + if args.session_timeout_s <= 0: + parser.error("--session-timeout-s must be > 0") + return args + + +def main() -> int: + try: + args = parse_args() + print("Starting LTX2 realtime stress test with the following parameters:") + for arg, value in vars(args).items(): + print(f" {arg}: {value}") + output_payload, exit_code = run_stress(args) + + output_path = Path(args.output_json).expanduser().resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text( + json.dumps(output_payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + print_summary( + run_info=output_payload["run_info"], + summary=output_payload["summary"], + ) + return exit_code + except SystemExit: + raise + except Exception as exc: + print(f"Error: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/apps/dreamverse/dreamverse/tests/test_session_init_image.py b/apps/dreamverse/dreamverse/tests/test_session_init_image.py new file mode 100644 index 0000000000..52e351c0c5 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_session_init_image.py @@ -0,0 +1,75 @@ +# pyright: reportMissingTypeArgument=false +import base64 +import io +from pathlib import Path + +from PIL import Image +import pytest + +from dreamverse.session_init_image import ( + MAX_SESSION_INIT_IMAGE_BYTES, + cleanup_session_init_image, + persist_session_init_image, +) + + +def make_data_url(format_name: str = "PNG") -> str: + image = Image.new("RGB", (2, 2), color=(16, 32, 64)) + buffer = io.BytesIO() + image.save(buffer, format=format_name) + encoded = base64.b64encode(buffer.getvalue()).decode("ascii") + mime_type = "image/png" if format_name == "PNG" else "image/jpeg" + return f"data:{mime_type};base64,{encoded}" + + +def test_persist_session_init_image_saves_normalized_file(tmp_path: Path): + session_image = persist_session_init_image( + { + "name": "frame.png", + "mime_type": "image/png", + "data_url": make_data_url("PNG"), + }, + temp_root=tmp_path, + ) + + assert session_image is not None + assert session_image.display_name == "frame.png" + assert session_image.file_path.is_file() + + cleanup_session_init_image(session_image) + assert not session_image.temp_dir.exists() + + +def test_persist_session_init_image_returns_none_when_missing_data(): + assert persist_session_init_image(None) is None + assert persist_session_init_image({"name": "frame.png", "data_url": ""}) is None + + +def test_persist_session_init_image_rejects_unsupported_mime(): + with pytest.raises(ValueError, match="PNG, JPEG, or WebP"): + persist_session_init_image( + { + "name": "frame.gif", + "mime_type": "image/gif", + "data_url": "data:image/gif;base64,R0lGODlhAQABAAAAACw=", + } + ) + + +def test_persist_session_init_image_rejects_large_payload(monkeypatch): + data_url = make_data_url("PNG") + oversized = "a" * (MAX_SESSION_INIT_IMAGE_BYTES + 1) + + def fake_b64decode(value: str, validate: bool = True): + return oversized.encode("ascii") + + monkeypatch.setattr(base64, "b64decode", fake_b64decode) + + with pytest.raises(ValueError, match="15 MB or smaller"): + persist_session_init_image( + { + "name": "frame.png", + "mime_type": "image/png", + "data_url": data_url, + } + ) diff --git a/apps/dreamverse/dreamverse/tests/test_session_logging.py b/apps/dreamverse/dreamverse/tests/test_session_logging.py new file mode 100644 index 0000000000..33c6433287 --- /dev/null +++ b/apps/dreamverse/dreamverse/tests/test_session_logging.py @@ -0,0 +1,1276 @@ +from __future__ import annotations +# pyright: reportArgumentType=false, reportAttributeAccessIssue=false, reportMissingImports=false, reportMissingTypeArgument=false, reportOperatorIssue=false + +import asyncio +import base64 +import io +import json +import os +import re +import socket +from types import SimpleNamespace + +from fastapi import WebSocketDisconnect +from PIL import Image +import pytest + + +os.environ.setdefault("CEREBRAS_API_KEY", "dummy") +os.environ.setdefault("GROQ_API_KEY", "dummy") + +import dreamverse.main as server_main # noqa: E402 +import dreamverse.runtime as runtime # noqa: E402 +from dreamverse.session_logger import SessionEventLogger # noqa: E402 +from dreamverse.worker_ipc import MediaChunk, MediaComplete, MediaInit # noqa: E402 +from dreamverse.session import controller as session_controller # noqa: E402 + +pytestmark = pytest.mark.gpu + + +class _FakeSessionLogger: + def __init__(self): + self.events: list[dict[str, object]] = [] + + async def write_event( + self, + *, + event: str, + client_id: str, + payload: dict[str, object] | None = None, + ) -> None: + entry: dict[str, object] = { + "event": event, + "client_id": client_id, + } + if payload: + entry.update(payload) + self.events.append(entry) + + +class _FakeWebSocket: + def __init__(self, messages: list[tuple[float, dict[str, object]]]): + self._messages = messages + self._index = 0 + self.sent_json: list[dict[str, object]] = [] + self.sent_bytes: list[bytes] = [] + + async def accept(self) -> None: + return None + + async def send_json(self, payload: dict[str, object]) -> None: + self.sent_json.append(payload) + + async def send_bytes(self, payload: bytes) -> None: + self.sent_bytes.append(payload) + + async def close(self, code: int = 1000, reason: str | None = None) -> None: + del code, reason + + async def receive_json(self) -> dict[str, object]: + if self._index >= len(self._messages): + raise WebSocketDisconnect() + delay_s, payload = self._messages[self._index] + self._index += 1 + if delay_s > 0: + await asyncio.sleep(delay_s) + return payload + + +class _FakeSlot: + def __init__(self, step_delay_s: float = 0.0): + self.shared_stream_buffer = None + self._stream_queues: dict[str, asyncio.Queue] = {} + self.calls: list[dict[str, object]] = [] + self._step_delay_s = max(0.0, float(step_delay_s)) + + async def join_user(self, client_id: str, model_id: str) -> None: + del client_id, model_id + + def register_stream_queue(self, client_id: str) -> asyncio.Queue: + queue: asyncio.Queue = asyncio.Queue() + self._stream_queues[client_id] = queue + return queue + + def unregister_stream_queue(self, client_id: str) -> None: + self._stream_queues.pop(client_id, None) + + async def user_step( + self, + client_id: str, + prompt: str, + segment_idx: int, + reset_conditioning: bool, + image_path: str | None = None, + ): + self.calls.append({ + "client_id": client_id, + "prompt": prompt, + "segment_idx": segment_idx, + "image_path": image_path, + "reset_conditioning": reset_conditioning, + }) + queue = self._stream_queues[client_id] + await queue.put(MediaInit( + user_id=client_id, + segment_idx=segment_idx, + stream_id=f"stream-{segment_idx}", + mime='video/mp4; codecs="avc1.640028,mp4a.40.2"', + uses_shared_buffer=False, + )) + await queue.put(MediaChunk( + user_id=client_id, + segment_idx=segment_idx, + stream_id=f"stream-{segment_idx}", + chunk=b"abc123", + uses_shared_buffer=False, + )) + await queue.put(MediaComplete( + user_id=client_id, + segment_idx=segment_idx, + stream_id=f"stream-{segment_idx}", + chunks=1, + )) + if self._step_delay_s > 0: + await asyncio.sleep(self._step_delay_s) + return {"e2e_latency_ms": 10.0} + + +class _FakeGPUPool: + def __init__(self, step_delay_s: float = 0.0): + self._slot = _FakeSlot(step_delay_s=step_delay_s) + + def get_status(self) -> dict[str, int]: + return { + "queue_size": 0, + "available_gpus": 1, + "total_gpus": 1, + } + + async def acquire(self, client_id: str, websocket) -> tuple[int, _FakeSlot]: + del client_id, websocket + return 0, self._slot + + async def release(self, client_id: str) -> None: + del client_id + + +def _make_png_data_url() -> str: + image = Image.new("RGB", (2, 2), color=(16, 32, 64)) + buffer = io.BytesIO() + image.save(buffer, format="PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("ascii") + return f"data:image/png;base64,{encoded}" + + +class _FakePromptEnhancer: + def set_provider(self, provider: object, preferred_model: object = None): + del provider, preferred_model + return { + "provider": "openai", + "provider_options": ["openai", "cerebras"], + "options": ["gpt-5-nano-2025-08-07"], + } + + def get_rewrite_model_config(self): + return self.set_provider(None, None) + + def resolve_rewrite_model(self, value: object) -> str: + if isinstance(value, str) and value.strip(): + return value.strip() + return "gpt-5-nano-2025-08-07" + + def resolve_rewrite_system_prompt(self, value: object) -> str: + if isinstance(value, str) and value.strip(): + return value.strip() + return "shared rewrite system prompt" + + def resolve_rewrite_new_rollout_system_prompt(self, value: object) -> str: + if isinstance(value, str) and value.strip(): + return value.strip() + return "new rollout rewrite system prompt" + + def resolve_rewrite_temperature(self, value: float | None) -> float: + return 1.0 if value is None else float(value) + + async def enhance_prompt( + self, + raw_prompt: str, + *, + locked_segments: list[str], + next_segment_idx: int, + preset_id: str | None, + mode: str, + model: str, + timeout_ms: int, + ): + del locked_segments, next_segment_idx, preset_id, mode, model, timeout_ms + return SimpleNamespace( + prompt=f"enhanced: {raw_prompt}", + fallback_used=False, + error=None, + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=12.5, + ) + + async def rewrite_prompt_sequence( + self, + snapshot_prompts: list[str], + *, + preset_id: str | None, + preset_label: str | None, + rewrite_instruction: str, + rewrite_model: str, + rewrite_temperature: float, + timeout_ms: int, + system_prompt_override: str | None = None, + ): + del ( + snapshot_prompts, + preset_id, + preset_label, + rewrite_instruction, + rewrite_model, + rewrite_temperature, + timeout_ms, + system_prompt_override, + ) + return SimpleNamespace( + prompts=["rewrite a", "rewrite b"], + fallback_used=False, + error=None, + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=23.0, + rollout_id="rewrite_rollout", + rollout_label="Rewrite Rollout", + raw_response_text=( + '{"id":"rewrite_rollout","label":"Rewrite Rollout",' + '"segment_prompts":["rewrite a","rewrite b"]}' + ), + ) + + +class _FallbackPromptEnhancer(_FakePromptEnhancer): + async def enhance_prompt( + self, + raw_prompt: str, + *, + locked_segments: list[str], + next_segment_idx: int, + preset_id: str | None, + mode: str, + model: str, + timeout_ms: int, + ): + del ( + raw_prompt, + locked_segments, + next_segment_idx, + preset_id, + mode, + model, + timeout_ms, + ) + return SimpleNamespace( + prompt="", + fallback_used=True, + error="mock enhancement timeout", + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=18.0, + ) + + +class _UnsafeEnhancedPromptEnhancer(_FakePromptEnhancer): + async def enhance_prompt( + self, + raw_prompt: str, + *, + locked_segments: list[str], + next_segment_idx: int, + preset_id: str | None, + mode: str, + model: str, + timeout_ms: int, + ): + del ( + raw_prompt, + locked_segments, + next_segment_idx, + preset_id, + mode, + model, + timeout_ms, + ) + return SimpleNamespace( + prompt="unsafe enhanced prompt", + fallback_used=False, + error=None, + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=14.0, + ) + + +class _FakePromptSafetyFilter: + def __init__(self, blocked_prompts: dict[str, str]): + self._blocked_prompts = { + str(prompt).strip(): str(error) + for prompt, error in blocked_prompts.items() + if str(prompt).strip() + } + + def get_prompt_safety_error(self, prompt: str) -> str | None: + return self._blocked_prompts.get(str(prompt).strip()) + + def get_first_blocked_prompt(self, prompts: list[str]): + for index, prompt in enumerate(prompts): + error = self.get_prompt_safety_error(prompt) + if error is not None: + return SimpleNamespace(index=index, prompt=prompt, error=error) + return None + + +def test_session_event_logger_initializes_hostname_folder_and_utc_filename( + tmp_path, +): + logger = SessionEventLogger(tmp_path) + assert logger.directory == tmp_path / socket.gethostname() + assert logger.directory.is_dir() + assert re.match(r"^\d{6}_\d{6}\.jsonl$", logger.path.name) + assert logger.path.is_file() + + +def test_session_event_logger_appends_valid_jsonl(tmp_path): + logger = SessionEventLogger(tmp_path) + + async def _write() -> None: + await logger.write_event( + event="append_prompt", + client_id="client-123", + payload={"prompt": "hello world"}, + ) + + asyncio.run(_write()) + + lines = logger.path.read_text(encoding="utf-8").strip().splitlines() + assert len(lines) == 1 + record = json.loads(lines[0]) + assert record["event"] == "append_prompt" + assert record["client_id"] == "client-123" + assert record["hostname"] == socket.gethostname() + assert isinstance(record["ts"], str) + assert record["prompt"] == "hello world" + + +def test_websocket_flow_emits_required_session_events(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + try: + fake_logger = _FakeSessionLogger() + runtime.gpu_pool = _FakeGPUPool() + runtime.prompt_enhancer = _FakePromptEnhancer() + runtime.session_event_logger = fake_logger + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "curated_prompts": ["segment one prompt"], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.01, {"type": "append_prompt", "prompt": "make it rainy"}), + ( + 0.01, + { + "type": "rewrite_seed_prompts", + "rewrite_instruction": "more dramatic", + }, + ), + (0.30, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + event_names = [event["event"] for event in fake_logger.events] + assert "ws_session_start" in event_names + assert "gpu_assigned" in event_names + assert "append_prompt" in event_names + assert "segment_start" in event_names + assert "segment_complete" in event_names + + rewrite_events = [ + event + for event in fake_logger.events + if event["event"] == "rewrite_done" + ] + rewrite_kinds = {event.get("kind") for event in rewrite_events} + assert "seed_rewrite" in rewrite_kinds + assert "enhance_prompt" in rewrite_kinds + + segment_complete = next( + event + for event in fake_logger.events + if event["event"] == "segment_complete" + ) + latency = segment_complete.get("latency_ms") + assert isinstance(latency, dict) + assert set(latency.keys()) == { + "total", + "worker_e2e", + "main_user_step", + "overhead", + } + assert isinstance(segment_complete.get("data_size_bytes"), int) + assert segment_complete["data_size_bytes"] > 0 + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + + +def test_simple_generate_reuses_session_and_resets_conditioning(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _FakePromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "simple_prompt_1", + "curated_prompts": ["a selected prompt"], + "single_clip_mode": True, + "enhancement_enabled": False, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "simple_generate", + "preset_id": "simple_custom_prompt", + "prompt_id": "simple_custom_prompt", + "prompt": "a fresh custom prompt", + "enhancement_enabled": True, + "initial_image": { + "name": "frame.png", + "mime_type": "image/png", + "data_url": _make_png_data_url(), + }, + }, + ), + (0.50, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert [call["segment_idx"] for call in fake_pool._slot.calls] == [1, 1] + assert fake_pool._slot.calls[0]["prompt"] == "a selected prompt" + assert fake_pool._slot.calls[0]["reset_conditioning"] is False + assert fake_pool._slot.calls[0]["image_path"] is None + assert fake_pool._slot.calls[1]["prompt"] == "enhanced: a fresh custom prompt" + assert fake_pool._slot.calls[1]["reset_conditioning"] is True + assert isinstance(fake_pool._slot.calls[1]["image_path"], str) + + event_names = [event["event"] for event in fake_logger.events] + assert "simple_generate" in event_names + + stream_start_events = [ + payload + for payload in ws.sent_json + if payload.get("type") == "ltx2_stream_start" + ] + assert len(stream_start_events) == 2 + assert all( + payload.get("generation_segment_cap") == 0 + for payload in stream_start_events + ) + stream_complete_events = [ + payload + for payload in ws.sent_json + if payload.get("type") == "ltx2_stream_complete" + ] + assert len(stream_complete_events) == 2 + assert "prompt_sources_blocked" not in [ + payload.get("type") for payload in ws.sent_json + ] + + segment_start_events = [ + payload + for payload in ws.sent_json + if payload.get("type") == "ltx2_segment_start" + ] + assert [payload["segment_idx"] for payload in segment_start_events] == [1, 1] + + step_complete_events = [ + payload + for payload in ws.sent_json + if payload.get("type") == "step_complete" + ] + assert len(step_complete_events) == 2 + assert all( + set(payload.get("latency_ms", {}).keys()) == { + "total", + "worker_e2e", + "main_user_step", + "overhead", + } + for payload in step_complete_events + ) + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_single5s_ignores_global_segment_cap(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + old_generation_segment_cap = session_controller.GENERATION_SEGMENT_CAP + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _FakePromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + session_controller.GENERATION_SEGMENT_CAP = 1 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "simple_prompt_1", + "curated_prompts": ["a selected prompt"], + "single_clip_mode": True, + "enhancement_enabled": False, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "simple_generate", + "preset_id": "simple_custom_prompt", + "prompt_id": "simple_custom_prompt", + "prompt": "a fresh custom prompt", + "enhancement_enabled": True, + "initial_image": None, + }, + ), + (0.50, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert [call["segment_idx"] for call in fake_pool._slot.calls] == [1, 1] + assert all( + payload.get("generation_segment_cap") == 0 + for payload in ws.sent_json + if payload.get("type") == "ltx2_stream_start" + ) + assert "generation_cap_reached" not in [ + payload.get("type") for payload in ws.sent_json + ] + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + session_controller.GENERATION_SEGMENT_CAP = old_generation_segment_cap + + +def test_regular_segment_cap_completes_stream_and_allows_rewrite_rollout_restart(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + old_generation_segment_cap = session_controller.GENERATION_SEGMENT_CAP + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _FakePromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + session_controller.GENERATION_SEGMENT_CAP = 1 + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "test_preset", + "curated_prompts": ["segment one prompt"], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.20, + { + "type": "rewrite_seed_prompts", + "rewrite_instruction": "start a new rollout", + }, + ), + (0.50, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert [call["segment_idx"] for call in fake_pool._slot.calls] == [1, 1] + assert fake_pool._slot.calls[0]["prompt"] == "segment one prompt" + assert fake_pool._slot.calls[1]["prompt"] == "rewrite a" + assert fake_pool._slot.calls[1]["reset_conditioning"] is True + + message_types = [payload["type"] for payload in ws.sent_json] + assert message_types.count("ltx2_stream_start") == 2 + assert message_types.count("ltx2_stream_complete") == 2 + assert "generation_cap_reached" not in message_types + assert "prompt_sources_blocked" not in message_types + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + session_controller.GENERATION_SEGMENT_CAP = old_generation_segment_cap + + +def test_rewrite_during_active_segment_restarts_from_rewritten_seed_window(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool(step_delay_s=0.15) + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _FakePromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "test_preset", + "curated_prompts": [ + "segment one prompt", + "segment two prompt", + ], + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "rewrite_seed_prompts", + "rewrite_instruction": "restart from rewrite", + }, + ), + (0.45, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert [call["prompt"] for call in fake_pool._slot.calls[:2]] == [ + "segment one prompt", + "rewrite a", + ] + assert all( + call["prompt"] != "segment two prompt" + for call in fake_pool._slot.calls[1:] + ) + assert fake_pool._slot.calls[1]["segment_idx"] == 1 + assert fake_pool._slot.calls[1]["reset_conditioning"] is True + + reset_events = [ + payload + for payload in ws.sent_json + if payload.get("type") == "seed_prompts_reset_applied" + ] + assert reset_events + assert any( + payload.get("reason") == "rewrite_during_generation" + for payload in reset_events + ) + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_initial_custom_rollout_prompt_generates_seed_window_before_streaming(): + class _InitialRolloutPromptEnhancer(_FakePromptEnhancer): + def __init__(self): + self.rewrite_calls: list[dict[str, object]] = [] + + async def rewrite_prompt_sequence( + self, + snapshot_prompts: list[str], + *, + preset_id: str | None, + preset_label: str | None, + rewrite_instruction: str, + rewrite_model: str, + rewrite_temperature: float, + timeout_ms: int, + system_prompt_override: str | None = None, + ): + self.rewrite_calls.append({ + "snapshot_prompts": list(snapshot_prompts), + "preset_id": preset_id, + "preset_label": preset_label, + "rewrite_instruction": rewrite_instruction, + "rewrite_model": rewrite_model, + "rewrite_temperature": rewrite_temperature, + "timeout_ms": timeout_ms, + "system_prompt_override": system_prompt_override, + }) + del rewrite_model, rewrite_temperature, timeout_ms, system_prompt_override + return SimpleNamespace( + prompts=[ + "rewrite 1", + "rewrite 2", + "rewrite 3", + "rewrite 4", + "rewrite 5", + "rewrite 6", + ], + fallback_used=False, + error=None, + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=23.0, + rollout_id=preset_id or "custom_editable", + rollout_label=preset_label or "Custom rollout", + raw_response_text=( + '{"id":"custom_editable","label":"Custom rollout",' + '"segment_prompts":["rewrite 1","rewrite 2","rewrite 3",' + '"rewrite 4","rewrite 5","rewrite 6"]}' + ), + ) + + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + fake_enhancer = _InitialRolloutPromptEnhancer() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = fake_enhancer + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "custom_editable", + "preset_label": "Custom rollout", + "curated_prompts": [], + "initial_rollout_prompt": "A moonbase corridor thriller with flooding", + "rewrite_model": "gpt-4.1-mini", + "rewrite_temperature": 0.4, + "rewrite_window_system_prompt": "Session-specific rewrite prompt", + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert len(fake_enhancer.rewrite_calls) == 1 + assert fake_enhancer.rewrite_calls[0] == { + "snapshot_prompts": [], + "preset_id": "custom_editable", + "preset_label": "Custom rollout", + "rewrite_instruction": "A moonbase corridor thriller with flooding", + "rewrite_model": "gpt-4.1-mini", + "rewrite_temperature": 0.4, + "timeout_ms": server_main.PROMPT_TIMEOUT_MS, + "system_prompt_override": "Session-specific rewrite prompt", + } + assert fake_pool._slot.calls + assert fake_pool._slot.calls[0]["prompt"] == "rewrite 1" + + message_types = [payload["type"] for payload in ws.sent_json] + assert "rewrite_seed_prompts_started" in message_types + assert "seed_prompts_updated" in message_types + assert "rewrite_seed_prompts_complete" in message_types + assert "seed_prompts_reset_applied" in message_types + assert "ltx2_stream_start" in message_types + assert "prompt_sources_blocked" not in message_types + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_initial_custom_rollout_prompt_uses_dedicated_user_prompt_by_default(): + class _InitialRolloutPromptEnhancer(_FakePromptEnhancer): + def __init__(self): + self.rewrite_calls: list[dict[str, object]] = [] + + async def rewrite_prompt_sequence( + self, + snapshot_prompts: list[str], + *, + preset_id: str | None, + preset_label: str | None, + rewrite_instruction: str, + rewrite_model: str, + rewrite_temperature: float, + timeout_ms: int, + system_prompt_override: str | None = None, + ): + self.rewrite_calls.append({ + "snapshot_prompts": list(snapshot_prompts), + "preset_id": preset_id, + "preset_label": preset_label, + "rewrite_instruction": rewrite_instruction, + "rewrite_model": rewrite_model, + "rewrite_temperature": rewrite_temperature, + "timeout_ms": timeout_ms, + "system_prompt_override": system_prompt_override, + }) + return SimpleNamespace( + prompts=[ + "rewrite 1", + "rewrite 2", + "rewrite 3", + "rewrite 4", + "rewrite 5", + "rewrite 6", + ], + fallback_used=False, + error=None, + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=23.0, + rollout_id=preset_id or "custom_editable", + rollout_label=preset_label or "Custom rollout", + raw_response_text=( + '{"id":"custom_editable","label":"Custom rollout",' + '"segment_prompts":["rewrite 1","rewrite 2","rewrite 3",' + '"rewrite 4","rewrite 5","rewrite 6"]}' + ), + ) + + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + fake_enhancer = _InitialRolloutPromptEnhancer() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = fake_enhancer + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "custom_editable", + "preset_label": "Custom rollout", + "curated_prompts": [], + "initial_rollout_prompt": "A moonbase corridor thriller with flooding", + "rewrite_model": "gpt-4.1-mini", + "rewrite_temperature": 0.4, + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert len(fake_enhancer.rewrite_calls) == 1 + assert fake_enhancer.rewrite_calls[0]["snapshot_prompts"] == [] + assert ( + fake_enhancer.rewrite_calls[0]["system_prompt_override"] + == "new rollout rewrite system prompt" + ) + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_initial_custom_rollout_prompt_prefers_session_specific_user_prompt(): + class _InitialRolloutPromptEnhancer(_FakePromptEnhancer): + def __init__(self): + self.rewrite_calls: list[dict[str, object]] = [] + + async def rewrite_prompt_sequence( + self, + snapshot_prompts: list[str], + *, + preset_id: str | None, + preset_label: str | None, + rewrite_instruction: str, + rewrite_model: str, + rewrite_temperature: float, + timeout_ms: int, + system_prompt_override: str | None = None, + ): + self.rewrite_calls.append({ + "snapshot_prompts": list(snapshot_prompts), + "preset_id": preset_id, + "preset_label": preset_label, + "rewrite_instruction": rewrite_instruction, + "rewrite_model": rewrite_model, + "rewrite_temperature": rewrite_temperature, + "timeout_ms": timeout_ms, + "system_prompt_override": system_prompt_override, + }) + return SimpleNamespace( + prompts=[ + "rewrite 1", + "rewrite 2", + "rewrite 3", + "rewrite 4", + "rewrite 5", + "rewrite 6", + ], + fallback_used=False, + error=None, + provider="openai", + model="gpt-5-nano-2025-08-07", + latency_ms=23.0, + rollout_id=preset_id or "custom_editable", + rollout_label=preset_label or "Custom rollout", + raw_response_text=( + '{"id":"custom_editable","label":"Custom rollout",' + '"segment_prompts":["rewrite 1","rewrite 2","rewrite 3",' + '"rewrite 4","rewrite 5","rewrite 6"]}' + ), + ) + + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + fake_enhancer = _InitialRolloutPromptEnhancer() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = fake_enhancer + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "custom_editable", + "preset_label": "Custom rollout", + "curated_prompts": [], + "initial_rollout_prompt": "A moonbase corridor thriller with flooding", + "rewrite_model": "gpt-4.1-mini", + "rewrite_temperature": 0.4, + "rewrite_window_system_prompt": "Window rewrite prompt", + "rewrite_user_system_prompt": "User rewrite prompt", + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert len(fake_enhancer.rewrite_calls) == 1 + assert fake_enhancer.rewrite_calls[0]["snapshot_prompts"] == [] + assert fake_enhancer.rewrite_calls[0]["system_prompt_override"] == "User rewrite prompt" + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_single5s_raw_prompt_block_prevents_generation(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _FakePromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = _FakePromptSafetyFilter({ + "blocked raw prompt": "Test raw prompt blocked.", + }) + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "simple_custom_prompt", + "curated_prompts": [], + "single_clip_mode": True, + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "append_prompt", + "prompt_id": "simple_custom_prompt", + "prompt": "blocked raw prompt", + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert fake_pool._slot.calls == [] + assert { + payload["type"] for payload in ws.sent_json + }.isdisjoint({"ltx2_segment_start", "ltx2_segment_complete"}) + error_payloads = [ + payload for payload in ws.sent_json if payload.get("type") == "error" + ] + assert any( + payload.get("message") == "Test raw prompt blocked." + for payload in error_payloads + ) + + prompt_blocked_events = [ + event + for event in fake_logger.events + if event["event"] == "prompt_blocked" + ] + assert len(prompt_blocked_events) == 1 + assert prompt_blocked_events[0]["kind"] == "user_raw" + assert prompt_blocked_events[0]["raw_prompt"] == "blocked raw prompt" + assert all(event["event"] != "segment_start" for event in fake_logger.events) + assert all(event["event"] != "rewrite_done" for event in fake_logger.events) + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_single5s_enhanced_prompt_block_prevents_generation(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _UnsafeEnhancedPromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = _FakePromptSafetyFilter({ + "unsafe enhanced prompt": "Test enhanced prompt blocked.", + }) + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "simple_custom_prompt", + "curated_prompts": [], + "single_clip_mode": True, + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "append_prompt", + "prompt_id": "simple_custom_prompt", + "prompt": "safe raw prompt", + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert fake_pool._slot.calls == [] + assert { + payload["type"] for payload in ws.sent_json + }.isdisjoint({"ltx2_segment_start", "ltx2_segment_complete"}) + error_payloads = [ + payload for payload in ws.sent_json if payload.get("type") == "error" + ] + assert any( + payload.get("message") == "Test enhanced prompt blocked." + for payload in error_payloads + ) + + rewrite_events = [ + event + for event in fake_logger.events + if event["event"] == "rewrite_done" + ] + assert len(rewrite_events) == 1 + assert rewrite_events[0]["kind"] == "enhance_prompt" + + prompt_blocked_events = [ + event + for event in fake_logger.events + if event["event"] == "prompt_blocked" + ] + assert len(prompt_blocked_events) == 1 + assert prompt_blocked_events[0]["kind"] == "user_enhanced" + assert prompt_blocked_events[0]["output_prompt"] == "unsafe enhanced prompt" + assert all(event["event"] != "segment_start" for event in fake_logger.events) + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter + + +def test_single5s_enhancement_fallback_does_not_start_generation(): + old_gpu_pool = runtime.gpu_pool + old_prompt_enhancer = runtime.prompt_enhancer + old_session_logger = runtime.session_event_logger + old_prompt_safety_filter = runtime.prompt_safety_filter + try: + fake_logger = _FakeSessionLogger() + fake_pool = _FakeGPUPool() + runtime.gpu_pool = fake_pool + runtime.prompt_enhancer = _FallbackPromptEnhancer() + runtime.session_event_logger = fake_logger + runtime.prompt_safety_filter = None + + ws = _FakeWebSocket( + [ + ( + 0.0, + { + "type": "session_init_v2", + "preset_id": "simple_custom_prompt", + "curated_prompts": [], + "single_clip_mode": True, + "enhancement_enabled": True, + "auto_extension_enabled": False, + "loop_generation_enabled": False, + }, + ), + ( + 0.01, + { + "type": "append_prompt", + "prompt_id": "simple_custom_prompt", + "prompt": "prompt that times out", + }, + ), + (0.20, {"type": "leave"}), + ] + ) + + asyncio.run(server_main.websocket_endpoint(ws)) + + assert fake_pool._slot.calls == [] + assert { + payload["type"] for payload in ws.sent_json + }.isdisjoint({"ltx2_segment_start", "ltx2_segment_complete"}) + + fallback_payloads = [ + payload + for payload in ws.sent_json + if payload.get("type") == "prompt_fallback_used" + ] + assert len(fallback_payloads) == 1 + assert fallback_payloads[0]["prompt_id"] == "simple_custom_prompt" + assert ( + fallback_payloads[0]["error"] + == server_main.PROMPT_EXTENSION_FAILURE_USER_MESSAGE + ) + assert fallback_payloads[0]["source"] == "user_enhancement_failed" + + rewrite_events = [ + event + for event in fake_logger.events + if event["event"] == "rewrite_done" + ] + assert len(rewrite_events) == 1 + assert rewrite_events[0]["kind"] == "enhance_prompt" + assert all(event["event"] != "segment_start" for event in fake_logger.events) + assert all(event["event"] != "prompt_blocked" for event in fake_logger.events) + finally: + runtime.gpu_pool = old_gpu_pool + runtime.prompt_enhancer = old_prompt_enhancer + runtime.session_event_logger = old_session_logger + runtime.prompt_safety_filter = old_prompt_safety_filter diff --git a/apps/dreamverse/dreamverse/utils.py b/apps/dreamverse/dreamverse/utils.py new file mode 100644 index 0000000000..c742c33b0f --- /dev/null +++ b/apps/dreamverse/dreamverse/utils.py @@ -0,0 +1,19 @@ +"""Shared helpers used by main.py, routes/, and session/.""" +from __future__ import annotations + +from datetime import datetime, timezone + + +def _main_print(level: str, message: str): + print(f"[MAIN][{level}] {message}", flush=True) + + +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +PROMPT_EXTENSION_FAILURE_USER_MESSAGE = ("Prompt extension failed for this request.") + + +def _resolve_generation_segment_cap(*, single_clip_mode: bool, cap: int) -> int: + return 0 if single_clip_mode else cap diff --git a/apps/dreamverse/dreamverse/video_generation.py b/apps/dreamverse/dreamverse/video_generation.py new file mode 100644 index 0000000000..b9e5302175 --- /dev/null +++ b/apps/dreamverse/dreamverse/video_generation.py @@ -0,0 +1,535 @@ +"""LTX2 model lifecycle and continuation conditioning. + +Runs inside a GPU worker subprocess. Owns the model, the audio +encoder, and the per-session continuation state carried across +segments. Callers must set ``os.environ["CUDA_VISIBLE_DEVICES"]`` +before constructing ``VideoGenerationWorker`` — all ``fastvideo.*`` +imports are deferred to method bodies so nothing touches CUDA at +module import time. +""" +# pyright: reportArgumentType=false, reportMissingImports=false, reportMissingTypeArgument=false, reportOptionalMemberAccess=false +# ruff: noqa: SIM105 +# mypy: ignore-errors +import gc +import os +import time +from dataclasses import dataclass +from typing import Any + +import numpy as np +import torch + +from dreamverse.config import ( + FRAME_HEIGHT, + FRAME_WIDTH, + MODEL_CONFIG, + NUM_FRAMES, + NUM_INFERENCE_STEPS, +) + +# Multi-frame decoded continuation defaults from +# examples/inference/basic/basic_ltx2_distilled_video_continuation.py. +# Overridable via environment variables. +LTX2_VIDEO_CONDITIONING_NUM_FRAMES = int(os.getenv("LTX2_VIDEO_CONDITIONING_NUM_FRAMES", "9")) +LTX2_VIDEO_CONDITIONING_END_OFFSET = int(os.getenv("LTX2_VIDEO_CONDITIONING_END_OFFSET", "0")) +LTX2_VIDEO_CONDITIONING_FRAME_IDX = int(os.getenv("LTX2_VIDEO_CONDITIONING_FRAME_IDX", "0")) +LTX2_VIDEO_CONDITIONING_STRENGTH = float(os.getenv("LTX2_VIDEO_CONDITIONING_STRENGTH", "1.0")) + +if (LTX2_VIDEO_CONDITIONING_NUM_FRAMES - 1) % 8 != 0: + raise ValueError("LTX2_VIDEO_CONDITIONING_NUM_FRAMES must satisfy " + "(frames - 1) % 8 == 0; got " + f"{LTX2_VIDEO_CONDITIONING_NUM_FRAMES}") + +# Audio conditioning: reuse denoised audio latents from the previous +# segment as initial latents for the next segment. +ENABLE_AUDIO_COND = os.getenv("ENABLE_AUDIO_COND", "1").lower() in ("1", "true", "yes") +# Number of decoded video frames worth of audio to condition on. +# Not subject to the (n-1)%8==0 constraint since this is audio-only. +AUDIO_CONDITIONING_NUM_FRAMES = int(os.getenv("AUDIO_CONDITIONING_NUM_FRAMES", '49')) +AUDIO_CONDITIONING_STRENGTH = float(os.getenv("AUDIO_CONDITIONING_STRENGTH", "1.0")) + +# Noise injected into conditioning context to prevent error +# accumulation across segments. 0 = no noise (default). +VIDEO_CONTEXT_NOISE = float(os.getenv("VIDEO_CONTEXT_NOISE", "0")) +AUDIO_CONTEXT_NOISE = float(os.getenv("AUDIO_CONTEXT_NOISE", "0")) + +# Re-encode audio latents through decode→encode round-trip to +# regularize noise accumulation (mirrors the video PIL→VAE path). +ENABLE_AUDIO_RE_ENCODE = os.getenv("ENABLE_AUDIO_RE_ENCODE", "").lower() in ("1", "true", "yes") + +DEFAULT_LTX2_AUDIO_SAMPLE_RATE = 16000 +DEFAULT_LTX2_AUDIO_HOP_LENGTH = 160 +DEFAULT_LTX2_AUDIO_DOWNSAMPLE = 4 + + +@dataclass +class StepResult: + """Output of one generation step. + + ``head_trim_frames`` / ``head_trim_audio_frames`` are derived here + so downstream AV streaming never needs to import conditioning + constants. + """ + frames: list + audio: Any + audio_sample_rate: int | None + timings: dict + head_trim_frames: int + head_trim_audio_frames: int + + +class ContinuationState: + """Per-session video + audio conditioning carried across segments.""" + + def __init__(self): + self.video_images: list | None = None + self.audio_latents: torch.Tensor | None = None + + def clear(self) -> None: + if self.video_images: + for old_image in self.video_images: + try: + old_image.close() + except Exception: + pass + self.video_images = None + self.audio_latents = None + + def apply_video(self, request_kwargs: dict, segment_idx: int) -> None: + """Seed next-segment kwargs with the cached tail frames.""" + if segment_idx <= 1 or not self.video_images: + return + from PIL import Image + + cond_images = list(self.video_images) + if VIDEO_CONTEXT_NOISE > 0: + noisy = [] + for img in cond_images: + arr = np.array(img, dtype=np.float32) + arr += np.random.normal(0, VIDEO_CONTEXT_NOISE * 255, arr.shape) + arr = np.clip(arr, 0, 255).astype(np.uint8) + noisy.append(Image.fromarray(arr)) + cond_images = noisy + request_kwargs["ltx2_video_conditions"] = [( + cond_images, + LTX2_VIDEO_CONDITIONING_FRAME_IDX, + LTX2_VIDEO_CONDITIONING_STRENGTH, + )] + request_kwargs["ltx2_images"] = None + request_kwargs["image_path"] = None + + def apply_audio( + self, + request_kwargs: dict, + segment_idx: int, + audio_lps: float, + ) -> None: + """Seed next-segment kwargs with clean audio latents + denoise mask. + + When audio conditioning is longer than video, extend audio + generation and shift video RoPE forward so the audio prefix + sits before video t=0. ``audio_lps`` (audio latent frames per + second) is passed in so this class never imports fastvideo. + """ + if not (ENABLE_AUDIO_COND and segment_idx > 1 and self.audio_latents is not None): + return + cached = self.audio_latents # [B,C,T,mel] + cond_duration = float(AUDIO_CONDITIONING_NUM_FRAMES) / 24.0 + audio_cond_T = max(1, round(cond_duration * audio_lps)) + audio_cond_T = min(audio_cond_T, cached.shape[2]) + + audio_extra = max(0, AUDIO_CONDITIONING_NUM_FRAMES - LTX2_VIDEO_CONDITIONING_NUM_FRAMES) + if audio_extra > 0: + audio_num_frames = NUM_FRAMES + audio_extra + request_kwargs["audio_num_frames"] = (audio_num_frames) + prefix_sec = float(audio_extra) / 24.0 + request_kwargs["video_position_offset_sec"] = prefix_sec + + new_duration = float(NUM_FRAMES + audio_extra) / 24.0 + total_T = max( + round(new_duration * audio_lps), + audio_cond_T + 1, + ) + + B, C, _, mel = cached.shape + clean = torch.zeros((B, C, total_T, mel), dtype=cached.dtype) + clean[:, :, :audio_cond_T, :] = (cached[:, :, -audio_cond_T:, :]) + if AUDIO_CONTEXT_NOISE > 0: + noise = torch.randn_like(clean[:, :, :audio_cond_T, :]) + clean[:, :, :audio_cond_T, :] += (AUDIO_CONTEXT_NOISE * noise) + + mask = torch.ones((B, 1, total_T, 1), dtype=torch.float32) + mask[:, :, :audio_cond_T, :] = (1.0 - AUDIO_CONDITIONING_STRENGTH) + + request_kwargs["ltx2_audio_clean_latent"] = clean + request_kwargs["ltx2_audio_denoise_mask"] = mask + + def save_video(self, frames: list) -> None: + """Snapshot trailing N frames as PIL images for next-segment conditioning.""" + from PIL import Image + num_cond_frames = LTX2_VIDEO_CONDITIONING_NUM_FRAMES + end_offset = LTX2_VIDEO_CONDITIONING_END_OFFSET + if end_offset + num_cond_frames > len(frames): + raise RuntimeError(f"Cannot extract {num_cond_frames} conditioning frames with " + f"end_offset={end_offset} from {len(frames)} generated frames.") + start_idx = len(frames) - end_offset - num_cond_frames + self.video_images = [ + Image.fromarray(np.ascontiguousarray(frames[start_idx + i])) for i in range(num_cond_frames) + ] + + def save_audio_latents(self, latents: torch.Tensor | None) -> None: + if latents is None: + self.audio_latents = None + return + self.audio_latents = latents.detach().clone().cpu() + + +class VideoGenerationWorker: + """Single-GPU LTX2 generator with continuation state. + + Caller must set ``os.environ["CUDA_VISIBLE_DEVICES"]`` before + instantiating, and call ``initialize()`` before any + ``generate_step()`` / ``warmup()``. + """ + + def __init__(self, gpu_id: int): + self.gpu_id = gpu_id + self.generator = None + self.current_model_config: dict = dict(MODEL_CONFIG) + self.continuation = ContinuationState() + self.audio_encoder_module = None + self.audio_processor_module = None + + def _gpu_mem(self) -> str: + a = torch.cuda.memory_allocated() / 1024**3 + r = torch.cuda.memory_reserved() / 1024**3 + return f"alloc={a:.2f}GiB, reserved={r:.2f}GiB" + + @staticmethod + def _resolve_refine_upsampler_path(model_root: str) -> str: + candidates = ( + os.path.join(model_root, "spatial_upscaler"), + os.path.join(model_root, "spatial_upsampler"), + ) + for candidate in candidates: + config_path = os.path.join(candidate, "config.json") + if os.path.isfile(config_path): + return candidate + raise FileNotFoundError("Could not find an LTX2 refine upsampler directory under " + f"{model_root}. Checked: {', '.join(candidates)}") + + def initialize(self, model_config: dict | None = None) -> None: + """Load (or reload) the LTX2 generator on the visible GPU.""" + if model_config is not None: + self.current_model_config = model_config + + if self.generator is not None: + print(f"[GPU {self.gpu_id}] Freeing old model...") + try: + self.generator.shutdown() + except Exception: + pass + del self.generator + self.generator = None + gc.collect() + torch.cuda.empty_cache() + print(f"[GPU {self.gpu_id}] After cleanup: {self._gpu_mem()}") + + print(f"[GPU {self.gpu_id}] Loading model: " + f"{self.current_model_config['model_path']}") + print(f"[GPU {self.gpu_id}] Before model load: {self._gpu_mem()}") + + from fastvideo.api.schema import ( + CompileConfig, + ComponentConfig, + EngineConfig, + GeneratorConfig, + OffloadConfig, + PipelineSelection, + QuantizationConfig, + ) + from fastvideo.entrypoints.video_generator import VideoGenerator + from fastvideo.utils import maybe_download_model + + model_root = maybe_download_model(self.current_model_config["model_path"]) + refine_upsampler_path = self._resolve_refine_upsampler_path(model_root) + config_model_path = (self.current_model_config.get("config_model_path") + or self.current_model_config["model_path"]) + + enable_compile = os.getenv("ENABLE_TORCH_COMPILE", "1") == "1" + + components = ComponentConfig( + config_root=config_model_path, + upsampler_weights=refine_upsampler_path, + ) + init_weights = self.current_model_config.get("init_weights_from_safetensors") + if init_weights: + components.transformer_weights = init_weights + + generator_config = GeneratorConfig( + model_path=model_root, + engine=EngineConfig( + num_gpus=1, + offload=OffloadConfig( + dit=False, + dit_layerwise=False, + text_encoder=False, + vae=False, + pin_cpu_memory=True, + ), + compile=CompileConfig( + enabled=enable_compile, + text_encoder_enabled=enable_compile, + backend="inductor", + fullgraph=True, + mode="max-autotune-no-cudagraphs", + dynamic=False, + ), + use_fsdp_inference=False, + quantization=QuantizationConfig(transformer_quant="NVFP4"), + ), + pipeline=PipelineSelection( + components=components, + vae_tiling=False, + preset_overrides={ + "refine": { + "enabled": True, + "num_inference_steps": 2, + "guidance_scale": 1.0, + "add_noise": True, + }, + }, + ), + ) + + self.generator = VideoGenerator.from_pretrained(config=generator_config) + print(f"[GPU {self.gpu_id}] After model load: {self._gpu_mem()}") + self._load_audio_encoder(model_root) + print(f"[GPU {self.gpu_id}] LTX2 model loaded (warmup pending)") + + def _load_audio_encoder(self, model_root: str) -> None: + if not ENABLE_AUDIO_RE_ENCODE: + return + from fastvideo.models.audio.ltx2_audio_processing import AudioProcessor + from fastvideo.models.loader.component_loader import ComponentLoader + + audio_vae_path = os.path.join(model_root, "audio_vae") + if not os.path.isdir(audio_vae_path): + print(f"[GPU {self.gpu_id}] audio_vae dir not found at " + f"{audio_vae_path}; disabling re-encode") + return + + loader = ComponentLoader.for_module_type("audio_encoder", "diffusers") + enc = loader.load(audio_vae_path, self.generator.fastvideo_args) + target = getattr(enc, "model", enc) + + proc = AudioProcessor( + sample_rate=target.sample_rate, + mel_bins=target.mel_bins, + mel_hop_length=target.mel_hop_length, + n_fft=target.n_fft, + ).to(torch.device("cuda")) + + self.audio_encoder_module = target + self.audio_processor_module = proc + print(f"[GPU {self.gpu_id}] Audio encoder loaded for " + f"re-encode conditioning ({self._gpu_mem()})") + + def _re_encode_audio( + self, + waveform: torch.Tensor, + sample_rate: int, + ) -> torch.Tensor | None: + """Waveform → mel → encoder → latents.""" + if (self.audio_encoder_module is None or self.audio_processor_module is None): + return None + device = torch.device("cuda") + if waveform.ndim == 1: + waveform = waveform.unsqueeze(0) # [channels, samples] + waveform = waveform.unsqueeze(0).to(device=device, dtype=torch.float32) # [1, ch, samples] + with torch.no_grad(): + mel = self.audio_processor_module.waveform_to_mel( + waveform, + waveform_sample_rate=sample_rate, + ).to(device=device, dtype=torch.float32) + latents = self.audio_encoder_module(mel) + return latents.detach() + + def shutdown(self) -> None: + if self.generator is not None: + try: + self.generator.shutdown() + except Exception: + pass + + def clear_conditioning(self) -> None: + self.continuation.clear() + + def generate_step( + self, + prompt: str, + segment_idx: int, + image_path: str | None, + reset_conditioning: bool, + ) -> StepResult: + """Execute one generation step; snapshot state for the next segment.""" + timings: dict = {} + + request_kwargs = dict( + prompt=prompt, + negative_prompt="", + save_video=False, + height=FRAME_HEIGHT, + width=FRAME_WIDTH, + num_frames=NUM_FRAMES, + fps=24, + num_inference_steps=NUM_INFERENCE_STEPS, + guidance_scale=1.0, + seed=10, + ltx2_image_crf=0.0, + image_path=image_path if segment_idx == 1 else None, + return_continuation_state=False, + ) + + if reset_conditioning: + self.continuation.clear() + + audio_lps = (DEFAULT_LTX2_AUDIO_SAMPLE_RATE / DEFAULT_LTX2_AUDIO_HOP_LENGTH / DEFAULT_LTX2_AUDIO_DOWNSAMPLE) + + # Phase 1: seed kwargs with prior-segment conditioning. + self.continuation.apply_video(request_kwargs, segment_idx) + self.continuation.apply_audio(request_kwargs, segment_idx, audio_lps) + + # Phase 2: generate. + t0 = time.perf_counter() + result = self.generator.generate_video(**request_kwargs) + torch.cuda.synchronize() + timings["generation_ms"] = (time.perf_counter() - t0) * 1000 + + if not isinstance(result, dict): + raise RuntimeError("Expected dictionary output from generate_video.") + frames = result.get("frames") + if not isinstance(frames, list) or len(frames) == 0: + raise RuntimeError("Generation did not return frames.") + audio = result.get("audio") + audio_sample_rate = result.get("audio_sample_rate") + if audio is not None and audio_sample_rate is None: + # LTX2 audio decoding stage uses 24kHz output by default. + audio_sample_rate = 24000 + print(f"[GPU {self.gpu_id}] audio_sample_rate missing from result; " + f"defaulting to {audio_sample_rate}Hz") + + timings["generation_time_ms"] = result.get("generation_time", 0.0) * 1000 + + # Phase 3: snapshot continuation state for the next segment. + t_save_start = time.perf_counter() + self.continuation.clear() + self.continuation.save_video(frames) + next_audio_latents = self._derive_next_audio_latents(audio, audio_sample_rate, result, segment_idx) + self.continuation.save_audio_latents(next_audio_latents) + + timings["save_conditioning_ms"] = (time.perf_counter() - t_save_start) * 1000 + timings["e2e_latency_ms"] = (time.perf_counter() - t0) * 1000 + + print(f"[GPU {self.gpu_id}] LTX2 segment {segment_idx}: " + f"{len(frames)} frames, gen={timings['generation_ms']:.0f}ms, " + f"save_conditioning={timings['save_conditioning_ms']:.0f}ms, " + f"e2e={timings['e2e_latency_ms']:.0f}ms") + + # Head-trim values for downstream AV streaming — computed here so + # the streaming layer never needs to know conditioning constants. + is_continuation = segment_idx > 1 and not reset_conditioning + head_trim_frames = (LTX2_VIDEO_CONDITIONING_NUM_FRAMES if is_continuation else 0) + audio_extra = (max(0, AUDIO_CONDITIONING_NUM_FRAMES - + LTX2_VIDEO_CONDITIONING_NUM_FRAMES) if ENABLE_AUDIO_COND else 0) + head_trim_audio_frames = (head_trim_frames + audio_extra if is_continuation else 0) + + return StepResult( + frames=frames, + audio=audio, + audio_sample_rate=audio_sample_rate, + timings=timings, + head_trim_frames=head_trim_frames, + head_trim_audio_frames=head_trim_audio_frames, + ) + + def _derive_next_audio_latents( + self, + audio: object, + audio_sample_rate: int | None, + result: dict, + segment_idx: int, + ) -> torch.Tensor | None: + """Pick which tensor to cache for next-segment audio conditioning.""" + if not ENABLE_AUDIO_COND: + return None + if (ENABLE_AUDIO_RE_ENCODE and audio is not None and audio_sample_rate is not None): + re_encoded = self._re_encode_audio(audio, audio_sample_rate) + if re_encoded is not None: + print(f"[GPU {self.gpu_id}] Re-encoded audio " + f"latents shape=" + f"{tuple(re_encoded.shape)} " + f"for segment {segment_idx + 1}") + return re_encoded + return None + audio_latents = result.get("ltx2_audio_latents") + if audio_latents is not None: + print(f"[GPU {self.gpu_id}] Cached audio latents " + f"shape={tuple(audio_latents.shape)} " + f"for segment {segment_idx + 1}") + return audio_latents + return None + + def warmup(self, prompt: str) -> dict[str, float]: + warmup_prompt = (prompt or "").strip() + if not warmup_prompt: + raise RuntimeError("Startup warmup prompt must be non-empty.") + + print(f"[GPU {self.gpu_id}] Startup warmup starting " + "(synthetic segments: seg1, seg2, seg1-post-LoRA)") + warmup_t0 = time.perf_counter() + + r1 = self.generate_step( + warmup_prompt, + segment_idx=1, + image_path=None, + reset_conditioning=True, + ) + r2 = self.generate_step( + warmup_prompt, + segment_idx=2, + image_path=None, + reset_conditioning=False, + ) + # r1 stage 1 compiled BEFORE LoRA wrapping (which happens during + # r1 stage 2 via ltx2_refine_lora_stage), so the resulting graph + # is keyed off pre-LoRA module identity and is stale once r1 r2 + # finish. The first real user seg=1 then re-compiles stage 1 + # ("stage1-LoRA-nocont"), wasting ~90s on the user's first + # request. Run a 3rd warmup pass with seg=1 reset=True after r2 + # so this graph is compiled while no client is waiting. r3 + # stage 2 hits r1 stage 2's cache (shape match, both LoRA-nocont) + # so the only real work is the missing stage 1 graph. + self.continuation.clear() + r3 = self.generate_step( + warmup_prompt, + segment_idx=1, + image_path=None, + reset_conditioning=True, + ) + warmup_total_ms = (time.perf_counter() - warmup_t0) * 1000.0 + self.continuation.clear() + + segment1_ms = float(r1.timings.get("e2e_latency_ms", 0.0)) + segment2_ms = float(r2.timings.get("e2e_latency_ms", 0.0)) + segment3_ms = float(r3.timings.get("e2e_latency_ms", 0.0)) + print(f"[GPU {self.gpu_id}] Startup warmup complete: " + f"segment1={segment1_ms:.0f}ms, " + f"segment2={segment2_ms:.0f}ms, " + f"segment3={segment3_ms:.0f}ms, total={warmup_total_ms:.0f}ms") + return { + "warmup_segment1_ms": segment1_ms, + "warmup_segment2_ms": segment2_ms, + "warmup_segment3_ms": segment3_ms, + "warmup_total_ms": warmup_total_ms, + } diff --git a/apps/dreamverse/dreamverse/worker_ipc.py b/apps/dreamverse/dreamverse/worker_ipc.py new file mode 100644 index 0000000000..e11478d0f6 --- /dev/null +++ b/apps/dreamverse/dreamverse/worker_ipc.py @@ -0,0 +1,157 @@ +# pyright: reportMissingTypeArgument=false +"""Typed worker IPC events: replaces the fat ``Response`` dataclass. + +Each message kind from GPU worker → main process is its own small +dataclass; ``WorkerEvent`` is the union of them. Consumers dispatch +via ``match``/``case`` or ``isinstance`` — mirroring the existing +``StreamEvent`` pattern in ``av_streaming.py``. + +Invalid states are unrepresentable: a ``MediaChunk`` simply has no +``frames`` field, a ``StepComplete`` has no ``chunk_offset`` field, +and a ``JoinAck`` can't accidentally default to ``kind="step_result"`` +because there is no ``kind`` string. +""" +from __future__ import annotations + +from dataclasses import dataclass + +# ---- User-scoped events (carry user_id) ------------------------------------ + + +@dataclass(frozen=True) +class StepComplete: + """Generation step finished. Frames/audio were already sent via ``MediaChunk``.""" + user_id: str + segment_idx: int + timings: dict[str, float] + + +@dataclass(frozen=True) +class WorkerError: + """Any worker failure. ``user_id`` is None for system-scoped failures.""" + user_id: str | None + message: str + + +@dataclass(frozen=True) +class JoinAck: + user_id: str + + +@dataclass(frozen=True) +class LeaveAck: + user_id: str + + +@dataclass(frozen=True) +class ReloadAck: + user_id: str + + +@dataclass(frozen=True) +class WarmupComplete: + user_id: str | None + timings: dict[str, float] + + +# ---- Streaming events (carry user_id + segment_idx for routing) ------------ + + +@dataclass(frozen=True) +class MediaInit: + user_id: str + segment_idx: int + stream_id: str + mime: str + uses_shared_buffer: bool + + +@dataclass(frozen=True) +class MediaChunk: + """One fMP4 chunk. + + Either ``chunk`` (raw bytes) is set, or both ``chunk_offset`` and + ``chunk_length`` are set (read from the shared buffer). The + invariant is enforced in ``__post_init__``. + """ + user_id: str + segment_idx: int + stream_id: str + chunk: bytes | None = None + chunk_offset: int | None = None + chunk_length: int | None = None + uses_shared_buffer: bool = False + + def __post_init__(self) -> None: + has_bytes = self.chunk is not None + has_offset = (self.chunk_offset is not None and self.chunk_length is not None) + if has_bytes == has_offset: + raise ValueError("MediaChunk must carry either chunk bytes or " + "(chunk_offset + chunk_length), not both or neither") + + +@dataclass(frozen=True) +class MediaComplete: + user_id: str + segment_idx: int + stream_id: str + chunks: int + + +# ---- System events (no user_id) -------------------------------------------- + + +@dataclass(frozen=True) +class InitAck: + success: bool + error: str | None = None + + +@dataclass(frozen=True) +class ShutdownAck: + pass + + +WorkerEvent = (StepComplete + | WorkerError + | JoinAck + | LeaveAck + | ReloadAck + | WarmupComplete + | MediaInit + | MediaChunk + | MediaComplete + | InitAck + | ShutdownAck) + +# ---- Command payloads (main process → worker) ------------------------------ +# +# The envelope (``Command`` + ``CommandType``) lives in ``gpu_pool.py`` +# alongside the enum; only the typed payloads live here so they sit +# next to the response-side types. Commands without a payload +# (``INIT``, ``SHUTDOWN``, ``USER_JOIN``, ``USER_LEAVE``) leave +# ``Command.payload`` as ``None``. + + +@dataclass(frozen=True) +class UserStepPayload: + prompt: str + segment_idx: int + image_path: str | None + reset_conditioning: bool + + +@dataclass(frozen=True) +class WarmupPayload: + prompt: str + + +@dataclass(frozen=True) +class ReloadModelPayload: + # ``model_config`` stays a dict because ``MODEL_REGISTRY`` values + # are dicts shaped by the external config module; typing that dict + # is a separate refactor. + model_config: dict + + +CommandPayload = UserStepPayload | WarmupPayload | ReloadModelPayload diff --git a/apps/dreamverse/gpu-pool.drawio b/apps/dreamverse/gpu-pool.drawio new file mode 100644 index 0000000000..ea9fe67b56 --- /dev/null +++ b/apps/dreamverse/gpu-pool.drawio @@ -0,0 +1,577 @@ +<mxfile host="65bd71144e"> + <diagram id="gpu-pool" name="gpu_pool.py"> + <mxGraphModel dx="1687" dy="3380" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1300" pageHeight="1700" math="0" shadow="0"> + <root> + <mxCell id="0"/> + <mxCell id="1" parent="0"/> + <mxCell id="title" value="gpu_pool.py — runtime architecture" style="text;html=1;align=center;fontSize=18;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="350" y="-150" width="600" height="30" as="geometry"/> + </mxCell> + <mxCell id="client" value="Client (WebSocket)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="560" y="-60" width="180" height="60" as="geometry"/> + </mxCell> + <mxCell id="pool" value="GPUPool" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="560" y="60" width="180" height="60" as="geometry"/> + </mxCell> + <mxCell id="slot_wrap" value="GPUSlot (one per GPU)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e6f2e0;strokeColor=#82b366;strokeWidth=2;verticalAlign=top;align=left;spacingLeft=15;spacingTop=8;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="40" y="216" width="1220" height="455" as="geometry"/> + </mxCell> + <mxCell id="m_start" value="start()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="110" y="270" width="60" height="30" as="geometry"/> + </mxCell> + <mxCell id="m_shutdown" value="shutdown()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="180" y="270" width="70" height="30" as="geometry"/> + </mxCell> + <mxCell id="m_join" value="join_user()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="499" y="260" width="70" height="40" as="geometry"/> + </mxCell> + <mxCell id="m_step" value="user_step()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="580" y="260" width="80" height="40" as="geometry"/> + </mxCell> + <mxCell id="m_leave" value="leave_user()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="670" y="255" width="80" height="45" as="geometry"/> + </mxCell> + <mxCell id="m_send" value="_send_command()<br><br>puts on command_queue,<br>blocks on response_queue.get<div><br></div><div><b>**for user agonistic commands</b></div>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;fontStyle=2;" parent="1" vertex="1"> + <mxGeometry x="79" y="370" width="230" height="80" as="geometry"/> + </mxCell> + <mxCell id="m_send_tagged" value="_send_command_tagged()<br><br>creates Future; registers in<br>_pending_futures[user_id];<br>ensures response reader; <b>awaits future</b><div><br></div><div><b>**for user specific commands</b></div>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;fontStyle=2;" parent="1" vertex="1"> + <mxGeometry x="350" y="374" width="300" height="90" as="geometry"/> + </mxCell> + <mxCell id="m_reader" value="_response_reader()<br><br>a while loop that reads response_queue;<br>dispatch WorkerEvent:<br>• media → _stream_queues<br>• acks/errors → _pending_futures" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;align=left;spacingLeft=8;spacingTop=4;fontStyle=2;" parent="1" vertex="1"> + <mxGeometry x="800" y="360" width="340" height="110" as="geometry"/> + </mxCell> + <mxCell id="c_futures" value="«dict» _pending_futures {user_id → asyncio.Future} created by _send_command_tagged; resolved + popped by _response_reader; cleared on model reload or leave" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="300" y="524" width="340" height="110" as="geometry"/> + </mxCell> + <mxCell id="c_streams" value="«dict» _stream_queues:&nbsp;<div><span style="color: rgb(63, 63, 63); background-color: transparent;">each is a queue of metadata that tell the consumer which part of the shared&nbsp;</span></div><div><div><font color="#000000">stream buffer to read next.<br></font><br>{user_id → asyncio.Queue}<br><br>created by register_stream_queue()<br>written by _response_reader (MediaInit / Chunk / Complete);<br>consumed by&nbsp; AV loop → ws.send_bytes / send_json;<br>popped by leave_user(); cleared on reload</div></div>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;align=left;spacingLeft=8;spacingTop=4;" parent="1" vertex="1"> + <mxGeometry x="720" y="524" width="460" height="126" as="geometry"/> + </mxCell> + <mxCell id="div_line" value="" style="line;strokeColor=#888888;strokeWidth=1;dashed=1;" parent="1" vertex="1"> + <mxGeometry x="40" y="690" width="1220" height="10" as="geometry"/> + </mxCell> + <mxCell id="div_label" value="── process boundary (mp.Queue + mp.RawArray) ──" style="text;html=1;align=center;fontSize=11;fontColor=#666666;fontStyle=2;" parent="1" vertex="1"> + <mxGeometry x="450" y="688" width="400" height="20" as="geometry"/> + </mxCell> + <mxCell id="cmdq" value="command_queue" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="79" y="720" width="200" height="100" as="geometry"/> + </mxCell> + <mxCell id="respq" value="response_queue" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="520" y="720" width="200" height="100" as="geometry"/> + </mxCell> + <mxCell id="sharedbuf" value="shared_stream_buffer (mp.RawArray, 256 MiB) zero-copy mp4 bytes" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" parent="1" vertex="1"> + <mxGeometry x="900" y="720" width="280" height="100" as="geometry"/> + </mxCell> + <mxCell id="worker_ipc" value="worker_ipc.py — shared types (imported on both sides of the process boundary) Command(type: CommandType, payload: CommandPayload | None, user_id: str | None) CommandPayload = UserStepPayload | WarmupPayload | ReloadModelPayload WorkerEvent = StepComplete | WorkerError | JoinAck | LeaveAck | ReloadAck | WarmupComplete | MediaInit | MediaChunk | MediaComplete | InitAck | ShutdownAck" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0e0e0;strokeColor=#666666;fontSize=10;align=left;spacingLeft=10;spacingTop=6;fontFamily=monospace;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="80" y="860" width="1140" height="80" as="geometry"/> + </mxCell> + <mxCell id="worker_wrap" value="Worker Subprocess - in each GPUSlot" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fef1e0;strokeColor=#d79b00;strokeWidth=2;verticalAlign=top;align=left;spacingLeft=15;spacingTop=8;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="73" y="1071" width="1140" height="360" as="geometry"/> + </mxCell> + <mxCell id="dispatcher" value="command dispatcher gpu_worker_process() branches on CommandType; asserts payload type INIT / WARMUP / RELOAD_MODEL USER_JOIN / USER_STEP / USER_LEAVE SHUTDOWN" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=11;align=left;spacingLeft=10;spacingTop=8;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="120" y="1120" width="240" height="120" as="geometry"/> + </mxCell> + <mxCell id="do_step" value="VideoGenerationWorker.generate_step() video_generation.py:380 reads + updates ContinuationState, calls generator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10;spacingTop=8;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="460" y="1120" width="240" height="120" as="geometry"/> + </mxCell> + <mxCell id="stream_av" value="stream_fmp4() av_streaming.py:121 trims overlap, pipes to ffmpeg, publishes StreamInit / StreamChunk / StreamComplete via callback" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#b1d8d7;strokeColor=#23445d;fontSize=11;align=left;spacingLeft=10;spacingTop=8;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="800" y="1120" width="260" height="120" as="geometry"/> + </mxCell> + <mxCell id="Ot8BU52QTIb4EhyRSe7I-2" value="" style="edgeStyle=none;html=1;" parent="1" source="generator" target="Ot8BU52QTIb4EhyRSe7I-1" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="generator" value="VideoGenerator (fastvideo) LTX2 DiT + refine upsampler FP4 quant, torch.compile owned by VideoGenerationWorker video_generation.py:211" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" parent="1" vertex="1"> + <mxGeometry x="460" y="1300" width="240" height="100" as="geometry"/> + </mxCell> + <mxCell id="ffmpeg" value="ffmpeg subprocess libx264 / *_nvenc fragmented mp4" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d79b00;fontSize=11;" parent="1" vertex="1"> + <mxGeometry x="800" y="1300" width="260" height="100" as="geometry"/> + </mxCell> + <mxCell id="caches" value="ContinuationState video_generation.py:89 • video_images: list[PIL.Image] • audio_latents: torch.Tensor (CPU) carried across segments" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;align=left;spacingLeft=10;spacingTop=8;" parent="1" vertex="1"> + <mxGeometry x="120" y="1300" width="240" height="100" as="geometry"/> + </mxCell> + <mxCell id="e_cp" value="acquire" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#6c8ebf;endArrow=classic;fontSize=11;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="client" target="pool" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_ps" value="assigns slot" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#82b366;endArrow=classic;fontSize=11;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="pool" target="slot_wrap" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_start_send" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#82b366;endArrow=classic;fontSize=10;" parent="1" target="m_send" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="138" y="300" as="sourcePoint"/> + <Array as="points"> + <mxPoint x="138" y="380"/> + <mxPoint x="138" y="380"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_join_tag" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#82b366;endArrow=classic;fontSize=10;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" parent="1" source="m_join" target="m_send_tagged" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="531.6999999999998" y="400.3600000000001" as="targetPoint"/> + <Array as="points"> + <mxPoint x="532" y="300"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_step_tag" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#82b366;endArrow=classic;fontSize=10;exitX=0.1;exitY=1;exitDx=0;exitDy=0;" parent="1" source="m_step" target="m_send_tagged" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="588" y="400" as="targetPoint"/> + <Array as="points"> + <mxPoint x="588" y="337"/> + <mxPoint x="590" y="337"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_leave_tag" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#82b366;endArrow=classic;fontSize=10;exitX=0.1;exitY=1;exitDx=0;exitDy=0;entryX=0.874;entryY=-0.002;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="m_leave" target="m_send_tagged" edge="1"> + <mxGeometry x="-0.1388" relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="680" y="300"/> + <mxPoint x="680" y="330"/> + <mxPoint x="612" y="330"/> + </Array> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="e_tag_futures" value="register future" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d6b656;endArrow=classic;fontSize=10;exitX=0.397;exitY=0.989;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" source="m_send_tagged" target="c_futures" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_reader_futures" value="<b>set future result</b>" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d6b656;endArrow=classic;fontSize=10;exitX=0.008;exitY=0.908;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" source="m_reader" target="c_futures" edge="1"> + <mxGeometry x="-0.325" relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="803" y="470"/> + <mxPoint x="670" y="470"/> + <mxPoint x="670" y="557"/> + </Array> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="e_reader_streams" value="put(MediaEvent)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d6b656;endArrow=classic;fontSize=10;exitX=0.146;exitY=1.008;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.282;entryY=0.021;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="m_reader" target="c_streams" edge="1"> + <mxGeometry relative="1" as="geometry"> + <Array as="points"/> + <mxPoint x="850" y="522" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="e_send_cmdq" value="put Command" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=11;entryX=0.35;entryY=0;entryDx=0;entryDy=0;" parent="1" source="m_send" target="cmdq" edge="1"> + <mxGeometry relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="150" y="585"/> + <mxPoint x="149" y="585"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_tag_cmdq" value="put Command" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=11;exitX=0.022;exitY=0.97;exitDx=0;exitDy=0;entryX=0.85;entryY=0;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" source="m_send_tagged" target="cmdq" edge="1"> + <mxGeometry x="0.1755" relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="357" y="490"/> + <mxPoint x="250" y="490"/> + <mxPoint x="250" y="592"/> + <mxPoint x="249" y="592"/> + </Array> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="e_respq_reader" value="WorkerEvent" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=11;exitX=0.753;exitY=0.056;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" source="respq" target="m_reader" edge="1"> + <mxGeometry x="0.7113" relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="671" y="720"/> + <mxPoint x="670" y="720"/> + <mxPoint x="670" y="415"/> + </Array> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="e_respq_send" value="blocking .get" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;dashed=1;fontSize=10;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="respq" target="m_send" edge="1"> + <mxGeometry x="-0.2187" relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="520" y="680"/> + <mxPoint x="200" y="680"/> + </Array> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="e_cd" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=11;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;" parent="1" source="cmdq" edge="1" target="dispatcher"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="310" y="1120" as="targetPoint"/> + <Array as="points"> + <mxPoint x="179" y="970"/> + <mxPoint x="180" y="970"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_dds" value="USER_STEP" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d79b00;endArrow=classic;fontSize=10;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="dispatcher" target="do_step" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_dr" value="StepComplete / ack types" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=10;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.15;entryY=1;entryDx=0;entryDy=0;" parent="1" source="dispatcher" target="respq" edge="1"> + <mxGeometry relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="240" y="1000"/> + <mxPoint x="550" y="1000"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_dsg" value="generator.generate_video()" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#9673a6;endArrow=classic;fontSize=10;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="do_step" target="generator" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_dscache" value="read / write" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d6b656;endArrow=classic;startArrow=classic;fontSize=10;exitX=0;exitY=0.8;exitDx=0;exitDy=0;entryX=1;entryY=0.2;entryDx=0;entryDy=0;" parent="1" source="do_step" target="caches" edge="1"> + <mxGeometry relative="1" as="geometry"/> + <Array as="points"> + <mxPoint x="420" y="1058"/> + <mxPoint x="420" y="1150"/> + </Array> + </mxCell> + <mxCell id="e_dsstream" value="frames + audio" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d79b00;endArrow=classic;fontSize=10;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="do_step" target="stream_av" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_sf" value="rawvideo + wav" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#d79b00;endArrow=classic;fontSize=10;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="stream_av" target="ffmpeg" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_ssb" value="write mp4 bytes at shared_write_offset" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=10;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="stream_av" target="sharedbuf" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="e_sbs" value="bytes from the buffer are sent through websocket&nbsp;<div>based on info from the stream queue</div>" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;dashed=1;fontSize=10;exitX=0.591;exitY=0.027;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;exitPerimeter=0;" parent="1" source="sharedbuf" target="c_streams" edge="1"> + <mxGeometry x="0.0048" relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="1066" y="720"/> + </Array> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="e_scl" value="session AV loop (main.py:2124): ws.send_bytes(chunk) — binary mp4 frames ws.send_json({type: media_init | media_segment_complete | ...})" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#6c8ebf;endArrow=classic;fontSize=10;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="c_streams" target="client" edge="1"> + <mxGeometry x="0.0003" relative="1" as="geometry"> + <mxPoint as="offset"/> + <Array as="points"> + <mxPoint x="1310" y="577"/> + <mxPoint x="1310" y="-30"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="e_sr" value="MediaChunk (offset, len)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=10;exitX=0.1;exitY=0;exitDx=0;exitDy=0;entryX=0.85;entryY=1;entryDx=0;entryDy=0;" parent="1" source="stream_av" target="respq" edge="1"> + <mxGeometry relative="1" as="geometry"/> + <Array as="points"> + <mxPoint x="826" y="880"/> + <mxPoint x="690" y="880"/> + </Array> + </mxCell> + <mxCell id="legend" value="Legend ■ blue client / external ■ green main-process pool/slot (methods — italic label) ■ yellow containers (routing state) ■ red IPC primitives (mp.Queue, mp.RawArray) Worker subprocess modules: ■ orange gpu_pool.py (dispatcher) ■ lavender video_generation.py ■ teal av_streaming.py ■ gray worker_ipc.py (shared types) Flow: client → pool → slot → _send_command(_tagged) → command_queue → dispatcher → generate_step() → stream_fmp4() → ffmpeg → shared_buf + response_queue → _response_reader → futures / stream_queues → client awaits (via main.py AV loop)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;fontSize=11;align=left;spacingLeft=10;spacingTop=8;" parent="1" vertex="1"> + <mxGeometry x="39" y="-200" width="270" height="380" as="geometry"/> + </mxCell> + <mxCell id="Ot8BU52QTIb4EhyRSe7I-1" value="FastVideo video_generator" style="whiteSpace=wrap;html=1;fontSize=11;fillColor=#e1d5e7;strokeColor=#9673a6;rounded=1;" parent="1" vertex="1"> + <mxGeometry x="520" y="1470" width="120" height="60" as="geometry"/> + </mxCell> + <mxCell id="5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#82b366;endArrow=classic;fontSize=10;exitX=0.421;exitY=1.01;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.569;entryY=0.02;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="m_shutdown" target="m_send"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="260" y="300" as="sourcePoint"/> + <mxPoint x="260" y="400" as="targetPoint"/> + <Array as="points"/> + </mxGeometry> + </mxCell> + <mxCell id="6" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.983;entryY=0.021;entryDx=0;entryDy=0;entryPerimeter=0;strokeColor=#61A25D;" edge="1" parent="1" source="m_join" target="m_send"> + <mxGeometry relative="1" as="geometry"> + <Array as="points"> + <mxPoint x="534" y="320"/> + <mxPoint x="306" y="320"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="11" value="reload model" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6"> + <mxGeometry x="0.1965" relative="1" as="geometry"> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="9" value="" style="group" vertex="1" connectable="0" parent="1"> + <mxGeometry x="1340" y="750" width="240" height="40" as="geometry"/> + </mxCell> + <mxCell id="7" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=6.353056066176691;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="9"> + <mxGeometry y="4" width="21.39" height="22" as="geometry"/> + </mxCell> + <mxCell id="8" value="Queue: owned by GPUSlot, passed to worker process as argument." style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="9"> + <mxGeometry x="21.3900000000001" width="218.61" height="40" as="geometry"/> + </mxCell> + </root> + </mxGraphModel> + </diagram> + <diagram id="gpu-pool-boot" name="boot & ws"> + <mxGraphModel grid="1" page="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" pageScale="1" pageWidth="1300" pageHeight="1900" math="0" shadow="0"> + <root> + <mxCell id="0"/> + <mxCell id="1" parent="0"/> + <mxCell id="b_title" value="gpu_pool.py — boot flow & websocket session" style="text;html=1;align=center;fontSize=18;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="350" y="20" width="600" height="30" as="geometry"/> + </mxCell> + <mxCell id="secA" value="A. Call chain" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#039be5;strokeWidth=2;verticalAlign=top;align=left;spacingLeft=15;spacingTop=8;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="40" y="70" width="400" height="420" as="geometry"/> + </mxCell> + <mxCell id="a1" value="$ uv run dreamverse-server" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#039be5;fontSize=12;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="70" y="120" width="340" height="40" as="geometry"/> + </mxCell> + <mxCell id="a2" value="server_entry.cli() (pyproject.toml [project.scripts]) guards missing fastvideo extra" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#039be5;fontSize=11;align=left;spacingLeft=10;" parent="1" vertex="1"> + <mxGeometry x="70" y="180" width="340" height="70" as="geometry"/> + </mxCell> + <mxCell id="a3" value="main.cli() (main.py:2325) argparse --host/--port uvicorn.run(app, host, port)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#039be5;fontSize=11;align=left;spacingLeft=10;" parent="1" vertex="1"> + <mxGeometry x="70" y="270" width="340" height="80" as="geometry"/> + </mxCell> + <mxCell id="a4" value="FastAPI(lifespan=lifespan) (main.py:147) on startup → enter lifespan ctx → Section B" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#039be5;fontSize=11;align=left;spacingLeft=10;" parent="1" vertex="1"> + <mxGeometry x="70" y="370" width="340" height="90" as="geometry"/> + </mxCell> + <mxCell id="ea12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#039be5;endArrow=classic;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="a1" target="a2" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="ea23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#039be5;endArrow=classic;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="a2" target="a3" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="ea34" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#039be5;endArrow=classic;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="a3" target="a4" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="secB" value="B. lifespan() zoom (main.py:116-144)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#8e24aa;strokeWidth=2;verticalAlign=top;align=left;spacingLeft=15;spacingTop=8;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="480" y="70" width="780" height="420" as="geometry"/> + </mxCell> + <mxCell id="b1" value="1. gpu_ids = get_available_gpus() respects FASTVIDEO_GPU_COUNT env" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8e24aa;fontSize=11;align=left;spacingLeft=10;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="510" y="115" width="720" height="45" as="geometry"/> + </mxCell> + <mxCell id="b2" value="2. gpu_pool = GPUPool(gpu_ids) creates one GPUSlot per GPU (not yet started)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8e24aa;fontSize=11;align=left;spacingLeft=10;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="510" y="170" width="720" height="45" as="geometry"/> + </mxCell> + <mxCell id="b3" value="3. await gpu_pool.initialize() asyncio.create_task(_init_gpu(id)) for each GPU returns immediately — GPUs init in background → Section C" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff9c4;strokeColor=#8e24aa;fontSize=11;align=left;spacingLeft=10;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="510" y="225" width="720" height="60" as="geometry"/> + </mxCell> + <mxCell id="b4" value="4. prompt_enhancer = PromptEnhancer() 5. session_event_logger = SessionEventLogger(...) 6. prompt_safety_filter = PromptSafetyFilter() (optional)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8e24aa;fontSize=11;align=left;spacingLeft=10;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="510" y="295" width="720" height="55" as="geometry"/> + </mxCell> + <mxCell id="byield" value="yield server is now accepting HTTP + WS /healthz → 200 /readyz → 503 until GPUs warm up" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#388e3c;fontSize=11;align=left;spacingLeft=10;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="510" y="360" width="720" height="55" as="geometry"/> + </mxCell> + <mxCell id="bshut" value="[on shutdown] await gpu_pool.shutdown()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffcdd2;strokeColor=#c62828;fontSize=11;align=left;spacingLeft=10;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="510" y="425" width="720" height="35" as="geometry"/> + </mxCell> + <mxCell id="eab" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#666;endArrow=classic;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.2;entryDx=0;entryDy=0;" parent="1" source="a4" target="b1" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="secC" value="C. GPUSlot.start() zoom — runs in background per GPU (gpu_pool.py:426)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ef6c00;strokeWidth=2;verticalAlign=top;align=left;spacingLeft=15;spacingTop=8;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="40" y="530" width="1220" height="520" as="geometry"/> + </mxCell> + <mxCell id="cparent" value="Parent (main) process" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffe0b2;strokeColor=#ef6c00;fontSize=12;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;" parent="1" vertex="1"> + <mxGeometry x="70" y="580" width="380" height="440" as="geometry"/> + </mxCell> + <mxCell id="cp1" value="mp.get_context('spawn') cmd_q, resp_q = ctx.Queue(), ctx.Queue() (optional) shared_stream_buffer = mp.RawArray('B', 256 MiB)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#ef6c00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="85" y="620" width="350" height="60" as="geometry"/> + </mxCell> + <mxCell id="cp2" value="ctx.Process(target=gpu_worker_process, args=(gpu_id, cuda_device, cmd_q, resp_q, ...)) process.start()" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#ef6c00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="85" y="690" width="350" height="60" as="geometry"/> + </mxCell> + <mxCell id="cp3" value="_send_command(Command(INIT), timeout=600s) await response on resp_q" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#ef6c00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="85" y="760" width="350" height="50" as="geometry"/> + </mxCell> + <mxCell id="cp4" value="if STARTUP_WARMUP_ENABLED: _send_command(Command(WARMUP, prompt=...), timeout=STARTUP_WARMUP_TIMEOUT)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#ef6c00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="85" y="820" width="350" height="60" as="geometry"/> + </mxCell> + <mxCell id="cp5" value="slot.ready = True" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#388e3c;fontSize=11;fontStyle=1;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="85" y="890" width="350" height="35" as="geometry"/> + </mxCell> + <mxCell id="cp6" value="parent now enters pool as a ready slot; /readyz flips to 200 once ≥1 slot is ready" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#888;fontSize=10;align=left;spacingLeft=8;fontStyle=2;" parent="1" vertex="1"> + <mxGeometry x="85" y="935" width="350" height="45" as="geometry"/> + </mxCell> + <mxCell id="cipc" value="IPC" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffcdd2;strokeColor=#b71c1c;fontSize=12;fontStyle=1;verticalAlign=top;align=center;spacingTop=5;" parent="1" vertex="1"> + <mxGeometry x="480" y="580" width="150" height="440" as="geometry"/> + </mxCell> + <mxCell id="ccmdq" value="command_queue (mp.Queue)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" parent="1" vertex="1"> + <mxGeometry x="490" y="650" width="130" height="80" as="geometry"/> + </mxCell> + <mxCell id="crespq" value="response_queue (mp.Queue)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" parent="1" vertex="1"> + <mxGeometry x="490" y="760" width="130" height="80" as="geometry"/> + </mxCell> + <mxCell id="cshared" value="shared_stream_buffer (mp.RawArray)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" parent="1" vertex="1"> + <mxGeometry x="490" y="870" width="130" height="80" as="geometry"/> + </mxCell> + <mxCell id="cworker" value="Worker subprocess (gpu_worker_process, gpu_pool.py:151)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fef1e0;strokeColor=#d79b00;fontSize=12;fontStyle=1;verticalAlign=top;align=left;spacingLeft=10;spacingTop=5;" parent="1" vertex="1"> + <mxGeometry x="660" y="580" width="580" height="440" as="geometry"/> + </mxCell> + <mxCell id="cw1" value="os.environ['CUDA_VISIBLE_DEVICES'] = cuda_device os.environ['FASTVIDEO_ATTENTION_BACKEND'] = 'FLASH_ATTN' must be set BEFORE importing torch" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d79b00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="675" y="620" width="550" height="65" as="geometry"/> + </mxCell> + <mxCell id="cw2" value="from fastvideo.entrypoints.video_generator import VideoGenerator from fastvideo.models.dits.ltx2 import DEFAULT_LTX2_AUDIO_* ** Dreamverse reaches into fastvideo internals here **" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe0b2;strokeColor=#d79b00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="675" y="695" width="550" height="60" as="geometry"/> + </mxCell> + <mxCell id="cw3" value="on Command(INIT): VideoGenerationWorker.initialize() (video_generation.py:247) maybe_download_model(model_id) VideoGenerator.from_pretrained(path, FP4Config, PipelineConfig) load audio VAE, resolve refine upsampler resp_q.put(InitAck(success=True))" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d79b00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="675" y="765" width="550" height="95" as="geometry"/> + </mxCell> + <mxCell id="cw4" value="on Command(WARMUP) with WarmupPayload: VideoGenerationWorker.warmup(payload.prompt) (video_generation.py:518) two synthetic segments prime caches + torch.compile resp_q.put(WarmupComplete(timings=...))" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d79b00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="675" y="870" width="550" height="55" as="geometry"/> + </mxCell> + <mxCell id="cw5" value="enter main worker loop → waits for JOIN_USER / USER_STEP / LEAVE" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#c8e6c9;strokeColor=#388e3c;fontSize=11;fontStyle=1;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="675" y="935" width="550" height="40" as="geometry"/> + </mxCell> + <mxCell id="cea1" value="Command" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=10;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="cp3" target="ccmdq" edge="1"> + <mxGeometry relative="1" as="geometry"/> + </mxCell> + <mxCell id="cea2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="ccmdq" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="675" y="780" as="targetPoint"/> + <Array as="points"> + <mxPoint x="648" y="690"/> + <mxPoint x="648" y="780"/> + <mxPoint x="675" y="780"/> + </Array> + </mxGeometry> + </mxCell> + <mxCell id="cea3" value="WorkerEvent" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;fontSize=10;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.969;entryY=0.798;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="crespq" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="684" y="825.5" as="sourcePoint"/> + <mxPoint x="629" y="813" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="cea4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#b85450;endArrow=classic;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.995;entryY=0.887;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="cp3" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="490" y="804" as="sourcePoint"/> + <mxPoint x="435" y="789" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="secD" value="D. WebSocket session — after startup" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e8f5e9;strokeColor=#2e7d32;strokeWidth=2;verticalAlign=top;align=left;spacingLeft=15;spacingTop=8;fontSize=13;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="40" y="1080" width="1220" height="820" as="geometry"/> + </mxCell> + <mxCell id="dc_head" value="Browser / WS" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="80" y="1130" width="160" height="40" as="geometry"/> + </mxCell> + <mxCell id="ds_head" value="main.py / Session" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="290" y="1130" width="180" height="40" as="geometry"/> + </mxCell> + <mxCell id="dp_head" value="GPUPool" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="520" y="1130" width="160" height="40" as="geometry"/> + </mxCell> + <mxCell id="dsl_head" value="GPUSlot" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e6f2e0;strokeColor=#82b366;fontSize=11;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="730" y="1130" width="160" height="40" as="geometry"/> + </mxCell> + <mxCell id="dw_head" value="Worker subprocess" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fef1e0;strokeColor=#d79b00;fontSize=11;fontStyle=1;" parent="1" vertex="1"> + <mxGeometry x="940" y="1130" width="200" height="40" as="geometry"/> + </mxCell> + <mxCell id="dc_life" style="endArrow=none;html=1;strokeColor=#6c8ebf;dashed=1;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="160" y="1175" as="sourcePoint"/> + <mxPoint x="160" y="1885" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="ds_life" style="endArrow=none;html=1;strokeColor=#82b366;dashed=1;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="380" y="1175" as="sourcePoint"/> + <mxPoint x="380" y="1885" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dp_life" style="endArrow=none;html=1;strokeColor=#82b366;dashed=1;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="600" y="1175" as="sourcePoint"/> + <mxPoint x="600" y="1885" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dsl_life" style="endArrow=none;html=1;strokeColor=#82b366;dashed=1;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="810" y="1175" as="sourcePoint"/> + <mxPoint x="810" y="1885" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dw_life" style="endArrow=none;html=1;strokeColor=#d79b00;dashed=1;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="1040" y="1175" as="sourcePoint"/> + <mxPoint x="1040" y="1885" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm1" value="1. WS connect /ws + session_init_v2" style="endArrow=classic;html=1;strokeColor=#6c8ebf;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="160" y="1200" as="sourcePoint"/> + <mxPoint x="380" y="1200" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm2" value="2. pool.acquire(client_id, ws)" style="endArrow=classic;html=1;strokeColor=#82b366;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="380" y="1240" as="sourcePoint"/> + <mxPoint x="600" y="1240" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm3a" value="3a. if slot free: (gpu_id, slot)" style="endArrow=classic;html=1;strokeColor=#82b366;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="600" y="1280" as="sourcePoint"/> + <mxPoint x="380" y="1280" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm3b" value="3b. else: enqueue, emit queue_status until a slot frees" style="endArrow=classic;html=1;strokeColor=#c62828;fontSize=10;dashed=1;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="600" y="1320" as="sourcePoint"/> + <mxPoint x="160" y="1320" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm4" value="4. emit gpu_assigned" style="endArrow=classic;html=1;strokeColor=#6c8ebf;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="380" y="1360" as="sourcePoint"/> + <mxPoint x="160" y="1360" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm5" value="5. slot._send_command_tagged(Command(USER_JOIN))" style="endArrow=classic;html=1;strokeColor=#82b366;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="380" y="1400" as="sourcePoint"/> + <mxPoint x="810" y="1400" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm6" value="6. command_queue.put(JOIN_USER)" style="endArrow=classic;html=1;strokeColor=#b85450;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="810" y="1440" as="sourcePoint"/> + <mxPoint x="1040" y="1440" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dssteady" value="── steady state: per prompt ──" style="text;html=1;align=center;fontSize=11;fontColor=#666;fontStyle=2;" parent="1" vertex="1"> + <mxGeometry x="300" y="1470" width="600" height="25" as="geometry"/> + </mxCell> + <mxCell id="dm7" value="7. append_prompt / simple_generate" style="endArrow=classic;html=1;strokeColor=#6c8ebf;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="160" y="1510" as="sourcePoint"/> + <mxPoint x="380" y="1510" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm9" value="8. slot._send_command_tagged(Command(USER_STEP, payload=UserStepPayload(prompt, segment_idx, image_path, reset_conditioning)))" style="endArrow=classic;html=1;strokeColor=#82b366;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="380" y="1560" as="sourcePoint"/> + <mxPoint x="810" y="1560" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm10" value="9. command_queue.put(USER_STEP)" style="endArrow=classic;html=1;strokeColor=#b85450;fontSize=10;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="810" y="1610" as="sourcePoint"/> + <mxPoint x="1040" y="1610" as="targetPoint"/> + </mxGeometry> + </mxCell> + <mxCell id="dm11a" value="10a. worker runs: VideoGenerationWorker.generate_step() (video_generation.py:380) → generator.generate_video() → updates ContinuationState then stream_fmp4() (av_streaming.py:121) → ffmpeg (rawvideo+wav → fmp4)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe0b2;strokeColor=#d79b00;fontSize=10;align=left;spacingLeft=8;fontFamily=monospace;" parent="1" vertex="1"> + <mxGeometry x="955" y="1640" width="180" height="70" as="geometry"/> + </mxCell> + <mxCell id="dm11" value="10b. resp_q.put(MediaInit / MediaChunk / MediaComplete / StepComplete)" style="endArrow=classic;html=1;strokeColor=#b85450;fontSize=10;labelBackgroundColor=#ffffff;" parent="1" edge="1"> + <mxGeometry x="0.0435" relative="1" as="geometry"> + <mxPoint x="1092" y="1733" as="sourcePoint"/> + <mxPoint x="862" y="1733" as="targetPoint"/> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="dm12" value="11. _response_reader isinstance-dispatches: MediaChunk/etc → _stream_queues[user_id]; StepComplete → _pending_futures" style="endArrow=classic;html=1;strokeColor=#d6b656;fontSize=10;" parent="1" edge="1"> + <mxGeometry x="0.0014" relative="1" as="geometry"> + <mxPoint x="810" y="1760" as="sourcePoint"/> + <mxPoint x="380" y="1760" as="targetPoint"/> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="dm13a" value="12a. binary WS frame (mp4 bytes)" style="endArrow=classic;html=1;strokeColor=#6c8ebf;fontSize=10;labelBackgroundColor=#ffffff;" parent="1" edge="1"> + <mxGeometry x="-0.0909" relative="1" as="geometry"> + <mxPoint x="380" y="1795" as="sourcePoint"/> + <mxPoint x="160" y="1795" as="targetPoint"/> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="dm13b" value="12b. JSON WS event (media_init, media_segment_complete, prompt_ready, …)" style="endArrow=classic;html=1;strokeColor=#6c8ebf;fontSize=10;labelBackgroundColor=#ffffff;" parent="1" edge="1"> + <mxGeometry x="-0.0909" y="5" relative="1" as="geometry"> + <mxPoint x="380" y="1825" as="sourcePoint"/> + <mxPoint x="160" y="1825" as="targetPoint"/> + <mxPoint as="offset"/> + </mxGeometry> + </mxCell> + <mxCell id="dm14" value="13. on WS close: pool.release(client_id) → Command(USER_LEAVE) → LeaveAck" style="endArrow=classic;html=1;strokeColor=#c62828;fontSize=10;dashed=1;labelBackgroundColor=#ffffff;" parent="1" edge="1"> + <mxGeometry relative="1" as="geometry"> + <mxPoint x="160" y="1865" as="sourcePoint"/> + <mxPoint x="1040" y="1865" as="targetPoint"/> + </mxGeometry> + </mxCell> + </root> + </mxGraphModel> + </diagram> +</mxfile> \ No newline at end of file diff --git a/apps/dreamverse/gpu-pool.svg b/apps/dreamverse/gpu-pool.svg new file mode 100644 index 0000000000..aa379cf605 --- /dev/null +++ b/apps/dreamverse/gpu-pool.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="background: transparent; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1542px" height="1731px" viewBox="-0.5 -0.5 1542 1731" content="<mxfile><diagram id="gpu-pool" name="gpu_pool.py">7V1pk5tGE/41qtKmyioOgdDHPZNNreONdx0nn1QjGEnECDCgPfLr37mAuQAdSLt+E9llS8Mc0P1MT09PdzOwL9cvP2cgXX1MAhgNLCN4GdhXA8syXW+C/sMlr7TEtj2DliyzMGC16oKH8B/ICstqmzCAuVCxSJKoCFOx0E/iGPqFUAayLHkWqy2SSBw1BUuoFDz4IFJLv4ZBsWKltmHUF36B4XJVDj0pr6xBWZsV5CsQJM9ckX09sC+zJCnot/XLJYww+UrC0HY3DVerO8tgXGzToAgL9FSk0ROINuwBl+lmliKKjtJXdGFwbQ0Qh6Zj9D3bxEW4hoSQ/iosEHk3GesgL15LChXwBQ1/sSrWESow0VcQhcsYfffRjcEMFSySuGCsNb3yN+sBN1CfhD3cE8xQ91wRe7KfYbKGRYbvmF21HUZlBrQPZlnwXLPNLXmz4jhmszLAkLKs+q5pib4wcupJ60chvnWZtpes2Bh+hfOHxP8GizOFflmyiQMYMFI8Y0I/pMDHV5/RpBJJuwij6DKJkoy0tQMAvYWPyvMiS75B7orre3C+kGhvH4f2jivR3lVJb3oa0rs9kB5jVyH8z/df7qvyvmjtQC8Y62jtWXPbdd+G1qckdR4lxYzQSUPvB3QRCxAE9iTGQiNFU98y0JU2yBs7swG6CwsarWygV0pxjYh5gekbIqF+ziRTkaScnIrgAkuwHN1CGC/vyK8r06mLHnH1K+80HB6LDEaLqMrhak3hWTx2nMN5vJ7lBchUUUZKhz1Lr91mlNHFMk/m2LgfjpimxJKJbmk5zsqC+LHaFEhviFWWsAv/Vq543VyZHI0rfyehyhFcONvkMPuXsmQ8nYos0axOOpaM+5koBVRXJswNcuVfyhJHniUaluj0hX5YEkHwpG45SOm/eZ64E4kp5crdxZR+FngYBwpPSOnMT9ZrEAeYLW6E6TFHOpy7LMhjyyXppshRJ0lMtt+k4ez7BqIerUu19jxC+5+qfgbzNIlzSBuMlrAoWwThU/OQZQl6SE09XdOy4KefFgnWRzHqMAWXSRzmSCes7z3nOp93DfijgZbXU61+QDwRZb2tWX6rtZYHsdePsMdwLcByCTuwzCptB2k/g6CAGKU3G2LswJUQWJcIKzDD5XjVl1vNUjQeIvdsQRrlA+eCLDvobp0r7TgwzknFeiKQryAg5hKyjZLwC55BSKYbHaMNq6eZQXkK/XDx3wTqz2RlT8bKDLJ1FqtpLzOIwU2ZPJVophW2mzgA3/YqxPZFI0oQ1SyjWIGCoZoHOlsidJ0EISJ74a9Q7a9J9g1m10+EuueaO6B2ynNiaIVBiMcnZe5gisuQyocGXtPB8vb2gKxLNzDLEjLFhX7kqf0fcLEEl4Cr0SntsQa41Ub6IFuryAsOuoNLa3COUOQX+NvFhZZ/LiIvISX/dVIJbJH9IH+N/TAZsdVgcqVvT5cN3Hj+SsCnrj9iA3QnSfREWgwsfJ9pkqZcB9IclEZD+nNGKhNVas0OXTIYJYCUYvHMad49oXWxWFi+1tYcuHPXeTfKtS2h07E0YvWI6KSSZzt0ilKKCDqwxmyI53navi4j0sUCc93vm4ToMJQx5xgSy/nQtYk6btT/n1EVY47k3pIA4gPXpMhAnJf0v6h7rTQXQORziAUlFrpM4TeSBZHEBQhAAWrpX1DqFCtI9ASk9qyJ9oCgR/pJia2RtqaV8hUFt54QiPz0wffUZjRFGLTk5ko814+MoIQ/KhUo3zARN4sFeaAiYWsd+i+GL8WoVfWiYzbqaF3i6HdCdCKNtlRpK9FS6rIC9HRr/HMWFgWMG0WSZQw/4oX3Ng6p3f8G/Xu52sTf6l/JOo1gAc/0N0bRUN6ZzG/j/I9al+CJ8JyPiHydv1JNnY5Fiv7OkUTUblUF8SrYH8hYkkgtRWkbuI6o0P4oknZidUvasc4wblru4ZIWUX8WhbFqXlJ4QWrptCzyqa6UB0aYIwHIVxX3eiCVdKjjTlWNSX+o08eaRCgF5lA9J8XzajrGmnD1BT1olvgwx1NrjkEMyHMM1ykTO6XKggo+g+fzLAOvZ2yKyl1JfNjRU8Bkv/lTbfI5jlY7lrZjLoKGgmbddszqQ21YB98V7khGNYmaaKFM8Vf/FeEbSWS7W87MqVC6m1cFlQ7waVPQaULKc8YCRyOcPB/qhdPcczANRSZag6MckkrGp0oS8cYnHa9Mowdm4aVQ5Za0wf2PXfwphLRSnJZfVKVEqprCM3ql1IVKZa7aaQ05GUe1ZwutvpbxMbw40+8E/4FZgtRp4ku1TsdE4yg0ZoMfAw09cX9qbMF93XlHL9x/JqakWZj6CvvrS6r3W7kPMYrXlOiZw3CdJllRa4nzpMBbmJw4KAp7GHUFbYDLJTtvwWPQ/RcrecQFBHEpeCXKKH/1npWhO57gOr8Rjxv8rdw3kNqIyUKVM3VkridMEeMLav9QwFQe4SvI1hul9DPRk4nXZ3WpGoE34ZXd467LXUHdN62IzW9V2a9JGJ/736rfd1hl5wvo0HwJvUWld2GTUpdUOxWuSG6KW/EjPDCHC1LWo74PDfxHNytFfWdbfZ/UEBT+sosbsA4jPAvXSZzk9P6OIe+lQ2dP56Vm6mwwfRwOsVmtdVSjWMO83MzrWfphQA92DGbdqNzZGpm8u9/aAi5MPZODyXRu1Fd+VL+1iS3w3DQm5pZMt/twTiwPDzRHGkyPRoV8JZ08xs7QDD4MHdg0YswzEKNG5em1LESJsCZ2BJAj+UmO6dJKVBLZrh3t9rfbx0FpwPh6/vnjl/vq5+fru0/nV7OPn66u77gmXx6uP89+/XT7W1WRlDw8Xt+LJXfX539ccw0ffvnyePXp62+9Ci6kLrkNhooS07I6saPgOpK3uCnpo6apU0m0RuI+dntBondW+gOpEsnPMIYZKMIkpsJqtKQFsHJjqpj6hOvPllUDrMXY5yTEQoe38iiO7t83acDOui8RgcN4Q7p4KEDBHDiq8wYQRbgaGyfJ+l38zMCBEx2Gpu7EBvLi934wNHbfFENszwKe1E0NvbJA+w8BLeCJbXQQZShSTPyIOqQUWbgm0g6RI8LLKNFEQ6oIE1P3AnUPlyJQ0s08CnMqJx/ISIJxmBYJJuJaPNFrtQb2RI50MfTwNqdXxM3NwAu0iLPs8dgJ3i3i5HNXPeL0BtceEPep8C6+ONbvj7fz8fXq9fMDnNx+sBT4KbyCwRKWlIgTYorlmNNIlzzZZD4sb1cQPQXIsMdY812xLvDIrYTNYISk3hMUBj6ERtKNNsh2utFBW8oFyAsixBv2h3ePf+JursLHSmxncEGM3kh+5wBPF16VubnHW9fvG4D3XJdkrmb+aoQUoJT4ZujGQBua+mhEuwJ1rjgIiW+9JhxDoFcnyZ0CvQ87RSlRJdyUxUbO7Vh0fIzC+Yvljitx+9MsRntvn6uxyMByjS3tmN3EOtWrLog/O+mCxxCJWp5pRWIfPPMB3RZIPNMoVB0zyJs2eKZwfkq0ZbgGS+YrYKDVthg4F/e3d6NbXIzdDfVtwSYIkxkSdojUrDEVDY8wzok4Gl6SSC2tewvIspCABvhZQnbMOSRI6tcf6jSq4DG2D6eVFHDmq3sH4H/fhJrwXH71TbJilSyTGETXdemFaNXg2NMcVArj4BwHV6NiP0Kb3tDX8Aa+hMWfuNeRw379xTrG369e2IDkx2v5I0a04Brhn3/x1+pm5FfZbistgo/S5VWIOoT09AoDnKWq/MAUXcZknunMUH2ytPI0/DFZWrOOZ6gUqPoWXCXxkvpQi3fCz1Ya89TkI0b6IyVrep+E5HyglKy2J0hWW5aXlPmsVc0QdkRXVUtxhXyHcTwpd8Ju9dEXegc1OiqabAkYEjtYAFUBfEdw2Wf6bzWN+XhKFXpCnMexEejY5sid1h+R72PDGNmuUX2k7um9H4xOx7a0s6A/tGFT4o+DNrNvtNXW17dGm6fg6ziIksax7UmrvHOmRlv9HhBInTD/DyHI6TveZMxrPB+MkWFYHWoP+XUPsxBRE7vE7QJrzun/UFwzHKBbRksfw85uWN8Voq50aCwJve76dkd902qrL0K6bl3efrJY5LCQWuwOe8SExviV0jd7UEf5HXEmVL7EO84Eezrh5oIxmnrTo+j/uJM954ECd342SPFDb7E5oC70zXFMcuglAV6FCRxKt4mqgGkx2vIdAsYwPBEw1e8OwJgCXEb28QDDx0IegpVaatqWcwqh6Rmi18V40iEEpcwDu9Z3nBY94GhCk82XpsiqdFPQWBTibKamlHgPk8Acu7wGwU2KhkmgATMnSS3PEieHYZld0+MAxaJ5fgg86Vlbbp4MjXj1JCd+p1QnO/Tp/TZRJMxT57mPEDmovDWPisfKb3c7+12NILt/YxxvKBJQUpHoZABpsOLI6PCcVjSZ42lb/R52QVgd/LEQVC/rliUu65N2gcZti96HHrg9KhkckBSfOCdZ1m22zFbL9LR9mbYkYO9a35la7fXliSDWP5kakKffm/JWCO7m73C6TBxbmC6G4245X8SF3jnWdOGimkTjAU/w7afKxDTtU0wVt3QzloJLttVod60/NlvWgCNDX3ucRBJrhTF2EaF5tN4M/ELQbKMqLMwCp3UOHIbbHU6t6n2bZZYpy48LWzkizu04gLKM1vongqGvwu9dCtu9TqetI3gcNOkYIlbF2IXeDxlsKX+tqbiD9nTKYEphudMO64KcwlWq34N+HQSquYCLmDimoaD0etvJUGDuIB331RC2Qq4cTiPAlQ9feAv7aaCqf3JkIXGBJJ7jZQDnuxBTzafoxrZiyhTEVJfV52Bmc8vrG+/e5SQaptFxQuQ47Q36kC+5en5Z+YDXkTvEdVOT7LZPEFY+kkdz5TiqJ5cgVHgASi71byJvcuLoq3Ca5XuiwgYnTHrLEzvicbYD44WVZr8Tma6z7MMYzztX98/1XaXPWPb5NRyvVfooDUrrY7P06QBhmXVMQuEiA2tYx/URV+//VJs9ACeF1L2J26iaoyQDz2T5qBj8XN7gu2Lvu1lIJC7yDOZDbN6Eu/lcYS9bNsRsMXXMJhYoLEsNqTnjdvLvTZ2d7KXP9q/OtiBASgX0JiCYq1vSMpfhIktwVkmaRabKLQmI10dOE6kUK8TIJc5S8QznOXmn2BZZQ+cgrxPXhPEikQerMlqy7FVbJTl8b6bFkTMVVxnD2vpQarITDve2sksAPOQwvTK1G8b4JB56puG6gkoj2c5PZITMfTWRYQ4RUAi4+bShwzUIqxBVa3xGM+yWslVIJDr0cVw6SWQo5oGahzHNgEgFdKlsKb3g3KPDweSiTuVE8pLPQj71EC1iwXM4Q7SYcGg0Gg0mV8fdJm4fw3Ww8ibtFnpR3qRJIswfLrxrl8lj7HVOtQ28d49KkSy1zqTdTVtp8KHNqXW/2aba2oQEWsPysTGAIxgfF737qiYCfrddEryTqiZHMbTti0TPEiW913E85UrxAl5nfFQL6CK41B103rHipjjpqTs4x9J7HrEstUYpEZiFBr6gxRpBr6HdMoMk3zWW02jd+FDnB6NRhjc0GJMb3qj+DhHFV0lQvT6CLR5hASLyihSSfvesYeBX9PgIyfSWEYTR4PRlM0M0MQp6tJvjMPKmDjL2JgHj9p4kakF6SYjhQfqoE/eSFUjI2qsjZZUijU84gN8ysInKHPW6e0gyEC8Z3cV3Cg9r83LTA0TgCcY0q/igDHQXQ+T17QqIuYm/ycltmlhMZgFWnqXUj0Mx32MDbW4wn0QSSEDjEpVT1AhFegQJ7y3hX8PFfJfOpEpSXuCW7njDvnChOaeU2omUT0i4Vu6o25rT3StReascJmKq3DaCqHnnhfFL92w2xeXXvjR3XLGserPSkOYcKnVGTpXs9y11Cwf/0a1uNOJw8F7SHNjiae4HS5flQPuKzT7SOLYl9uFWhRuQF38w+5ggODR5yrr4JCcf3zk9hQiLHlggu4mYY81r1qrj/L5f+Owo1D6qcrdvbN7YEqLzRkaH9bHdtd5xp+K+5ZCQPcXto9tnVXjdr96jqXcPEUtKfbRt2H1nR4fHsx5gKnD3QCuHTw4TU8+WMbFXuIW6GzfPLfaOwP3g0hY33wdUdg9dH4tI6nCvtA23rf7BG1hTXTGqF2WxN2dpMHJH3pbR/qoKOTfvOgwCKvBgHv4D5qQ/zHRGMtS5czHgmK2sAkT7j6FfsMaD6tUSPAzc1gUDbXWn7l4+6key10271xCcxD49iCYd6ZpsyXNk4mgUGV26pj7egjzpJsAJ3gbgjmzHNhzXcF1z4rpTnV6678sBNEzjmTPVM6dcIVROmKNS8xRes2IdzgtP4QXdGhN7rZAIsUoBTnPw5/T4hKQ+fS43yPXuGJB/suUG23VHCn+3ePFNozTR4UBUqQ5hwgtHciWricATb+SavUwQ9DNL8NvUaoGCnmpF3iKACv8H</diagram><diagram id="gpu-pool-boot" name="boot &amp; ws">7V1pc6O4Fv01rnZPVbtYjGN/9JaZzEsnee2ke+YTJYNs85ptWOJ4fv3TFRIGJGdrkoa2pzJpEEgocM7VXaSrjj71Hn6PULj5HNjY7WjKOnLsjj7raJpK/icFIVrjUgHcsXD+5YUKK00dG8elG5MgcBMnLBdage9jKymVoSgKtuXbVoErdmNhIRcLpd8cO9mwUl1R9hf+wM56w5804lc8xO9mBfEG2cG2UKTPO8q4A8dP/ejTKAiSF1QoV/YeptiFl85fedaj8/oazF9ThP3kjZ6xNBMnId+FPukeuSn7ROswNUMCgV64g5pzrTNUOqM+OV7CSyPf2CWvHZodIC/s6BNyvMXLOLC+Y7gc4zh2Ap99pGTHP32CH8ifMtkknksKVHKIXGftk2OL/JE4IgWrwE8YRNUhP2ctqPRJ1bfCXtQ9jkjzhaLXvaXqG/sdBx5OIngPrHHdYOjbZeccjNs9mAccsZsCjnVWhhhd1nnLP/RRSV32XWuDRYytsYCJcY+cTxG90dogR/y6UZD6NoY2SKuT7cZJ8CJEFlzdEjlV/vArx3WngRtEtK6O1ZWxIndO4iQKvuPCFUUfLbGRX+Eig3zfCXxyhwiWMQNREoQFSLl4BWCLSRccf31Jz2aqsS+6hdtnwwro9EaCrl/G3JmIub4Mc30OzuaDDqkC5DoaiJz0nvyKUoI3xY4w8sg7j/GnGEfk4BEMqi/G4Ir+9ygGi0DR2Pk58hwXvosX+EGcPawBiOEQYYhRJWJK56gqQaY9iNEExGSoMMlLj3Y9y3W6H8kQRT6gChKpG+7CKPgfUWF6SeBBSx1jwktiK3LCJO4Ys2KVwuE6RZEdk0qeQwY3f02OVihO7onuFJBjMrJF6GfCUX1S9CmNxOXwmbjk9VqAS13ApUfGS45HqNalBUS70searhkHIIeiNflSMdHPlE+fNkFMVKvzT5/CIEoKN6X3jhVEfo9IyC4Kw442hRdHb4YjuPvjCZcvxqVsiJXikuO3BbjsC7g8JxJsfHPRdZ0VJl+FfiN2WJKce7iq/bMDaAV9n2AMRUkaQg/Aahh0RvDETLnXlPwxRIVMHlhPC/ctiCSmdoMyOUH2xZDVnwvZUWsgi5GqCUjA9hpz7ZxIt02wDnzkzvelk7IlUsDF4e+PfXsMvgywRV1ERngLCh+c5C9opWews79ZQ3A8e2APoCc7fgK6R6ESnP5dvLavRs94vYNwioM0snBZSSYsW+OkrAbBa3lL0EXYRYlzX/bitAFCmn6CUAFCmgRC+glCj0JI758gVICQLoFQ/wShRyAUY2siaF8TcKlxnYiaBv8GgZc1UlS51MEntd9/TIt/ucNtpWNjZcjAOMRaH6Gjd7hxvf6wx+2M39JSj9tS9Lipvex2CAA41NtByxVCcxPdI8dFSxeb5GrcLVsBpYdFBNBEk4fq5+PF7deL2fza/P3mzpxe313ddsAeuH8n/T4H88v1+wb79gweuuNOFMIyAZ0ciCV0Gq0Bp+jc0wrghOhUjk4CrRty3mWofQSZVoRRggGYgY+zmguXRrRCaqCSc/K769OiXRbLAosW2+/lQzkSwMrEacsBK3r9dAZYtEVOUgBuz/GdxCEf9F/8mBhF8c63nKCXgdZMUPy9a0JVEMBdx/4IGsMqANxiZG04fA9L5SSNfMC+43nYdkibbjW8SxqgN/i0vxDjU5bI+r6mWGctSf0107rpMbL6x0sPUvI8egyU1tBDdD72GT3CKPDCxMT+BvkWFcOZWL+h5XNWXGKKwaqyiQYmviffznSD9bpQf5FdnMO1S3qp2+v1is0Myj2I0QonO5OAMRG6saDXzukl7sUnP90gBAYg9zQ+1EqA0TMJYLRnfNg52LUFErDSHJI8rq04IIh9Os0GWRYmMKMxxz9ub2/oQ0C8f1uUxP35BiM32fzbKUtpTRG7eE7GFHtXvdNQyCCmpH7iQM/ZYLBFEdik4NSvEeHWEA+skQzh+nCIdevVCG+WGVkFts4l9i8E7HiTJgKuOwYLCJGLdrD1O8Ysa0JQhvgd3bpFqGXbmgxg1kAbalUvxC8nQvvP1SH01iANo6UAkTrdsIPB4JAP1kbxJgcid8jyY+pYzZ2zTzlkS+7YnlaLQ5YpV0WHLPfunByyhxyyU0FoTUEfZD6BHjX8S17ZgqEUpZklJVhIRT8C+ekWp9Lq4742qNmNu1rpWJGBGa8GlrK/cqxu3LJQNPgk3IJQVPMZvKXxlxc2H8oWe8FVNN+w4iyWAEAmdo5FjKKaEYiVpXSczREomzVZhMlr8KhU8Wg0Am+VKRmGbHabNGzQnmmXVijGDbywByECi3xY/JB0P5Avs/U/FE1ty7PNf7IpaRAegGNmX1vJQ++/KU6pL2xaLdjPQSpY2rDwI8K2SSCHkWcuUwLCvcFOOvMFbckYjnbdD5MPWaOaQVpSPjuT9zLUpehXnsI1l6oNVTKHRgneAy4ki/DmyyPa6aeyQjHwAIC8yURnN1exYGDfBtF3HJm5WJ0e9r4e+iHNwTthsQtGgNRGpo3vHfLVswKRPHAM3qwiRVg/9prLCeh1AZ1PmfuVgC4GLMyYGD9Einse8u3ulP17cXVxy0Rz4ng4SOFvHSiKPNDG7XsAauDTycvUEcCBe8JkTZg8k7iSpJjkZW3ApBglcFbkfHE7/nJ7d2N+G3/5TP6ZX40nl/MZMagEBMohnNVjM+KpP580ngUDXiGzKz97UlS6eXvxeX59d3sSxPWBfqj9goLYEEAfgwOCuuhpJdBqb6MU1wqkV3jgq4Zbi3Dz3AG8Pd5QKxwIuOGeABY4ousuwE/FpssgOEYdGIwzaAHOOqVVHMXQ0MqFFACQDaDDo0iBDzpp5gobdcaGyhthASvWcJ3yzlgdmqg4rPqgni3sOIi1JmJ1pIueeylW2zM5xnJCSwDrxc1js0Ze44k6FPFZnqmWWhVoL/dE5UkKGuh4qs5XlXmeVCmIWuR5InbwP6J1nul45j/gMiotWgszN5Kof8UbFMKhtXMdArdIfxprywyYl8u8IPf6X6cJaYZDKGbwMiQAHVrYsqQAHRp9Q6a6NQBYfOTkpjAH0baUvEUEVnsWRlpgm4rA4hbsCVnvgyyZQdt2ZGWOalG/l/qvSwDjLuwTxurE2FAy8bjtGMu80ALGvrFiJU6XuYcaGukecF0r5Vi1aqg1x6rxSpXHqu2z0fLYIoUDLu0eUdiMtocKt2KoMIh72L93osDvGJMP07vZ2Px6sbiYXM7N2fzrxXS++MDmilG3RykWss81UGpkv8hofHs7v7q9uL4yJ+Ppf+ZXs1JbH84vx4s/4KarD/IcBl4KeTOUJdi4MV3+MZmfX3+Zw9/kQSqNbBJmEkQw3/5dvHpSbrTdqzc4ezqQaEjdeu0xebdiIHEV0YlEed6gHp3rFQaOn8Q9WmKusY8jlNBVHRnkyMFXuPT7/kqOV6FBD3JBxj3bIQ26yYNWbGU2Px/fXd6al7d/aeb4bnZxbf4mp8Fvv8HtebKtDvXrWBucTXmi7qBi7iMHzGIfuTH9WBHcDy3USo9Dk0yOgx6S6fByerRoaBDDj9lKolLUURLhqfDBCfxM06msqto/vltiFrmfJV6qZrKpdttDuyU2YWqyGyDbpNTq0t+mYz9etUzYHtCUqFrkSyCiatvdEBKZUpXr/KY/DfyVs85Ob5wQgzaelT3+DOgUfOrUdoCEX8fzfIZA4N5npF05dJljGsbIC92SjSE2mEVoe2GadC/Iqxxb37txalH1UJ9B+OHje0WyjoLUZ4NnknrUojFPjN+WSJ0FRj/SPxoooHxDkZeGN2hH0fwStm9pTUIlWrWXRXWfw3tDHT5OrGQLfIp3frLBxNqAY7z2CG5oJCVyPKCUxcfDbCUO1Qh7FumD42JZ6wVyZX8zeSWEkQnuJo5HMAzfls3nOVGsLorJrH0pxdqz2sbaitFinl7No1l5Fe4PICMEsXe10govmJwDqM2WDf95fXFl3i3mX+jzzslvODEXt/ObvORyPv46rxWUv3bguYpBWTRPisEWmfVYkqyXiXgBKXUu0MkdjdI1OlVJVucynTyJ0o8s08mn3BXX6RSCWqelOofgpv1EWL0GRs+DQwO//I8/udqLG/BudA5rwVVHe8YNVusHoHi4TzSsUnhi5n+p/69/6k30hxUjX3mTP/iF3eAf5L27cQAX9X9+0maGgDpYRsVdTrRmCU3RwZKZTjQhRgPHaeVV43RvNBiVx+qz0fCJ0Zqe3WAw5eiEnsdldmm0LkwVOCKhPeyX7SrN6FUspmxge0uxLfZKG5V7pepvP5Q0m/Jvm6b0eXrSa2k8Kicq7Q2HZ29H47BpGW/fnC3CTAil/9MZ3Nerg/7oqBkcY2smDNozSErwDS8Xwu5apYwEaJU5gfIM/DXO3MDDlYGlHhsNn9n6PjB3yjJAn6XIJt9K0wzk61mab4bblrnByBbwOYEdALPl19RhCEmpavQW2oigTzrtbGAN8XL1DG/hzwdIZXK2qsryUPDgbTudgnYshwfPY83hsTiwN+APYcTAQ1ueL1Jb6jSrT/MxolUGaDlIpDPC2gOSUA4SlsH3BAohq5d2DJLDPYiKLDtzjajAg5Umn4TaIlSccRD8yqjYykEhndZcZ8j8ZbOUmwmQUVUjlQFEk+4Y2h6AWCbsnSF8/IJzwocs70/k/ONKZCnD38EveDz+ArUyP11Vz36+y0/o1HBY6dSReQyI1l0DCfKh70SCMt50wXBrAAmETh09CcITCd4Qb/lu9k0igdCpoycBsaJOLHg7wA2FPZcawAKhU0fPgm0dJMgtvRMJKvq3IhiWDWCB2Kujp4F3YG8/CM4oVuD72Epo9fPtfhED302H7jF1rz1Gon3Y/1nG9bMTAxwRlaqmbO6OaZJpIXTq6IikCUSi+xDSPAnI+id1Ity1XAf2n+L5e7exuIrnxdyRuqNP3DkA06rbsgkWidCpo+OOjgTy6AjYQ5O6sryBqwhjuvJPKSXBhqsnGr0zYquT8RsxBL3HCoFm02gp0mgJNMJuzKiDfZakDKiDPZqBm5aYcYKSFHQ8vq0b4WSRemL07MUck26odTKfnkE4Xfv5hBMUUaFTR0e4A3uVMl7BKAW0WPs8s9vJRnobaApjgV5NcNIIvlQ7dXR8EZek0w16aRbzUkJ+M0HrNd7n5afLzWEVuiTvwUnVe0sm9RvgbRC810Knjo5JYkp3ukd1KccxTSKSp26ogTgvz995PMQRMdoAV4Po8BZ6dWzMieNkv/VAcR/iuQbrY4bK/gDIwvc/IMYRs6CyjTvZ5jCaIqtX4RlsdVdmlpAoXjZTUNhwtnkbEuhVO6UvSaQzkE0f1FqTSMf2zgSwnIGsRWEIGsseCXTZQuxAuiaeTOrRAONJ739xL55SsfPNrZvkmBI6dWwy1xsJDBq+QO/P0kzRLbl4/rfZXYyjRYJ5RrguZyH1CGdZ2EzHfsgKHA+tsblPphjhONuE1HYg3Zvjrz+e7Ir3JkUDLHRBZxM6dXRM5W+kQNWR3LDIqXkyLN4XpIMGjHGiYSH06vioo4pRTFWhYcw802KU+nE5e6k8aSlXH01igYTdQ2lIDyQuJbJWUqOQ3DHPHb5/Dm1J9qBCtTS0ya0QIpoSmjp+Sp+5oKbRvl6ywZBygG1ZsvLCPs2y3EX3bBsTMtyyjSM09fEHrkhtDHn0uxHasiTiky26r95GnlF3OtSjSiM+MowKmTm7n1pRzM29NphxqmSqm0LDo4Wcu5+x7SDIak1bAoOOlkw3qf+9UsTy8ualoI7ywrccE120xO4k35dHlsX3pw2bDFBKT8lTtjRnJFUVEBdFnJ/plUxQP0MJHTzVqbfPdLjvFH9msFrRfU2OYuDWRMFAJ8Ga+c5usF0pHcCd2PHjBPkW/mQ7RIInNLs3dQwWBcU5TiAfeHGU4pt4UQ067hiTlJiwMIvJmGWDX1F+VOuCp4kMJ+YqTdKojqkY9mA5MBplq+5Fh6L2myY6BCX8rAHmq2BTC506SY5yK7VLDsnERVWjKv/S8RHFNZ1Jv4oQ3Q2g68HO9cpyR/ToGnSEg77hFugInwjTR8qoaUwXSVXdVqMJU0mETp2YXm6lfqaLcytVjVoPfy6urzqM5zhLmUt4DsoAXSCTuZyzc+6ZtvJhnnq1qe/aZFuiQwnL1zcenGTEXkbw/Q8bLzGG1eBmEySG0KmTxCi3UrvEECeHqjrIC5qRM1tf5wZ8XjZdJkQwjVFcWCZEtykq2AGlsBjdcKV6xyVG93hsff9xufG8ydpNFiLNEREiG6v7UzbChy/06lfz4dNaURAkT9ThfYxQuPkMG/mR+/8P</diagram></mxfile>"><defs/><g><g><rect x="311" y="50" width="600" height="30" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 57px; margin-left: 611px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 18px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: nowrap; ">gpu_pool.py — runtime architecture</div></div></div></foreignObject><text x="611" y="75" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="18px" text-anchor="middle" font-weight="bold">gpu_pool.py — runtime architecture</text></switch></g></g><g><rect x="521" y="140" width="180" height="60" rx="9" ry="9" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all" style="fill: light-dark(rgb(218, 232, 252), rgb(29, 41, 59)); stroke: light-dark(rgb(108, 142, 191), rgb(92, 121, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 178px; height: 1px; padding-top: 170px; margin-left: 522px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 13px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">Client (WebSocket)</div></div></div></foreignObject><text x="611" y="174" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="13px" text-anchor="middle" font-weight="bold">Client (WebSocket)</text></switch></g></g><g><rect x="521" y="260" width="180" height="60" rx="9" ry="9" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 178px; height: 1px; padding-top: 290px; margin-left: 522px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 13px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">GPUPool</div></div></div></foreignObject><text x="611" y="294" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="13px" text-anchor="middle" font-weight="bold">GPUPool</text></switch></g></g><g><rect x="1" y="416" width="1220" height="455" fill="#e6f2e0" stroke="#82b366" stroke-width="2" pointer-events="all" style="fill: light-dark(rgb(230, 242, 224), rgb(26, 36, 20)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 1203px; height: 1px; padding-top: 431px; margin-left: 18px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 13px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">GPUSlot (one per GPU)</div></div></div></foreignObject><text x="18" y="444" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="13px" font-weight="bold">GPUSlot (one per GPU)</text></switch></g></g><g><rect x="71" y="470" width="60" height="30" rx="4.5" ry="4.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 50px; height: 1px; padding-top: 487px; margin-left: 81px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">start()</div></div></div></foreignObject><text x="81" y="490" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">start()</text></switch></g></g><g><rect x="141" y="470" width="70" height="30" rx="4.5" ry="4.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 60px; height: 1px; padding-top: 487px; margin-left: 151px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">shutdown()</div></div></div></foreignObject><text x="151" y="490" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">shutdown()</text></switch></g></g><g><rect x="460" y="460" width="70" height="40" rx="6" ry="6" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 60px; height: 1px; padding-top: 482px; margin-left: 470px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">join_user()</div></div></div></foreignObject><text x="470" y="485" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">join_user()</text></switch></g></g><g><rect x="541" y="460" width="80" height="40" rx="6" ry="6" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 70px; height: 1px; padding-top: 482px; margin-left: 551px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">user_step()</div></div></div></foreignObject><text x="551" y="485" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">user_step()</text></switch></g></g><g><rect x="631" y="455" width="80" height="45" rx="6.75" ry="6.75" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 70px; height: 1px; padding-top: 480px; margin-left: 641px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">leave_user()</div></div></div></foreignObject><text x="641" y="483" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">leave_user()</text></switch></g></g><g><rect x="40" y="570" width="230" height="80" rx="12" ry="12" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 220px; height: 1px; padding-top: 612px; margin-left: 50px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-style: italic; white-space: normal; word-wrap: normal; ">_send_command()<br /><br />puts on command_queue,<br />blocks on response_queue.get<div><br /></div><div><b>**for user agonistic commands</b></div></div></div></div></foreignObject><text x="50" y="615" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" font-style="italic">_send_command()...</text></switch></g></g><g><rect x="311" y="574" width="300" height="90" rx="13.5" ry="13.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 290px; height: 1px; padding-top: 621px; margin-left: 321px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-style: italic; white-space: normal; word-wrap: normal; ">_send_command_tagged()<br /><br />creates Future; registers in<br />_pending_futures[user_id];<br />ensures response reader; <b>awaits future</b><div><br /></div><div><b>**for user specific commands</b></div></div></div></div></foreignObject><text x="321" y="624" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" font-style="italic">_send_command_tagged()...</text></switch></g></g><g><rect x="761" y="560" width="340" height="110" rx="16.5" ry="16.5" fill="#d5e8d4" stroke="#82b366" pointer-events="all" style="fill: light-dark(rgb(213, 232, 212), rgb(31, 47, 30)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 330px; height: 1px; padding-top: 617px; margin-left: 771px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-style: italic; white-space: normal; word-wrap: normal; ">_response_reader()<br /><br />a while loop that reads response_queue;<br />dispatch WorkerEvent:<br />• media → _stream_queues<br />• acks/errors → _pending_futures</div></div></div></foreignObject><text x="771" y="620" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" font-style="italic">_response_reader()...</text></switch></g></g><g><rect x="261" y="724" width="340" height="110" rx="16.5" ry="16.5" fill="#fff2cc" stroke="#d6b656" pointer-events="all" style="fill: light-dark(rgb(255, 242, 204), rgb(40, 29, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 330px; height: 1px; padding-top: 781px; margin-left: 271px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">«dict» _pending_futures<br /><br />{user_id → asyncio.Future}<br /><br />created by _send_command_tagged;<br />resolved + popped by _response_reader;<br />cleared on model reload or leave</div></div></div></foreignObject><text x="271" y="784" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">«dict» _pending_futures...</text></switch></g></g><g><rect x="681" y="724" width="460" height="126" rx="18.9" ry="18.9" fill="#fff2cc" stroke="#d6b656" pointer-events="all" style="fill: light-dark(rgb(255, 242, 204), rgb(40, 29, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 450px; height: 1px; padding-top: 789px; margin-left: 691px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">«dict» _stream_queues: <div><span style="color: light-dark(rgb(63, 63, 63), rgb(183, 183, 183)); background-color: transparent;">each is a queue of metadata that tell the consumer which part of the shared </span></div><div><div><font color="#000000" style="color: light-dark(rgb(0, 0, 0), rgb(237, 237, 237));">stream buffer to read next.<br /></font><br />{user_id → asyncio.Queue}<br /><br />created by register_stream_queue()<br />written by _response_reader (MediaInit / Chunk / Complete);<br />consumed by  AV loop → ws.send_bytes / send_json;<br />popped by leave_user(); cleared on reload</div></div></div></div></div></foreignObject><text x="691" y="792" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px">«dict» _stream_queues:...</text></switch></g></g><g><path d="M 1 895 L 1221 895" fill="none" stroke="#888888" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="all" style="stroke: light-dark(rgb(136, 136, 136), rgb(120, 120, 120));"/></g><g><rect x="411" y="888" width="400" height="20" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 895px; margin-left: 611px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #666666; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#666666, #959595); line-height: 1.2; pointer-events: all; font-style: italic; white-space: nowrap; ">── process boundary (mp.Queue + mp.RawArray) ──</div></div></div></foreignObject><text x="611" y="906" fill="#666666" font-family=""Helvetica"" font-size="11px" text-anchor="middle" font-style="italic">── process boundary (mp.Queue + mp.RawArray) ──</text></switch></g></g><g><path d="M 40 935 C 40 926.72 84.77 920 140 920 C 166.52 920 191.96 921.58 210.71 924.39 C 229.46 927.21 240 931.02 240 935 L 240 1005 C 240 1013.28 195.23 1020 140 1020 C 84.77 1020 40 1013.28 40 1005 Z" fill="#f8cecc" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(248, 206, 204), rgb(81, 45, 43)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 240 935 C 240 943.28 195.23 950 140 950 C 84.77 950 40 943.28 40 935" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 198px; height: 1px; padding-top: 984px; margin-left: 41px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">command_queue</div></div></div></foreignObject><text x="140" y="987" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle" font-weight="bold">command_queue</text></switch></g></g><g><path d="M 481 935 C 481 926.72 525.77 920 581 920 C 607.52 920 632.96 921.58 651.71 924.39 C 670.46 927.21 681 931.02 681 935 L 681 1005 C 681 1013.28 636.23 1020 581 1020 C 525.77 1020 481 1013.28 481 1005 Z" fill="#f8cecc" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(248, 206, 204), rgb(81, 45, 43)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 681 935 C 681 943.28 636.23 950 581 950 C 525.77 950 481 943.28 481 935" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 198px; height: 1px; padding-top: 984px; margin-left: 482px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">response_queue</div></div></div></foreignObject><text x="581" y="987" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle" font-weight="bold">response_queue</text></switch></g></g><g><path d="M 861 935 C 861 926.72 923.68 920 1001 920 C 1038.13 920 1073.74 921.58 1099.99 924.39 C 1126.25 927.21 1141 931.02 1141 935 L 1141 1005 C 1141 1013.28 1078.32 1020 1001 1020 C 923.68 1020 861 1013.28 861 1005 Z" fill="#f8cecc" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(248, 206, 204), rgb(81, 45, 43)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 1141 935 C 1141 943.28 1078.32 950 1001 950 C 923.68 950 861 943.28 861 935" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 278px; height: 1px; padding-top: 984px; margin-left: 862px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">shared_stream_buffer<br />(mp.RawArray, 256 MiB)<br /><br />zero-copy mp4 bytes</div></div></div></foreignObject><text x="1001" y="987" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">shared_stream_buffer...</text></switch></g></g><g><rect x="41" y="1060" width="1140" height="80" rx="12" ry="12" fill="#e0e0e0" stroke="#666666" pointer-events="all" style="fill: light-dark(rgb(224, 224, 224), rgb(45, 45, 45)); stroke: light-dark(rgb(102, 102, 102), rgb(149, 149, 149));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1128px; height: 1px; padding-top: 1103px; margin-left: 53px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 10px; font-family: "monospace"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">worker_ipc.py — shared types (imported on both sides of the process boundary)<br /><br />Command(type: CommandType, payload: CommandPayload | None, user_id: str | None)<br />CommandPayload = UserStepPayload | WarmupPayload | ReloadModelPayload<br />WorkerEvent = StepComplete | WorkerError | JoinAck | LeaveAck | ReloadAck | WarmupComplete | MediaInit | MediaChunk | MediaComplete | InitAck | ShutdownAck</div></div></div></foreignObject><text x="53" y="1106" fill="light-dark(#000000, #ffffff)" font-family=""monospace"" font-size="10px" font-weight="bold">worker_ipc.py — shared types (imported on both sides of the process boundary)...</text></switch></g></g><g><rect x="34" y="1271" width="1140" height="360" fill="#fef1e0" stroke="#d79b00" stroke-width="2" pointer-events="all" style="fill: light-dark(rgb(254, 241, 224), rgb(38, 27, 12)); stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 1123px; height: 1px; padding-top: 1286px; margin-left: 51px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 13px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">Worker Subprocess - in each GPUSlot</div></div></div></foreignObject><text x="51" y="1299" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="13px" font-weight="bold">Worker Subprocess - in each GPUSlot</text></switch></g></g><g><rect x="81" y="1320" width="240" height="120" rx="18" ry="18" fill="#ffe6cc" stroke="#d79b00" pointer-events="all" style="fill: light-dark(rgb(255, 230, 204), rgb(54, 33, 10)); stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 228px; height: 1px; padding-top: 1384px; margin-left: 93px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">command dispatcher<br /><br />gpu_worker_process() branches on<br />CommandType; asserts payload type<br /><br />INIT / WARMUP / RELOAD_MODEL<br />USER_JOIN / USER_STEP / USER_LEAVE<br />SHUTDOWN</div></div></div></foreignObject><text x="93" y="1387" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" font-weight="bold">command dispatcher...</text></switch></g></g><g><rect x="421" y="1320" width="240" height="120" rx="18" ry="18" fill="#e1d5e7" stroke="#9673a6" pointer-events="all" style="fill: light-dark(rgb(225, 213, 231), rgb(57, 47, 63)); stroke: light-dark(rgb(150, 115, 166), rgb(149, 119, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 228px; height: 1px; padding-top: 1384px; margin-left: 433px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">VideoGenerationWorker.generate_step()<br />video_generation.py:380<br /><br />reads + updates ContinuationState,<br />calls generator</div></div></div></foreignObject><text x="433" y="1387" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" font-weight="bold">VideoGenerationWorker.generate_step()...</text></switch></g></g><g><rect x="761" y="1320" width="260" height="120" rx="18" ry="18" fill="#b1d8d7" stroke="#23445d" pointer-events="all" style="fill: light-dark(rgb(177, 216, 215), rgb(32, 66, 65)); stroke: light-dark(rgb(35, 68, 93), rgb(160, 188, 210));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 248px; height: 1px; padding-top: 1384px; margin-left: 773px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">stream_fmp4()<br />av_streaming.py:121<br /><br />trims overlap, pipes to ffmpeg,<br />publishes StreamInit / StreamChunk /<br />StreamComplete via callback</div></div></div></foreignObject><text x="773" y="1387" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" font-weight="bold">stream_fmp4()...</text></switch></g></g><g><path d="M 541 1600 L 541 1663.63" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/><path d="M 541 1668.88 L 537.5 1661.88 L 541 1663.63 L 544.5 1661.88 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); stroke: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));"/></g><g><rect x="421" y="1500" width="240" height="100" rx="15" ry="15" fill="#e1d5e7" stroke="#9673a6" pointer-events="all" style="fill: light-dark(rgb(225, 213, 231), rgb(57, 47, 63)); stroke: light-dark(rgb(150, 115, 166), rgb(149, 119, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 238px; height: 1px; padding-top: 1550px; margin-left: 422px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">VideoGenerator (fastvideo)<br /><br />LTX2 DiT + refine upsampler<br />FP4 quant, torch.compile<br /><br />owned by VideoGenerationWorker<br />video_generation.py:211</div></div></div></foreignObject><text x="541" y="1553" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">VideoGenerator (fastvideo)...</text></switch></g></g><g><rect x="761" y="1500" width="260" height="100" rx="15" ry="15" fill="#ffffff" stroke="#d79b00" pointer-events="all" style="fill: light-dark(rgb(255, 255, 255), rgb(18, 18, 18)); stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 258px; height: 1px; padding-top: 1550px; margin-left: 762px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">ffmpeg subprocess<br /><br />libx264 / *_nvenc<br />fragmented mp4</div></div></div></foreignObject><text x="891" y="1553" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">ffmpeg subprocess...</text></switch></g></g><g><rect x="81" y="1500" width="240" height="100" rx="15" ry="15" fill="#e1d5e7" stroke="#9673a6" pointer-events="all" style="fill: light-dark(rgb(225, 213, 231), rgb(57, 47, 63)); stroke: light-dark(rgb(150, 115, 166), rgb(149, 119, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 228px; height: 1px; padding-top: 1554px; margin-left: 93px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">ContinuationState<br />video_generation.py:89<br /><br />• video_images: list[PIL.Image]<br />• audio_latents: torch.Tensor (CPU)<br /><br />carried across segments</div></div></div></foreignObject><text x="93" y="1557" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px">ContinuationState...</text></switch></g></g><g><path d="M 611 200 L 611 253.63" fill="none" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(108, 142, 191), rgb(92, 121, 163));"/><path d="M 611 258.88 L 607.5 251.88 L 611 253.63 L 614.5 251.88 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(108, 142, 191), rgb(92, 121, 163)); stroke: light-dark(rgb(108, 142, 191), rgb(92, 121, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 231px; margin-left: 611px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">acquire</div></div></div></foreignObject><text x="611" y="234" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">acquire</text></switch></g></g><g><path d="M 611 320 L 611 409.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/><path d="M 611 414.88 L 607.5 407.88 L 611 409.63 L 614.5 407.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(130, 179, 102), rgb(68, 110, 44)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 367px; margin-left: 611px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">assigns slot</div></div></div></foreignObject><text x="611" y="371" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">assigns slot</text></switch></g></g><g><path d="M 99 500 L 99 563.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/><path d="M 99 568.88 L 95.5 561.88 L 99 563.63 L 102.5 561.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(130, 179, 102), rgb(68, 110, 44)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><path d="M 495 500 L 493.09 500 L 493.09 567.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/><path d="M 493.09 572.88 L 489.59 565.88 L 493.09 567.63 L 496.59 565.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(130, 179, 102), rgb(68, 110, 44)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><path d="M 549 500 L 549.09 537.09 L 551.09 537.09 L 551.09 567.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/><path d="M 551.09 572.88 L 547.59 565.88 L 551.09 567.63 L 554.59 565.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(130, 179, 102), rgb(68, 110, 44)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><path d="M 639 500 L 641.09 500 L 641.09 530 L 573.27 530 L 573.21 567.45" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/><path d="M 573.2 572.7 L 569.71 565.7 L 573.21 567.45 L 576.71 565.71 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(130, 179, 102), rgb(68, 110, 44)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><path d="M 430.1 663.01 L 430.18 694 L 430.83 717.63" fill="none" stroke="#d6b656" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/><path d="M 430.97 722.88 L 427.28 715.98 L 430.83 717.63 L 434.28 715.79 Z" fill="#d6b656" stroke="#d6b656" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(214, 182, 86), rgb(109, 81, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 694px; margin-left: 430px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">register future</div></div></div></foreignObject><text x="430" y="697" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">register future</text></switch></g></g><g><path d="M 763.72 659.88 L 763.64 670 L 631.09 670 L 631.09 757.09 L 607.37 757.02" fill="none" stroke="#d6b656" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/><path d="M 602.12 757 L 609.13 753.52 L 607.37 757.02 L 609.11 760.52 Z" fill="#d6b656" stroke="#d6b656" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(214, 182, 86), rgb(109, 81, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 670px; margin-left: 686px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; "><b>set future result</b></div></div></div></foreignObject><text x="686" y="673" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">set future result</text></switch></g></g><g><path d="M 810.64 670.88 L 810.55 697.09 L 810.68 720.28" fill="none" stroke="#d6b656" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/><path d="M 810.71 725.53 L 807.17 718.55 L 810.68 720.28 L 814.17 718.51 Z" fill="#d6b656" stroke="#d6b656" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(214, 182, 86), rgb(109, 81, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 698px; margin-left: 811px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">put(MediaEvent)</div></div></div></foreignObject><text x="811" y="701" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">put(MediaEvent)</text></switch></g></g><g><path d="M 111.09 650 L 111.09 785.09 L 110.05 913.63" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 110.01 918.88 L 106.57 911.85 L 110.05 913.63 L 113.57 911.91 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 785px; margin-left: 111px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">put Command</div></div></div></foreignObject><text x="111" y="788" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">put Command</text></switch></g></g><g><path d="M 317.6 661.3 L 317.64 690 L 211.09 690 L 211.09 792 L 210.05 913.63" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 210.01 918.88 L 206.57 911.85 L 210.05 913.63 L 213.57 911.91 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 769px; margin-left: 211px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">put Command</div></div></div></foreignObject><text x="211" y="773" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">put Command</text></switch></g></g><g><path d="M 631.6 925.6 L 631.64 920 L 631.09 615.09 L 754.63 615" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 759.88 615 L 752.88 618.51 L 754.63 615 L 752.88 611.51 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 615px; margin-left: 697px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">WorkerEvent</div></div></div></foreignObject><text x="697" y="618" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">WorkerEvent</text></switch></g></g><g><path d="M 481 970 L 481.09 880 L 161.09 880 L 161.09 656.37" fill="none" stroke="#b85450" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 161.09 651.12 L 164.59 658.12 L 161.09 656.37 L 157.59 658.12 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 880px; margin-left: 320px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">blocking .get</div></div></div></foreignObject><text x="320" y="883" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">blocking .get</text></switch></g></g><g><path d="M 140 1020 L 140 1170 L 140.96 1313.63" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 140.99 1318.88 L 137.45 1311.91 L 140.96 1313.63 L 144.45 1311.86 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><path d="M 321 1380 L 414.63 1380" fill="none" stroke="#d79b00" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/><path d="M 419.88 1380 L 412.88 1383.5 L 414.63 1380 L 412.88 1376.5 Z" fill="#d79b00" stroke="#d79b00" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(215, 155, 0), rgb(153, 101, 0)); stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1380px; margin-left: 372px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">USER_STEP</div></div></div></foreignObject><text x="372" y="1383" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">USER_STEP</text></switch></g></g><g><path d="M 201 1320 L 201.09 1200 L 511.09 1200 L 511 1026.37" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 511 1021.12 L 514.5 1028.12 L 511 1026.37 L 507.5 1028.12 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1200px; margin-left: 387px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">StepComplete / ack types</div></div></div></foreignObject><text x="387" y="1203" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">StepComplete / ack types</text></switch></g></g><g><path d="M 541 1440 L 541 1493.63" fill="none" stroke="#9673a6" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(150, 115, 166), rgb(149, 119, 163));"/><path d="M 541 1498.88 L 537.5 1491.88 L 541 1493.63 L 544.5 1491.88 Z" fill="#9673a6" stroke="#9673a6" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(150, 115, 166), rgb(149, 119, 163)); stroke: light-dark(rgb(150, 115, 166), rgb(149, 119, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1471px; margin-left: 541px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">generator.generate_video()</div></div></div></foreignObject><text x="541" y="1474" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">generator.generate_video()</text></switch></g></g><g><path d="M 414.63 1416 L 371.09 1416 L 371.09 1520 L 327.37 1520" fill="none" stroke="#d6b656" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/><path d="M 419.88 1416 L 412.88 1419.5 L 414.63 1416 L 412.88 1412.5 Z" fill="#d6b656" stroke="#d6b656" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(214, 182, 86), rgb(109, 81, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/><path d="M 322.12 1520 L 329.12 1516.5 L 327.37 1520 L 329.12 1523.5 Z" fill="#d6b656" stroke="#d6b656" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(214, 182, 86), rgb(109, 81, 0)); stroke: light-dark(rgb(214, 182, 86), rgb(109, 81, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1468px; margin-left: 371px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">read / write</div></div></div></foreignObject><text x="371" y="1471" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">read / write</text></switch></g></g><g><path d="M 661 1380 L 754.63 1380" fill="none" stroke="#d79b00" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/><path d="M 759.88 1380 L 752.88 1383.5 L 754.63 1380 L 752.88 1376.5 Z" fill="#d79b00" stroke="#d79b00" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(215, 155, 0), rgb(153, 101, 0)); stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1380px; margin-left: 712px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">frames + audio</div></div></div></foreignObject><text x="712" y="1383" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">frames + audio</text></switch></g></g><g><path d="M 891 1440 L 891 1493.63" fill="none" stroke="#d79b00" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/><path d="M 891 1498.88 L 887.5 1491.88 L 891 1493.63 L 894.5 1491.88 Z" fill="#d79b00" stroke="#d79b00" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(215, 155, 0), rgb(153, 101, 0)); stroke: light-dark(rgb(215, 155, 0), rgb(153, 101, 0));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1471px; margin-left: 891px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">rawvideo + wav</div></div></div></foreignObject><text x="891" y="1474" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">rawvideo + wav</text></switch></g></g><g><path d="M 956 1320 L 956 1170 L 1001.09 1170 L 1001 1026.37" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 1001 1021.12 L 1004.5 1028.12 L 1001 1026.37 L 997.5 1028.12 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1170px; margin-left: 979px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">write mp4 bytes<br />at shared_write_offset</div></div></div></foreignObject><text x="979" y="1173" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">write mp4 bytes...</text></switch></g></g><g><path d="M 1026.48 922.7 L 1026.55 920 L 1026.05 856.37" fill="none" stroke="#b85450" stroke-miterlimit="10" stroke-dasharray="3 3" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 1026.01 851.12 L 1029.56 858.09 L 1026.05 856.37 L 1022.56 858.15 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 886px; margin-left: 1026px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">bytes from the buffer are sent through websocket <div>based on info from the stream queue</div></div></div></div></foreignObject><text x="1026" y="889" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">bytes from the buffer are sent through websocket...</text></switch></g></g><g><path d="M 1141 787 L 1141.09 777.09 L 1271.09 777.09 L 1271.09 170 L 707.37 170" fill="none" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(108, 142, 191), rgb(92, 121, 163));"/><path d="M 702.12 170 L 709.12 166.5 L 707.37 170 L 709.12 173.5 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(108, 142, 191), rgb(92, 121, 163)); stroke: light-dark(rgb(108, 142, 191), rgb(92, 121, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 259px; margin-left: 1271px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">session AV loop (main.py:2124):<br />ws.send_bytes(chunk) — binary mp4 frames<br />ws.send_json({type: media_init | media_segment_complete | ...})</div></div></div></foreignObject><text x="1271" y="262" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">session AV loop (main.py:2124):...</text></switch></g></g><g><path d="M 787 1320 L 787.09 1170 L 651.09 1170 L 651 1026.37" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 651 1021.12 L 654.5 1028.12 L 651 1026.37 L 647.5 1028.12 Z" fill="#b85450" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(184, 84, 80), rgb(215, 129, 126)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 1170px; margin-left: 719px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 10px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">MediaChunk (offset, len)</div></div></div></foreignObject><text x="719" y="1173" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="10px" text-anchor="middle">MediaChunk (offset, len)</text></switch></g></g><g><rect x="0" y="0" width="270" height="380" rx="40.5" ry="40.5" fill="#f5f5f5" stroke="#999999" pointer-events="all" style="fill: light-dark(rgb(245, 245, 245), rgb(26, 26, 26)); stroke: light-dark(rgb(153, 153, 153), rgb(106, 106, 106));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 258px; height: 1px; padding-top: 194px; margin-left: 12px;"><div style="box-sizing: border-box; font-size: 0; text-align: left; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Legend<br /><br />■ blue client / external<br />■ green main-process pool/slot<br /> (methods — italic label)<br />■ yellow containers (routing state)<br />■ red IPC primitives (mp.Queue, mp.RawArray)<br /><br />Worker subprocess modules:<br />■ orange gpu_pool.py (dispatcher)<br />■ lavender video_generation.py<br />■ teal av_streaming.py<br />■ gray worker_ipc.py (shared types)<br /><br />Flow:<br /> client → pool → slot<br /> → _send_command(_tagged) → command_queue<br /> → dispatcher → generate_step()<br /> → stream_fmp4() → ffmpeg<br /> → shared_buf + response_queue<br /> → _response_reader → futures / stream_queues<br /> → client awaits (via main.py AV loop)</div></div></div></foreignObject><text x="12" y="197" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px">Legend...</text></switch></g></g><g><rect x="481" y="1670" width="120" height="60" rx="9" ry="9" fill="#e1d5e7" stroke="#9673a6" pointer-events="all" style="fill: light-dark(rgb(225, 213, 231), rgb(57, 47, 63)); stroke: light-dark(rgb(150, 115, 166), rgb(149, 119, 163));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 1700px; margin-left: 482px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">FastVideo video_generator</div></div></div></foreignObject><text x="541" y="1703" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">FastVideo video_genera...</text></switch></g></g><g><path d="M 170.47 500.3 L 170.55 535.09 L 170.81 565.23" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/><path d="M 170.86 570.48 L 167.3 563.51 L 170.81 565.23 L 174.3 563.45 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(130, 179, 102), rgb(68, 110, 44)); stroke: light-dark(rgb(130, 179, 102), rgb(68, 110, 44));"/></g><g><path d="M 495.09 500 L 495.09 510 Q 495.09 520 485.09 520 L 276.18 520 Q 266.18 520 266.16 530 L 266.1 565.31" fill="none" stroke="#61a25d" stroke-miterlimit="10" pointer-events="stroke" style="stroke: light-dark(rgb(97, 162, 93), rgb(74, 130, 71));"/><path d="M 266.09 570.56 L 262.6 563.56 L 266.1 565.31 L 269.6 563.57 Z" fill="#61a25d" stroke="#61a25d" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(97, 162, 93), rgb(74, 130, 71)); stroke: light-dark(rgb(97, 162, 93), rgb(74, 130, 71));"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 521px; margin-left: 336px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; background-color: #ffffff; "><div style="display: inline-block; font-size: 11px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; background-color: light-dark(#ffffff, var(--ge-dark-color, #121212)); white-space: nowrap; ">reload model</div></div></div></foreignObject><text x="336" y="524" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="11px" text-anchor="middle">reload model</text></switch></g></g><g/><g><path d="M 1301 960.35 C 1301 956.84 1305.79 954 1311.69 954 C 1314.53 954 1317.25 954.67 1319.26 955.86 C 1321.26 957.05 1322.39 958.67 1322.39 960.35 L 1322.39 969.65 C 1322.39 973.16 1317.6 976 1311.69 976 C 1305.79 976 1301 973.16 1301 969.65 Z" fill="#f8cecc" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="fill: light-dark(rgb(248, 206, 204), rgb(81, 45, 43)); stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/><path d="M 1322.39 960.35 C 1322.39 963.86 1317.6 966.71 1311.69 966.71 C 1305.79 966.71 1301 963.86 1301 960.35" fill="none" stroke="#b85450" stroke-miterlimit="10" pointer-events="all" style="stroke: light-dark(rgb(184, 84, 80), rgb(215, 129, 126));"/></g><g><rect x="1322.39" y="950" width="218.61" height="40" fill="none" stroke="none" pointer-events="all"/></g><g><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 217px; height: 1px; padding-top: 970px; margin-left: 1323px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; color: #000000; "><div style="display: inline-block; font-size: 12px; font-family: "Helvetica"; color: light-dark(#000000, #ffffff); line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">Queue: owned by GPUSlot, passed to worker process as argument.</div></div></div></foreignObject><text x="1432" y="974" fill="light-dark(#000000, #ffffff)" font-family=""Helvetica"" font-size="12px" text-anchor="middle">Queue: owned by GPUSlot, passed to w...</text></switch></g></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file diff --git a/apps/dreamverse/pyproject.toml b/apps/dreamverse/pyproject.toml new file mode 100644 index 0000000000..cc44a3bcf2 --- /dev/null +++ b/apps/dreamverse/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "dreamverse" +version = "0.1.0" +description = "Dreamverse application with FastAPI backend and Next.js frontend." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.115,<1", + "fastvideo>=0.1.7", + "pillow>=10.0", + "uvicorn[standard]>=0.30,<1", +] + +[project.optional-dependencies] +server = [ + "cerebras-cloud-sdk", + "flash-attn-cute @ git+https://github.com/XOR-op/flash-attention.git@fa4-compile#subdirectory=flash_attn/cute", + "flashinfer-python", + "openai>=1.40", +] +safety = [ + "fasttext>=0.9.2", +] +test = [ + "pytest>=8.0", +] + +[project.scripts] +dreamverse-server = "dreamverse.server_entry:cli" +dreamverse-mock-server = "dreamverse.mock_server:cli" + +[tool.uv] +package = false + +[tool.uv.sources] +fastvideo = { workspace = true } + +[tool.pytest.ini_options] +markers = [ + "gpu: requires real GPU + model weights; skip in CI", +] diff --git a/apps/dreamverse/scripts/install_native_ffmpeg.sh b/apps/dreamverse/scripts/install_native_ffmpeg.sh new file mode 100755 index 0000000000..a034c6d3d5 --- /dev/null +++ b/apps/dreamverse/scripts/install_native_ffmpeg.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash +# +# Build & install a perf-optimized ffmpeg (LTO + libx264 + native arch). +# Mirrors the team playbook's flags verbatim per stage, with three +# deliberate deviations made necessary by our build host: +# +# 1. --disable-libxcb --disable-xlib ffmpeg's auto-detect linked +# libxcb at build time → binary +# wouldn't even start at runtime. +# 2. LIBRARY_PATH / LD_LIBRARY_PATH conda-forge gcc wrappers search +# + -L$INSTALL_PREFIX/lib early $CONDA_PREFIX/lib implicitly, +# leaking an older libx264 into +# ffmpeg's link. Forcing our prefix +# first makes the resolver pick +# our just-built lib. +# 3. MAKE_JOBS cap (default 16) very high -j (e.g. nproc=96 on +# NVL72) tripped a race in +# ffmpeg's recursive recipes. +# +# Usage: +# bash scripts/install_native_ffmpeg.sh +# +# Knobs (env vars, all optional): +# INSTALL_PREFIX install destination default: $HOME/opt/ffmpeg-native +# SOURCE_DIR build workspace default: $HOME/src/ffmpeg-native +# X264_REF x264 git ref default: stable +# FFMPEG_REF FFmpeg git ref default: n7.1 +# NV_CODEC_REF nv-codec-headers ref default: master +# CUDA_PREFIX CUDA toolkit root default: /usr/local/cuda +# ENABLE_NVENC build with NVENC/NVDEC default: 1 (1|0) +# MAKE_JOBS parallel make jobs default: min(nproc, 16) +# FFMPEG_NATIVE_CC explicit C compiler command for native builds +# FFMPEG_NATIVE_CXX explicit C++ compiler command for native builds +# +# Toolchain selection: CC / CXX / AS are pinned to the conda-forge +# triplet matching `uname -m`. Inherited values are intentionally +# ignored — conda envs that have BOTH `gcc_linux-64` and +# `gcc_linux-aarch64` installed export the cross-compiler triplet on +# every `conda activate` (the `aarch64` activation script sorts later +# and wins), which silently breaks x264's compiler probe on the +# opposite host. If you genuinely need a non-host toolchain, set +# FFMPEG_NATIVE_CC and/or FFMPEG_NATIVE_CXX explicitly. +set -euo pipefail + +# ─── Defaults (override via env) ────────────────────────────────────────── +INSTALL_PREFIX="${INSTALL_PREFIX:-$HOME/opt/ffmpeg-native}" +SOURCE_DIR="${SOURCE_DIR:-$HOME/src/ffmpeg-native}" +X264_REF="${X264_REF:-stable}" +FFMPEG_REF="${FFMPEG_REF:-n7.1}" +NV_CODEC_REF="${NV_CODEC_REF:-master}" +CUDA_PREFIX="${CUDA_PREFIX:-/usr/local/cuda}" +ENABLE_NVENC="${ENABLE_NVENC:-1}" +case "${ENABLE_NVENC}" in + 0|1) ;; + *) echo "[install_native_ffmpeg] ENABLE_NVENC must be 0 or 1, got '${ENABLE_NVENC}'" >&2; exit 1 ;; +esac +NPROC="$(nproc)" +MAKE_JOBS="${MAKE_JOBS:-$(( NPROC < 16 ? NPROC : 16 ))}" + +# ─── Per-platform, per-stage flags (verbatim from the playbook) ─────────── +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) + DEFAULT_CC=x86_64-conda-linux-gnu-cc + DEFAULT_CXX=x86_64-conda-linux-gnu-c++ + AS=nasm + X264_CFLAGS="-O3 -march=native -mtune=native -fPIC -flto" + X264_LDFLAGS="-flto -fuse-linker-plugin" + FFMPEG_CFLAGS="-O3 -march=native -mtune=native -fPIC -flto" + FFMPEG_LDFLAGS="-flto -Wl,-rpath,$INSTALL_PREFIX/lib" + ;; + aarch64) + DEFAULT_CC=aarch64-conda-linux-gnu-cc + DEFAULT_CXX=aarch64-conda-linux-gnu-c++ + unset AS # GNU as on ARM + X264_CFLAGS="-O3 -mcpu=native -fPIC -flto" + X264_LDFLAGS="-flto -fuse-linker-plugin" + FFMPEG_CFLAGS="-O3 -mcpu=native -fPIC -flto -fno-tree-vectorize" + FFMPEG_LDFLAGS="-flto -Wl,-rpath,$INSTALL_PREFIX/lib" + ;; + *) + echo "[install_native_ffmpeg] unsupported arch: $ARCH" >&2 + exit 1 + ;; +esac + +CC="${FFMPEG_NATIVE_CC:-$DEFAULT_CC}" +CXX="${FFMPEG_NATIVE_CXX:-$DEFAULT_CXX}" + +require_compiler() { + local name="$1" compiler="$2" + if [[ -z "$compiler" ]]; then + echo "[install_native_ffmpeg] $name is empty" >&2 + exit 1 + fi + if ! command -v -- "$compiler" >/dev/null 2>&1; then + echo "[install_native_ffmpeg] $name is unavailable: $compiler" >&2 + exit 1 + fi + if ! "$compiler" --version >/dev/null 2>&1; then + echo "[install_native_ffmpeg] $name failed sanity check: $compiler --version" >&2 + exit 1 + fi +} + +require_compiler CC "$CC" +require_compiler CXX "$CXX" +export CC CXX +[[ -n "${AS:-}" ]] && export AS +echo "[install_native_ffmpeg] toolchain: CC=$CC CXX=$CXX AS=${AS:-<gnu-as>} (uname -m=$ARCH)" + +# ─── Step 0: probe required tools ───────────────────────────────────────── +required=("$CC" "$CXX" make pkg-config git) +[[ "$(uname -m)" == "x86_64" ]] && required+=(nasm) +missing=() +for cmd in "${required[@]}"; do + command -v -- "$cmd" >/dev/null 2>&1 || missing+=("$cmd") +done +if (( ${#missing[@]} > 0 )); then + echo "[install_native_ffmpeg] missing required tools: ${missing[*]}" >&2 + echo "[install_native_ffmpeg] install them, or set FFMPEG_NATIVE_CC/FFMPEG_NATIVE_CXX explicitly." >&2 + exit 1 +fi + +# ─── Step 0.5: destructive-path guards ──────────────────────────────────── +guard_path() { + local name="$1" value="$2" + case "$value" in + "") echo "[install_native_ffmpeg] $name is empty" >&2; exit 1 ;; + "/") echo "[install_native_ffmpeg] refusing to wipe '/'" >&2; exit 1 ;; + "$HOME") echo "[install_native_ffmpeg] refusing to wipe \$HOME" >&2; exit 1 ;; + esac + [[ "$value" == /* ]] || { + echo "[install_native_ffmpeg] $name must be absolute, got: $value" >&2; exit 1; } + [[ "$value" == *ffmpeg-native* ]] || { + echo "[install_native_ffmpeg] $name must contain 'ffmpeg-native' for safety, got: $value" >&2 + exit 1; } +} +guard_path INSTALL_PREFIX "$INSTALL_PREFIX" +guard_path SOURCE_DIR "$SOURCE_DIR" + +# ─── Step 1: clean ──────────────────────────────────────────────────────── +echo "[install_native_ffmpeg] cleaning prior install + sources" +rm -rf "$INSTALL_PREFIX" "$SOURCE_DIR/x264" "$SOURCE_DIR/ffmpeg" "$SOURCE_DIR/nv-codec-headers" +mkdir -p "$SOURCE_DIR" "$INSTALL_PREFIX/lib" + +if [[ "$ENABLE_NVENC" == "1" ]]; then + if [[ ! -f "$CUDA_PREFIX/include/cuda.h" ]]; then + echo "[install_native_ffmpeg] ENABLE_NVENC=1 but cuda.h not at $CUDA_PREFIX/include/cuda.h" >&2 + echo "[install_native_ffmpeg] set CUDA_PREFIX env to your CUDA toolkit root, or ENABLE_NVENC=0 to skip" >&2 + exit 1 + fi + echo "[install_native_ffmpeg] NVENC build enabled (CUDA_PREFIX=$CUDA_PREFIX)" +fi + +# Deviation 2: force our prefix to win over conda-forge gcc's implicit +# library search (otherwise an older libx264 from conda's lib dir leaks +# into ffmpeg's link, producing a binary that needs *two* x264 SONAMEs). +export LIBRARY_PATH="$INSTALL_PREFIX/lib${LIBRARY_PATH:+:$LIBRARY_PATH}" +export LD_LIBRARY_PATH="$INSTALL_PREFIX/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +# ─── Step 2: build x264 (LTO, shared lib, no CLI) ───────────────────────── +echo "[install_native_ffmpeg] cloning x264 ($X264_REF)" +git clone --depth 1 --branch "$X264_REF" \ + https://code.videolan.org/videolan/x264.git "$SOURCE_DIR/x264" +( + cd "$SOURCE_DIR/x264" + export CFLAGS="$X264_CFLAGS" + export CXXFLAGS="$X264_CFLAGS" + export LDFLAGS="$X264_LDFLAGS" + echo "[install_native_ffmpeg] configuring x264" + ./configure --prefix="$INSTALL_PREFIX" --enable-shared --enable-pic --disable-cli + echo "[install_native_ffmpeg] building x264 (-j$MAKE_JOBS)" + make -j"$MAKE_JOBS" + make install +) + +# ─── Step 2.5: nv-codec-headers (NVENC/NVDEC API headers, no CUDA libs) ── +# Required for ffmpeg's --enable-cuda --enable-nvenc --enable-cuvid configure +# flags. Installs ffnvcodec.pc + headers into $INSTALL_PREFIX so ffmpeg's +# pkg-config picks them up alongside libx264. The runtime libraries +# (libcuda.so, libnvcuvid.so, libnvidia-encode.so) come from the NVIDIA +# driver, not from these headers. +if [[ "$ENABLE_NVENC" == "1" ]]; then + echo "[install_native_ffmpeg] cloning nv-codec-headers ($NV_CODEC_REF)" + git clone --depth 1 --branch "$NV_CODEC_REF" \ + https://git.videolan.org/git/ffmpeg/nv-codec-headers.git \ + "$SOURCE_DIR/nv-codec-headers" + ( + cd "$SOURCE_DIR/nv-codec-headers" + make PREFIX="$INSTALL_PREFIX" install + ) +fi + +# ─── Step 3: build ffmpeg (LTO, libx264, shared, optional NVENC) ────────── +echo "[install_native_ffmpeg] cloning FFmpeg ($FFMPEG_REF)" +git clone --depth 1 --branch "$FFMPEG_REF" \ + https://github.com/FFmpeg/FFmpeg.git "$SOURCE_DIR/ffmpeg" +( + cd "$SOURCE_DIR/ffmpeg" + export PKG_CONFIG_PATH="$INSTALL_PREFIX/lib/pkgconfig" + export CFLAGS="$FFMPEG_CFLAGS" + export CXXFLAGS="$FFMPEG_CFLAGS" + export LDFLAGS="$FFMPEG_LDFLAGS" + [[ "$(uname -m)" == "x86_64" ]] && which nasm + pkg-config --modversion x264 + echo "[install_native_ffmpeg] configuring ffmpeg" + ffmpeg_configure_flags=( + --prefix="$INSTALL_PREFIX" + --enable-gpl + --enable-libx264 + --enable-lto + --enable-shared + --disable-static + --disable-debug + --disable-doc + --disable-ffplay + --disable-libxcb + --disable-xlib + --extra-cflags="$CFLAGS" + --extra-cxxflags="$CXXFLAGS" + --extra-ldflags="-L$INSTALL_PREFIX/lib $LDFLAGS" + ) + if [[ "$ENABLE_NVENC" == "1" ]]; then + pkg-config --modversion ffnvcodec + ffmpeg_configure_flags+=( + --enable-cuda + --enable-nvenc + --enable-cuvid + --enable-nvdec + --extra-cflags="-I$CUDA_PREFIX/include" + --extra-ldflags="-L$CUDA_PREFIX/lib64" + ) + fi + ./configure "${ffmpeg_configure_flags[@]}" + echo "[install_native_ffmpeg] building ffmpeg (-j$MAKE_JOBS)" + make -j"$MAKE_JOBS" + make install +) + +# ─── Step 4: sanity check ────────────────────────────────────────────────── +ffmpeg_bin="$INSTALL_PREFIX/bin/ffmpeg" +echo "[install_native_ffmpeg] verifying $ffmpeg_bin" +"$ffmpeg_bin" -hide_banner -buildconf | grep -i -E 'libx264|lto' +"$ffmpeg_bin" -hide_banner -encoders | grep -i libx264 +"$ffmpeg_bin" -hide_banner -h encoder=libx264 2>&1 | grep -i preset +if [[ "$ENABLE_NVENC" == "1" ]]; then + "$ffmpeg_bin" -hide_banner -encoders | grep -E 'h264_nvenc|hevc_nvenc' || { + echo "[install_native_ffmpeg] ENABLE_NVENC=1 but built ffmpeg has no h264_nvenc/hevc_nvenc encoder" >&2 + exit 1 + } +fi + +# ─── Step 5: emit env file ───────────────────────────────────────────────── +# FASTVIDEO_VIDEO_CODEC stays at libx264 by default to preserve runtime +# behavior; NVENC is selected at deploy time via dreamverse-deploy.sh +# --nvenc, which exports FASTVIDEO_VIDEO_CODEC=h264_nvenc. +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +env_file="$script_dir/ffmpeg-env.sh" +{ + printf '#!/usr/bin/env bash\n' + printf 'export FASTVIDEO_FFMPEG_BIN=%q\n' "$ffmpeg_bin" + printf 'export FASTVIDEO_VIDEO_CODEC=libx264\n' +} > "$env_file" +chmod +x "$env_file" + +echo +echo "[install_native_ffmpeg] ✓ done." +echo "[install_native_ffmpeg] binary: $ffmpeg_bin" +echo "[install_native_ffmpeg] env: $env_file" +echo "[install_native_ffmpeg] source it before running the demo:" +echo "[install_native_ffmpeg] source scripts/ffmpeg-env.sh" diff --git a/apps/dreamverse/scripts/launch/README.md b/apps/dreamverse/scripts/launch/README.md new file mode 100644 index 0000000000..39a07c8529 --- /dev/null +++ b/apps/dreamverse/scripts/launch/README.md @@ -0,0 +1,36 @@ +# Dreamverse Launch Scripts + +These scripts are convenience wrappers for local demos and lower-level backend +checks. The main Dreamverse README documents the normal manual startup path. + +## One-Command Demo + +From the FastVideo checkout: + +```bash +apps/dreamverse/scripts/launch/launch_demo.sh +``` + +The launcher starts the Dreamverse backend and frontend, polls readiness, and +prints the active URLs. It defaults to `dreamverse-server` on backend port +`8009` and frontend port `5274`. + +Useful overrides: + +```bash +BE_PORT=8010 FE_PORT=5274 apps/dreamverse/scripts/launch/launch_demo.sh +NO_FRONTEND=1 apps/dreamverse/scripts/launch/launch_demo.sh +NO_BROWSER=1 apps/dreamverse/scripts/launch/launch_demo.sh +``` + +## Individual Scripts + +`launch_backend_dreamverse.sh` starts the full Dreamverse backend path used by +the web app. + +`launch_frontend.sh` starts the Next.js frontend and installs `pnpm` +dependencies when `node_modules/` is missing. + +`launch_backend_fastvideo.sh` starts the typed `fastvideo serve --config` path +for lower-level serve-config checks. It is not a full Dreamverse app replacement +because the current frontend still depends on Dreamverse-specific routes. diff --git a/apps/dreamverse/scripts/launch/launch_backend_dreamverse.sh b/apps/dreamverse/scripts/launch/launch_backend_dreamverse.sh new file mode 100755 index 0000000000..889541be3c --- /dev/null +++ b/apps/dreamverse/scripts/launch/launch_backend_dreamverse.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Launch the Dreamverse-flavored backend via the installed console command. +# +# This is the path the Next.js frontend expects today — it serves +# ``/healthz``, ``/readyz``, ``/status``, ``/curated-presets``, and the +# devtools-only routes that ``fastvideo serve`` does not. Use this for +# the full demo experience until the FE-only routes migrate into +# ``fastvideo.entrypoints.streaming.server.build_app``. +# +# Usage: +# bash launch_backend_dreamverse.sh # default 0.0.0.0:8009 +# bash launch_backend_dreamverse.sh --port 8010 +# +# Mirrors internal/ui defaults via the same env variables internal's +# ``config.py`` reads (see ../../../serve_configs/streaming_demo.yaml +# for the canonical list and source-of-truth comments). + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +if [[ -f "${HOME}/.env" ]]; then + set -o allexport + # shellcheck disable=SC1091 + source "${HOME}/.env" + set +o allexport +fi + +# Internal/ui parity defaults — only set if the caller hasn't already +# pinned them in ~/.env or the surrounding environment. Each value +# matches FastVideo-internal/ui/ltx2-streaming/server/config.py. +export FASTVIDEO_ATTENTION_BACKEND="${FASTVIDEO_ATTENTION_BACKEND:-FLASH_ATTN}" +export STREAM_MODE="${STREAM_MODE:-av_fmp4}" +export ENABLE_TORCH_COMPILE="${ENABLE_TORCH_COMPILE:-1}" +export FASTVIDEO_ENABLE_STARTUP_WARMUP="${FASTVIDEO_ENABLE_STARTUP_WARMUP:-1}" +export FASTVIDEO_STARTUP_WARMUP_TIMEOUT_SECONDS="${FASTVIDEO_STARTUP_WARMUP_TIMEOUT_SECONDS:-2400}" +export FASTVIDEO_GENERATION_SEGMENT_CAP="${FASTVIDEO_GENERATION_SEGMENT_CAP:-6}" +export FASTVIDEO_PROMPT_AUTO_SLEEP_MS="${FASTVIDEO_PROMPT_AUTO_SLEEP_MS:-120}" +export FASTVIDEO_PROMPT_AUTO_TIMEOUT_MS="${FASTVIDEO_PROMPT_AUTO_TIMEOUT_MS:-1800}" + +cd "${DREAMVERSE_ROOT}" + +if ! command -v dreamverse-server >/dev/null 2>&1; then + echo "error: dreamverse-server not found on PATH. Install FastVideo with the dreamverse extra." >&2 + exit 1 +fi + +echo "[launch-demo] starting dreamverse-server" +echo " args: $*" +exec dreamverse-server "$@" diff --git a/apps/dreamverse/scripts/launch/launch_backend_fastvideo.sh b/apps/dreamverse/scripts/launch/launch_backend_fastvideo.sh new file mode 100755 index 0000000000..8693eaad01 --- /dev/null +++ b/apps/dreamverse/scripts/launch/launch_backend_fastvideo.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Launch the FastVideo streaming backend via the typed +# ``fastvideo serve --config`` entrypoint, driven by +# ``serve_configs/streaming_demo.yaml``. +# +# Usage: +# bash launch_backend_fastvideo.sh +# bash launch_backend_fastvideo.sh --server.port 8010 --streaming.warmup.enabled false +# +# Anything passed after the script name is forwarded verbatim to +# ``fastvideo serve``, so dotted overrides like +# ``--server.port 8010`` or ``--streaming.warmup.enabled false`` work +# without a parallel flag scheme. +# +# Caveat: the bare ``fastvideo serve`` build_app exposes ``/health`` +# and ``/v1/stream`` only. Dreamverse's existing Next.js shell also +# expects ``/healthz``, ``/readyz``, and ``/curated-presets`` which +# live in dreamverse-server. Use ``launch_backend_dreamverse.sh`` for +# the full FE compatibility path; this script is for verifying the +# typed serve config and bare-streaming-server flow. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +SERVE_CONFIG="${DREAMVERSE_ROOT}/serve_configs/streaming_demo.yaml" + +if [[ ! -f "${SERVE_CONFIG}" ]]; then + echo "error: serve config not found at ${SERVE_CONFIG}" >&2 + exit 1 +fi + +# Source ~/.env if present so prompt-enhancer credentials +# (CEREBRAS_API_KEY etc.) are visible to the worker process. +if [[ -f "${HOME}/.env" ]]; then + set -o allexport + # shellcheck disable=SC1091 + source "${HOME}/.env" + set +o allexport +fi + +# Match internal/ui's attention backend default (gpu_pool.py:161). +export FASTVIDEO_ATTENTION_BACKEND="${FASTVIDEO_ATTENTION_BACKEND:-FLASH_ATTN}" + +cd "${DREAMVERSE_ROOT}" + +echo "[launch-demo] starting fastvideo serve" +echo " config: ${SERVE_CONFIG}" +echo " overrides: $*" +exec uv run fastvideo serve --config "${SERVE_CONFIG}" "$@" diff --git a/apps/dreamverse/scripts/launch/launch_demo.sh b/apps/dreamverse/scripts/launch/launch_demo.sh new file mode 100755 index 0000000000..13d1a3863b --- /dev/null +++ b/apps/dreamverse/scripts/launch/launch_demo.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# One-command Dreamverse demo launcher. +# +# Spawns the backend and frontend, polls health, prints URLs, and +# stops both children on Ctrl-C. +# +# Defaults match internal/ui: +# * BE = dreamverse-server (8009) — full FE compatibility +# * FE = Next.js dev:devtools (5274) +# +# Switch BE to fastvideo serve --config (typed-only path): +# BE_FLAVOR=fastvideo bash launch_demo.sh +# +# Other env knobs: +# BE_PORT=8010 FE_PORT=5274 bash launch_demo.sh +# NO_FRONTEND=1 bash launch_demo.sh # backend only +# NO_BROWSER=1 bash launch_demo.sh # skip xdg-open + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +LOG_DIR="${DREAMVERSE_ROOT}/logs" +mkdir -p "${LOG_DIR}" + +BE_FLAVOR="${BE_FLAVOR:-dreamverse}" +BE_PORT="${BE_PORT:-8009}" +FE_PORT="${FE_PORT:-5274}" +HEALTH_TIMEOUT_SECONDS="${HEALTH_TIMEOUT_SECONDS:-300}" +READY_TIMEOUT_SECONDS="${READY_TIMEOUT_SECONDS:-2400}" + +case "${BE_FLAVOR}" in + dreamverse) + BE_SCRIPT="${SCRIPT_DIR}/launch_backend_dreamverse.sh" + BE_PORT_ARGS=(--port "${BE_PORT}") + HEALTH_PATH="/healthz" + READY_PATH="/readyz" + ;; + fastvideo) + BE_SCRIPT="${SCRIPT_DIR}/launch_backend_fastvideo.sh" + # fastvideo serve takes dotted overrides on top of the YAML; the + # nested ServerConfig path is server.port not --port. + BE_PORT_ARGS=(--server.port "${BE_PORT}") + # bare fastvideo serve only exposes /health; treat it as both the + # liveness and readiness probe. + HEALTH_PATH="/health" + READY_PATH="/health" + ;; + *) + echo "error: BE_FLAVOR must be 'dreamverse' or 'fastvideo' (got '${BE_FLAVOR}')" >&2 + exit 1 + ;; +esac + +BE_URL="http://localhost:${BE_PORT}" +FE_URL="http://localhost:${FE_PORT}" +BE_LOG="${LOG_DIR}/demo-be.log" +FE_LOG="${LOG_DIR}/demo-fe.log" + +cleanup() { + local rc=$? + echo + echo "[launch-demo] shutting down children" + if [[ -n "${BE_PID:-}" ]] && kill -0 "${BE_PID}" 2>/dev/null; then + kill "${BE_PID}" 2>/dev/null || true + fi + if [[ -n "${FE_PID:-}" ]] && kill -0 "${FE_PID}" 2>/dev/null; then + kill "${FE_PID}" 2>/dev/null || true + fi + wait 2>/dev/null || true + exit "${rc}" +} +trap cleanup EXIT INT TERM + +probe() { + local url="$1" + curl -fsS -m 2 -o /dev/null "${url}" +} + +wait_for() { + local url="$1" + local label="$2" + local timeout="$3" + local started end + started="$(date +%s)" + end="$((started + timeout))" + while (( $(date +%s) < end )); do + if probe "${url}"; then + echo "[launch-demo] ${label} ready: ${url}" + return 0 + fi + sleep 2 + done + echo "error: ${label} did not respond within ${timeout}s at ${url}" >&2 + return 1 +} + +# --- backend --- +echo "[launch-demo] backend logs: ${BE_LOG}" +( "${BE_SCRIPT}" "${BE_PORT_ARGS[@]}" >"${BE_LOG}" 2>&1 ) & +BE_PID=$! +echo "[launch-demo] backend PID ${BE_PID} (flavor=${BE_FLAVOR}, port=${BE_PORT})" + +if ! wait_for "${BE_URL}${HEALTH_PATH}" "backend health" "${HEALTH_TIMEOUT_SECONDS}"; then + echo "------ backend log (tail) ------" >&2 + tail -n 60 "${BE_LOG}" >&2 || true + exit 1 +fi + +if [[ "${HEALTH_PATH}" != "${READY_PATH}" ]]; then + echo "[launch-demo] waiting for backend readiness (warmup may take a few minutes)…" + if ! wait_for "${BE_URL}${READY_PATH}" "backend ready" "${READY_TIMEOUT_SECONDS}"; then + echo "------ backend log (tail) ------" >&2 + tail -n 100 "${BE_LOG}" >&2 || true + exit 1 + fi +fi + +# --- frontend --- +if [[ "${NO_FRONTEND:-0}" != "1" ]]; then + echo "[launch-demo] frontend logs: ${FE_LOG}" + ( "${SCRIPT_DIR}/launch_frontend.sh" >"${FE_LOG}" 2>&1 ) & + FE_PID=$! + echo "[launch-demo] frontend PID ${FE_PID} (port ${FE_PORT})" + + if ! wait_for "${FE_URL}/" "frontend" 60; then + echo "------ frontend log (tail) ------" >&2 + tail -n 60 "${FE_LOG}" >&2 || true + exit 1 + fi + + if [[ "${NO_BROWSER:-0}" != "1" ]] && command -v xdg-open >/dev/null 2>&1; then + xdg-open "${FE_URL}/" >/dev/null 2>&1 || true + fi +fi + +cat <<EOF + +[launch-demo] stack is up. Press Ctrl-C to stop. + backend : ${BE_URL} (${BE_FLAVOR}, log: ${BE_LOG}) + frontend: ${FE_URL} (log: ${FE_LOG}) + +EOF + +wait "${BE_PID}" diff --git a/apps/dreamverse/scripts/launch/launch_frontend.sh b/apps/dreamverse/scripts/launch/launch_frontend.sh new file mode 100755 index 0000000000..6b3bde41c2 --- /dev/null +++ b/apps/dreamverse/scripts/launch/launch_frontend.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Launch the Dreamverse Next.js frontend in dev mode on port 5274 +# (the devtools-enabled build the e2e tests target). +# +# Usage: +# bash launch_frontend.sh +# FRONTEND_MODE=dev bash launch_frontend.sh # plain dev (5299, no devtools) +# FRONTEND_MODE=single5s bash launch_frontend.sh # single-5s product mode +# +# The script ``cd``'s into ``web`` and shells out to pnpm. It runs +# ``pnpm install --frozen-lockfile`` only when ``node_modules/`` is missing so repeat +# launches are fast. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +WEB_ROOT="${DREAMVERSE_ROOT}/web" + +FRONTEND_MODE="${FRONTEND_MODE:-devtools}" +case "${FRONTEND_MODE}" in + devtools|dev|single5s) ;; + *) + echo "error: FRONTEND_MODE must be one of devtools|dev|single5s (got '${FRONTEND_MODE}')" >&2 + exit 1 + ;; +esac + +if [[ ! -d "${WEB_ROOT}" ]]; then + echo "error: web app not found at ${WEB_ROOT}" >&2 + exit 1 +fi + +cd "${WEB_ROOT}" + +if [[ ! -d node_modules ]]; then + echo "[launch-demo] node_modules missing — running pnpm install --frozen-lockfile" + pnpm install --frozen-lockfile +fi + +case "${FRONTEND_MODE}" in + devtools) + echo "[launch-demo] starting Next.js dev:devtools (port 5274)" + exec pnpm run dev:devtools -- "$@" + ;; + dev) + echo "[launch-demo] starting Next.js dev (port 5299)" + exec pnpm run dev -- "$@" + ;; + single5s) + echo "[launch-demo] starting Next.js dev:single5s (port 5274)" + exec pnpm run dev:single5s -- "$@" + ;; +esac diff --git a/apps/dreamverse/scripts/smoke_local.sh b/apps/dreamverse/scripts/smoke_local.sh new file mode 100755 index 0000000000..8e70967764 --- /dev/null +++ b/apps/dreamverse/scripts/smoke_local.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOST="${BACKEND_HOST:-127.0.0.1}" +PORT="${BACKEND_PORT:-8009}" +BACKEND_ORIGIN="http://${HOST}:${PORT}" +TIMEOUT_SECONDS="${DREAMVERSE_SMOKE_TIMEOUT_SECONDS:-240}" +POLL_INTERVAL_SECONDS="${DREAMVERSE_SMOKE_POLL_SECONDS:-2}" +BACKEND_LOG_PATH="${DREAMVERSE_SMOKE_LOG_PATH:-${ROOT_DIR}/outputs/smoke-local-backend.log}" +START_BACKEND="${DREAMVERSE_SMOKE_START_BACKEND:-1}" + +backend_pid="" +started_backend=0 + +cleanup() { + if [[ "${started_backend}" == "1" && -n "${backend_pid}" ]]; then + kill "${backend_pid}" >/dev/null 2>&1 || true + wait "${backend_pid}" >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Missing required command: $1" >&2 + exit 1 + fi +} + +probe_json() { + local path="$1" + curl --silent --show-error --fail "${BACKEND_ORIGIN}${path}" +} + +wait_for_endpoint() { + local path="$1" + local label="$2" + local deadline=$((SECONDS + TIMEOUT_SECONDS)) + while (( SECONDS < deadline )); do + if probe_json "${path}" >/dev/null 2>&1; then + return 0 + fi + sleep "${POLL_INTERVAL_SECONDS}" + done + echo "Timed out waiting for ${label} at ${BACKEND_ORIGIN}${path}" >&2 + return 1 +} + +require_command curl + +mkdir -p "$(dirname "${BACKEND_LOG_PATH}")" + +if ! probe_json "/healthz" >/dev/null 2>&1; then + if [[ "${START_BACKEND}" != "1" ]]; then + echo "Dreamverse backend is not reachable at ${BACKEND_ORIGIN} and auto-start is disabled." >&2 + exit 1 + fi + + require_command dreamverse-server + + echo "Starting Dreamverse backend on ${BACKEND_ORIGIN}..." + ( + cd "${ROOT_DIR}" + exec dreamverse-server --host "${HOST}" --port "${PORT}" + ) >"${BACKEND_LOG_PATH}" 2>&1 & + backend_pid=$! + started_backend=1 +else + echo "Dreamverse backend already running on ${BACKEND_ORIGIN}." +fi + +echo "Waiting for /healthz..." +wait_for_endpoint "/healthz" "healthz" +echo "Waiting for /readyz..." +wait_for_endpoint "/readyz" "readyz" + +status_payload="$(probe_json "/status")" +echo "Dreamverse local smoke check passed." +echo "Backend URL: ${BACKEND_ORIGIN}" +echo "Status: ${status_payload}" + +if [[ "${started_backend}" == "1" ]]; then + trap - EXIT + echo "Backend PID: ${backend_pid}" + echo "Backend log: ${BACKEND_LOG_PATH}" + echo "Backend is still running so you can launch the frontend:" + echo " cd ${ROOT_DIR}/web" + echo " BACKEND_HOST=${HOST} BACKEND_PORT=${PORT} pnpm run dev" +else + echo "You can now launch the frontend:" + echo " cd ${ROOT_DIR}/web" + echo " BACKEND_HOST=${HOST} BACKEND_PORT=${PORT} pnpm run dev" +fi diff --git a/apps/dreamverse/serve_configs/streaming_demo.yaml b/apps/dreamverse/serve_configs/streaming_demo.yaml new file mode 100644 index 0000000000..9db217df2a --- /dev/null +++ b/apps/dreamverse/serve_configs/streaming_demo.yaml @@ -0,0 +1,153 @@ +# Dreamverse streaming demo — fastvideo serve --config target. +# +# This config is for testing Dreamverse-like streaming through the lower-level +# `fastvideo serve --config` entrypoint. Use `dreamverse-server` for the full +# Dreamverse web app. +# +# Mirrors ../FastVideo-internal/ui/ltx2-streaming/server/config.py +# defaults so the public typed surface produces the same runtime +# behavior as the internal UI's GPU pool. Sources of each setting are +# annotated inline; deviations are explicit. +# +# Boot: uv run fastvideo serve --config serve_configs/streaming_demo.yaml +# Override at the CLI: +# uv run fastvideo serve --config <yaml> \ +# --server.port 8010 \ +# --streaming.warmup.enabled false +# +# Required environment (sourced from ~/.env when launched via the +# launch-demo skill): +# * CEREBRAS_API_KEY (prompt enhancement, default provider) +# * CEREBRAS_IFM_API_KEY (only required if you set +# streaming.prompt.provider to cerebras_ifm +# via dotted override; the public typed +# schema currently only accepts cerebras / +# groq, so cerebras_ifm flows via env on +# dreamverse-server, not fastvideo serve) +# * GROQ_API_KEY (only if you switch provider to groq) +# * OPENAI_API_KEY (downstream rewrites in some prompt +# system prompts) +# +# Hosts without flashinfer / NVFP4: comment out the +# `engine.quantization` block below — the loader falls back to bf16 +# automatically when no quant_config is set. + +generator: + # internal: MODEL_REGISTRY["fast-ltx2"] (config.py:14-19) + model_path: FastVideo/LTX2-Distilled-Diffusers + + engine: + # internal: NUM_GPUS=1 hardcoded in gpu_pool.py worker boot + num_gpus: 1 + + # internal: gpu_pool.py:251-258 — all offload flags False so the + # full DiT, text encoder, and VAE stay resident on GPU. + offload: + dit: false + dit_layerwise: false + text_encoder: false + vae: false + pin_cpu_memory: true + + # internal: gpu_pool.py:251-258 + the just-uncommented "mode" line + # — torch.compile on for transformer + text encoder, inductor + # backend, fullgraph for graph-break debugging, max-autotune- + # no-cudagraphs for the kernel sweep, dynamic off (static shapes). + compile: + enabled: true + text_encoder_enabled: true + backend: inductor + fullgraph: true + mode: max-autotune-no-cudagraphs + dynamic: false + + # internal: pipeline_config.dit_config.quant_config = FP4Config() + # set in gpu_pool.py:280 (via the legacy in-place mutation). The + # public typed surface resolves "NVFP4" to NVFP4Config() and pins + # it on dit_config in FastVideoArgs.__post_init__. Comment this + # block out on hosts without flashinfer / NVFP4 hardware. + quantization: + transformer_quant: NVFP4 + + pipeline: + workload_type: t2v + # internal: pipeline_config.vae_tiling left default (False). + vae_tiling: false + + components: + # Same as model_path; LTX-2 reads its tokenizer / scheduler + # config from the same root. + config_root: FastVideo/LTX2-Distilled-Diffusers + + # internal: gpu_pool.py:259-265 ltx2_refine_* kwargs. + # The distilled stage-2 refine runs in 2 inference steps with + # gs=1.0 and add_noise=True (no LoRA — empty path). + preset_overrides: + refine: + enabled: true + num_inference_steps: 2 + guidance_scale: 1.0 + add_noise: true + +server: + # internal: main.py defaults (host 0.0.0.0, port 8009). + host: 0.0.0.0 + port: 8009 + # internal: outputs/ relative to server dir. + output_dir: outputs/ + +# default_request is the operator-pinned baseline merged into every +# /v1/stream session_init. Internal/ui hardcodes these in gpu_pool.py +# (NUM_FRAMES, FRAME_HEIGHT, FRAME_WIDTH, NUM_INFERENCE_STEPS, +# TARGET_FPS); we put them on default_request.sampling so explicit +# client overrides still win. +default_request: + sampling: + num_frames: 121 # internal: config.py:36 NUM_FRAMES + height: 1088 # internal: config.py:37 FRAME_HEIGHT + width: 1920 # internal: config.py:38 FRAME_WIDTH + num_inference_steps: 5 # internal: config.py:39 NUM_INFERENCE_STEPS + fps: 24 # internal: gpu_pool.py:85 TARGET_FPS + +streaming: + # internal: config.py:33 SESSION_TIMEOUT_SECONDS = 300 + session_timeout_seconds: 300 + # internal: config.py:282-284 GENERATION_SEGMENT_CAP default 6 + generation_segment_cap: 6 + # internal: config.py:46 STREAM_MODE default "av_fmp4" + stream_mode: av_fmp4 + + # internal: config.py:288-300 STARTUP_WARMUP_* + warmup: + enabled: true + prompt: "A cinematic drone shot over coastal cliffs at sunrise, golden light, gentle ocean waves, ultra detailed" + timeout_seconds: 2400 + + # internal: gpu_pool.py session-controller defaults — 9 conditioning + # frames, 0 end-offset, audio re-encode on (matches Dreamverse + # session_controller and FastVideo-internal video_generation paths). + pool: + num_workers: 1 # one worker per GPU; gpu_pool spawns based on CUDA_VISIBLE_DEVICES + enable_audio_reencode: true + conditioning_num_frames: 9 + conditioning_end_offset: 0 + + # internal: PROMPT_PROVIDER = "cerebras_ifm" hardcoded in config.py:143. + # The public typed Literal is currently {"cerebras", "groq"} only — + # cerebras_ifm requires the legacy env-driven path on dreamverse-server. + # For the typed fastvideo serve path we default to "cerebras" with the + # same model id; switch to groq via dotted override if cerebras is down. + prompt: + enabled: true + provider: cerebras + model: gpt-oss-120b # internal: config.py:189 PROMPT_MODEL + timeout_ms: 20000 # internal: config.py:216 PROMPT_TIMEOUT_MS + # system_prompt_dir intentionally unset; the public PromptEnhancer + # falls back to its packaged defaults when None. Set to an absolute + # directory if you want to pin operator-edited prompts. + + # internal: PROMPT_SAFETY_ENABLED default False (config.py:115). + # Enable + provide classifier_path on hosts with the fasttext extra + # installed (uv sync --extra safety in Dreamverse). + safety: + enabled: false diff --git a/apps/dreamverse/web/.gitignore b/apps/dreamverse/web/.gitignore new file mode 100644 index 0000000000..6829dc565e --- /dev/null +++ b/apps/dreamverse/web/.gitignore @@ -0,0 +1,17 @@ +/node_modules +/.next +/out +/coverage +*.tsbuildinfo + +# local env files +.env*.local + +# package manager logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# macOS +.DS_Store diff --git a/apps/dreamverse/web/components.json b/apps/dreamverse/web/components.json new file mode 100644 index 0000000000..f982f493e4 --- /dev/null +++ b/apps/dreamverse/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "slate", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "ui": "@/components/ui", + "lib": "@/lib", + "utils": "@/lib/utils", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/dreamverse/web/e2e/backend-health.spec.ts b/apps/dreamverse/web/e2e/backend-health.spec.ts new file mode 100644 index 0000000000..cc542c07cd --- /dev/null +++ b/apps/dreamverse/web/e2e/backend-health.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; + +/** + * Smoke layer 1: the Dreamverse Python server is reachable and the + * Next.js rewrite proxy at /healthz forwards to it. This runs before + * any UI interaction so a UI failure has a known-good baseline. + */ +test.describe('backend health', () => { + test('healthz returns ok via the next.js rewrite', async ({ request }) => { + const response = await request.get('/healthz'); + expect(response.ok()).toBeTruthy(); + const body = await response.json(); + expect(body.status).toBe('ok'); + expect(body.service).toBe('ltx2-streaming-backend'); + }); + + test('readyz reports gpu pool state', async ({ request }) => { + // /readyz returns 200 once the GPU pool is warm; 503 with a + // {detail: ...} body otherwise. Either is a valid integration + // signal — we just want to confirm the route is wired and the + // payload shape is what the FE expects. + const response = await request.get('/readyz'); + expect([200, 503]).toContain(response.status()); + const text = await response.text(); + if (response.status() === 503) { + // Frontend reads .detail to render the "wait for warmup" banner. + expect(text).toMatch(/detail/); + } + }); + + test('status endpoint exposes gpu pool snapshot', async ({ request }) => { + const response = await request.get('/status'); + expect(response.ok()).toBeTruthy(); + const body = await response.json(); + expect(body).toHaveProperty('total_gpus'); + expect(body).toHaveProperty('gpu_status'); + expect(typeof body.total_gpus).toBe('number'); + }); + + test('prompt-system-config exposes the operator-tunable prompts', async ({ request }) => { + const response = await request.get('/prompt-system-config'); + expect(response.ok()).toBeTruthy(); + const body = await response.json(); + // page.tsx reads these to seed the composer when the user opens + // the prompt-edit drawer; missing keys = silent UI breakage. + expect(body).toHaveProperty('next_segment_system_prompt'); + expect(body).toHaveProperty('auto_extension_system_prompt'); + expect(body).toHaveProperty('rewrite_window_system_prompt'); + }); + + test('curated presets endpoint serves a non-empty list (devtools only)', async ({ request }) => { + const response = await request.get('/curated-presets'); + test.skip( + response.status() === 404, + 'Skipping curated-presets check: backend was not booted with ' + + 'FASTVIDEO_ENABLE_DEVTOOLS=1, so the devtools-only route is not ' + + 'mounted. Re-run with that env var to exercise it.', + ); + expect(response.ok()).toBeTruthy(); + const body = await response.json(); + expect(Array.isArray(body.presets)).toBeTruthy(); + expect(body.presets.length).toBeGreaterThan(0); + for (const preset of body.presets) { + expect(preset).toHaveProperty('id'); + expect(preset).toHaveProperty('label'); + expect(Array.isArray(preset.segment_prompts)).toBeTruthy(); + } + }); +}); diff --git a/apps/dreamverse/web/e2e/frontend-shell.spec.ts b/apps/dreamverse/web/e2e/frontend-shell.spec.ts new file mode 100644 index 0000000000..13e88a6479 --- /dev/null +++ b/apps/dreamverse/web/e2e/frontend-shell.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +/** + * Smoke layer 2: the Next.js shell renders without crashing and shows + * the expected GPU-warmup banner when the backend's GPU pool isn't + * ready yet. This catches integration breakage between the frontend + * (running against the public FastVideo backend) and the readyz + * payload shape. + */ +test.describe('frontend shell', () => { + test('main page loads and exposes the FastVideo brand chip', async ({ page }) => { + await page.goto('/'); + // The TopStatusBar element is keyed off aria-label="FastVideo" + // and is rendered before any session interaction is possible. + await expect(page.getByRole('img', { name: 'FastVideo' })).toBeVisible({ + timeout: 30_000, + }); + }); + + test('composer hydrates with curated preset cards', async ({ page }) => { + await page.goto('/'); + + // The Continuation prompt textarea + Generate button render once + // the FE has hydrated against the public-FastVideo-backed + // dreamverse-server. Their presence proves the integration handshake + // (CORS, /curated-presets, /prompt-system-config) completed. + const continuation = page.getByLabel('Continuation prompt'); + await expect(continuation).toBeVisible({ timeout: 30_000 }); + + const generate = page.getByRole('button', { name: /^generate$/i }); + await expect(generate).toBeVisible({ timeout: 30_000 }); + + // Curated presets render as buttons; verify at least one is + // available — that's the only way the user can populate the + // Continuation textarea in the default composer. + const presetCard = page.getByRole('button', { + name: /LEGO Stormtroopers|Clay Stop-Motion|Boy & Dog|School Prank|Gamer Gets Banned|Small Town Oil Strike|Grandpa's Wing Costume/i, + }).first(); + await expect(presetCard).toBeVisible({ timeout: 30_000 }); + }); +}); diff --git a/apps/dreamverse/web/e2e/long-running-segments.spec.ts b/apps/dreamverse/web/e2e/long-running-segments.spec.ts new file mode 100644 index 0000000000..0ed06420fb --- /dev/null +++ b/apps/dreamverse/web/e2e/long-running-segments.spec.ts @@ -0,0 +1,171 @@ +import { test, expect, type WebSocket as PWWebSocket } from '@playwright/test'; + +/** + * Long-running e2e: drive a real two-segment session end-to-end with + * torch.compile + warmup ENABLED on the backend, and assert audio + * conditioning carries from segment 1 → segment 2 without the + * BrokenPipe regression documented in + * `.agents/memory/dreamverse-integration/decisions-log.md` D-20. + * + * Skipped by default. Opt in with PLAYWRIGHT_LONG_RUNNING=1, and + * boot the backend with both knobs on: + * + * ./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh \ + * --warmup --torch-compile 4 + * PLAYWRIGHT_SKIP_WEBSERVER=1 \ + * BACKEND_HOST=127.0.0.1 \ + * BACKEND_PORT=8009 \ + * PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \ + * NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \ + * PLAYWRIGHT_LONG_RUNNING=1 \ + * pnpm exec playwright test e2e/long-running-segments.spec.ts + * + * The full run takes ~7-9 minutes on a B200: ~3-4min for torch.compile + * max-autotune to warm both DiT + text-encoder graphs, then ~30s for + * segment 1 inference and ~10s for segment 2. The default per-test + * timeout is bumped to 900_000 ms below. + */ +const LONG_RUNNING_ENABLED = process.env.PLAYWRIGHT_LONG_RUNNING === '1'; + +interface WSEvent { + raw: string | Buffer; + parsed?: { + type?: string; + segment_idx?: number; + message?: string; + [k: string]: unknown; + }; + isBinary: boolean; +} + +test.describe('long-running two-segment audio continuation', () => { + test.skip( + !LONG_RUNNING_ENABLED, + 'Skipping long-running e2e — set PLAYWRIGHT_LONG_RUNNING=1 to enable. ' + + 'Requires backend booted with --warmup --torch-compile (~7-9 min run).', + ); + + test.beforeEach(async ({ request }) => { + const ready = await request.get('/readyz'); + test.skip( + !ready.ok(), + 'Skipping — /readyz did not return 200. Boot dreamverse-server first.', + ); + }); + + test('segment 1 + segment 2 stream cleanly with torch.compile + warmup', async ({ + page, + request, + }) => { + test.setTimeout(900_000); + + // Confirm the deploy actually has warmup on. We don't gate on + // torch.compile because there's no public API surface for it + // (operator-controlled via ENABLE_TORCH_COMPILE env var). + const statusResponse = await request.get('/status'); + expect(statusResponse.ok()).toBe(true); + const status = await statusResponse.json(); + expect( + status.warmup_enabled, + 'BE must be booted with --warmup for the long-running e2e to be meaningful', + ).toBe(true); + + // Collect every WS event from the FE's session WS (the only one + // the page opens). page.on('websocket') fires before the WS is + // navigated into, so registering before page.goto is safe. + const events: WSEvent[] = []; + const errors: string[] = []; + let mediaInitCount = 0; + let mediaSegmentCompleteCount = 0; + const segmentsSeen = new Set<number>(); + const segmentsCompleted = new Set<number>(); + + page.on('websocket', (ws: PWWebSocket) => { + ws.on('framereceived', ({ payload }) => { + const isBinary = typeof payload !== 'string'; + const evt: WSEvent = { raw: payload, isBinary }; + if (!isBinary) { + try { + evt.parsed = JSON.parse(payload as string); + } catch { + return; + } + } + events.push(evt); + const t = evt.parsed?.type; + const seg = evt.parsed?.segment_idx; + if (t === 'media_init' && typeof seg === 'number') { + mediaInitCount += 1; + segmentsSeen.add(seg); + } else if (t === 'media_segment_complete' && typeof seg === 'number') { + mediaSegmentCompleteCount += 1; + segmentsCompleted.add(seg); + } else if (t === 'error' || t === 'step_error') { + const msg = + (typeof evt.parsed?.message === 'string' && evt.parsed.message) || + JSON.stringify(evt.parsed); + errors.push(msg); + } + }); + }); + + await page.goto('/'); + + const continuation = page.getByLabel('Continuation prompt'); + await expect(continuation).toBeVisible({ timeout: 30_000 }); + + // Same preset selector as preset-prompt-generation.spec.ts — + // the FE auto-fires segments through the curated prompt list. + const firstPreset = page + .getByRole('button', { + name: /LEGO Stormtroopers|Clay Stop-Motion|Boy & Dog|School Prank|Gamer Gets Banned|Small Town Oil Strike|Grandpa's Wing Costume/i, + }) + .first(); + await expect(firstPreset).toBeVisible({ timeout: 30_000 }); + await firstPreset.click(); + + await expect(continuation).toBeDisabled({ timeout: 30_000 }); + + // Poll for segment 2 to complete. The FE auto-progresses through + // the preset's segment_prompts list once a session is started, so + // segment 1 → segment 2 happens without further user action. + const deadline = Date.now() + 850_000; + while (Date.now() < deadline) { + if (errors.length > 0) { + throw new Error( + `WS error frame received: ${errors.join(' | ')}\n` + + `events captured: init=${mediaInitCount} ` + + `complete=${mediaSegmentCompleteCount} ` + + `segments_seen=${[...segmentsSeen].sort().join(',')} ` + + `segments_completed=${[...segmentsCompleted].sort().join(',')}`, + ); + } + if (segmentsCompleted.has(1) && segmentsCompleted.has(2)) { + break; + } + await page.waitForTimeout(2_000); + } + + expect( + errors, + `Expected no WS error frames; got: ${errors.join(' | ')}`, + ).toHaveLength(0); + expect( + [...segmentsCompleted].sort(), + `Expected segments 1 AND 2 to complete; segments_seen=` + + `${[...segmentsSeen].sort().join(',')}, ` + + `mediaInitCount=${mediaInitCount}, ` + + `mediaSegmentCompleteCount=${mediaSegmentCompleteCount}`, + ).toEqual(expect.arrayContaining([1, 2])); + + // Sanity: segment 2 must have produced at least one binary chunk + // (the actual fMP4 bytes). If the BrokenPipe regression returns, + // ffmpeg closes stdin before any chunks are emitted and we'd + // see media_init for segment 2 but zero binary frames. + const binaryFrameCount = events.filter((e) => e.isBinary).length; + expect( + binaryFrameCount, + 'Expected at least one binary fMP4 chunk; got zero', + ).toBeGreaterThan(0); + }); +}); diff --git a/apps/dreamverse/web/e2e/preset-prompt-generation.spec.ts b/apps/dreamverse/web/e2e/preset-prompt-generation.spec.ts new file mode 100644 index 0000000000..ba22316335 --- /dev/null +++ b/apps/dreamverse/web/e2e/preset-prompt-generation.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; + +/** + * E2E layer: pick a curated preset, kick off generation, wait for the + * first segment to land, and assert the streamed media event arrived. + * + * Skipped automatically on hosts where the GPU pool is not ready — + * real video generation needs LTX-2 weights + flashinfer + a GPU. + * The smoke layer (backend-health.spec.ts, frontend-shell.spec.ts) + * still validates the integration without requiring real generation. + * + * Devtools mode is required because the Run button only renders when + * NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 (the production product surface + * keeps preset selection inside the floating composer). Set + * BACKEND_HOST + BACKEND_PORT + NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 before running. + */ +test.describe('preset prompt generation', () => { + test.beforeEach(async ({ request }) => { + const ready = await request.get('/readyz'); + test.skip( + !ready.ok(), + 'Skipping real-generation e2e — GPU pool is not warm. ' + + 'Boot dreamverse-server with FASTVIDEO_GPU_COUNT=1 + valid model ' + + 'weights, then re-run.', + ); + }); + + test('generates the first segment from a curated preset prompt', async ({ page }) => { + test.setTimeout(300_000); + + await page.goto('/'); + + // Wait for the Continuation prompt textarea — guarantees the page + // has hydrated and the curated presets fetched. + const continuation = page.getByLabel('Continuation prompt'); + await expect(continuation).toBeVisible({ timeout: 30_000 }); + + // The default-mode composer renders each curated preset as a + // button whose accessible name is "<title> <description>". + // Clicking a preset card auto-fires generation: the FE seeds the + // session with the preset's segment_prompts, opens the WS, and + // disables the Continuation textarea (placeholder flips to + // "Generating video…"). No separate Generate click is needed. + const firstPreset = page.getByRole('button', { + name: /LEGO Stormtroopers|Clay Stop-Motion|Boy & Dog|School Prank|Gamer Gets Banned|Small Town Oil Strike|Grandpa's Wing Costume/i, + }).first(); + await expect(firstPreset).toBeVisible({ timeout: 30_000 }); + await firstPreset.click(); + + // Continuation textarea flips to disabled with the "Generating + // video…" placeholder once the WS session starts; this is the + // canonical "generation in progress" signal in the FE. + await expect(continuation).toBeDisabled({ timeout: 30_000 }); + await expect(continuation).toHaveAttribute('placeholder', /generating/i); + + // Once the WS session has started, the FE renders the "Leave" + // button (replaces Generate while a session is active). That's + // the canonical FE signal that the WS handshake succeeded and the + // backend accepted the prompt. + const leaveButton = page.getByRole('button', { name: /^leave$/i }); + await expect(leaveButton).toBeVisible({ timeout: 60_000 }); + + // The <video> element is in the DOM and waits for MSE chunks; we + // don't assert visibility here because the codec/MSE path is a + // pure FE concern downstream of the integration boundary, and + // running a real LTX-2 segment without torch.compile takes ~65s + // on a B200 plus encode/transfer time. The "Continuation flipped + // to Generating + Leave button rendered" pair above is the proof + // the integration works: FE → /readyz → /curated-presets → WS + // /ws → BE → GPU pool → VideoGenerator.generate_video, all green. + const video = page.locator('video').first(); + await expect(video).toHaveCount(1); + }); +}); diff --git a/apps/dreamverse/web/index.html b/apps/dreamverse/web/index.html new file mode 100644 index 0000000000..db3be576f4 --- /dev/null +++ b/apps/dreamverse/web/index.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <link rel="icon" type="image/svg+xml" href="/icon-simple.svg" /> + <link rel="shortcut icon" href="/icon-simple.svg" /> + <title>Dreamverse + + +
+ + + diff --git a/apps/dreamverse/web/next-env.d.ts b/apps/dreamverse/web/next-env.d.ts new file mode 100644 index 0000000000..830fb594ca --- /dev/null +++ b/apps/dreamverse/web/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/dreamverse/web/next.config.ts b/apps/dreamverse/web/next.config.ts new file mode 100644 index 0000000000..2850139598 --- /dev/null +++ b/apps/dreamverse/web/next.config.ts @@ -0,0 +1,65 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { NextConfig } from 'next'; + +const backendHost = process.env.BACKEND_HOST || '127.0.0.1'; +const backendPort = Number(process.env.BACKEND_PORT) || 8009; +const backendUrl = `http://${backendHost}:${backendPort}`; +const configDir = path.dirname(fileURLToPath(import.meta.url)); + +const nextConfig: NextConfig = { + outputFileTracingRoot: path.join(configDir, '..', '..', '..'), + async rewrites() { + return [ + { + source: '/ws', + destination: `${backendUrl}/ws` + }, + { + source: '/healthz', + destination: `${backendUrl}/healthz` + }, + { + source: '/readyz', + destination: `${backendUrl}/readyz` + }, + { + source: '/models', + destination: `${backendUrl}/models` + }, + { + source: '/status', + destination: `${backendUrl}/status` + }, + { + source: '/router/:path*', + destination: `${backendUrl}/router/:path*` + }, + { + source: '/prompt-system-config', + destination: `${backendUrl}/prompt-system-config`, + }, + { + source: '/curated-presets', + destination: `${backendUrl}/curated-presets`, + }, + { + source: '/curated-presets/:path*', + destination: `${backendUrl}/curated-presets/:path*`, + }, + { + source: '/server-assets/:path*', + destination: `${backendUrl}/server-assets/:path*`, + }, + ]; + }, + webpack: (config) => { + config.module.rules.push({ + test: /\.jsonl$/, + type: 'asset/source', + }); + return config; + }, +}; + +export default nextConfig; diff --git a/apps/dreamverse/web/package-lock.json b/apps/dreamverse/web/package-lock.json new file mode 100644 index 0000000000..c56547bc52 --- /dev/null +++ b/apps/dreamverse/web/package-lock.json @@ -0,0 +1,11445 @@ +{ + "name": "wm-interface-frontend", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "wm-interface-frontend", + "version": "0.0.1", + "dependencies": { + "@carbon/icons-react": "^11.76.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.36.0", + "geist": "^1.7.0", + "lucide-react": "^0.577.0", + "mp4box": "^2.3.0", + "next": "^15.3.3", + "radix-ui": "^1.4.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/postcss": "^4.2.1", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.15.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^3.2.4", + "jsdom": "^26.1.0", + "mock-socket": "^9.3.1", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@carbon/icon-helpers": { + "version": "10.73.0", + "resolved": "https://registry.npmjs.org/@carbon/icon-helpers/-/icon-helpers-10.73.0.tgz", + "integrity": "sha512-Grn/QxHZfSOdXp/WBadegVfq1LT96QwX9b977dqMU5qmxp6cxKqUGvV1oqs6GiBacKoQFrH2xgbKwlcecgSHfA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/icons-react": { + "version": "11.76.0", + "resolved": "https://registry.npmjs.org/@carbon/icons-react/-/icons-react-11.76.0.tgz", + "integrity": "sha512-FsRlufW8lcdEX/Vj0NpdloBDPmXGRCIaD6ENhYt0f42zJncs6OqNQ58plqNteHg/JMQAdA+pP8f5tSkopNhRRg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@carbon/icon-helpers": "^10.73.0", + "@ibm/telemetry-js": "^1.5.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@ibm/telemetry-js": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@ibm/telemetry-js/-/telemetry-js-1.11.0.tgz", + "integrity": "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA==", + "license": "Apache-2.0", + "bin": { + "ibmtelemetry": "dist/collect.js" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/framer-motion": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.36.0.tgz", + "integrity": "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.36.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/geist": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.7.0.tgz", + "integrity": "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==", + "license": "SIL OPEN FONT LICENSE", + "peerDependencies": { + "next": ">=13.2.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/motion-dom": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.36.0.tgz", + "integrity": "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/mp4box": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.3.0.tgz", + "integrity": "sha512-nnABYbdh4UguEYyV+uRwQBi1tbb8kXka2Fx9yKzmDKAeh8gkvRKYxoK1XDd8GQIjSfN4rvsXrW1CBo4yRQJZDA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.8.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.12", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + }, + "dependencies": { + "@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true + }, + "@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "requires": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true + }, + "@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "requires": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, + "@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true + }, + "@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "requires": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + } + }, + "@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "requires": { + "@babel/types": "^7.29.0" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true + }, + "@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + } + }, + "@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + } + }, + "@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true + }, + "@carbon/icon-helpers": { + "version": "10.73.0", + "resolved": "https://registry.npmjs.org/@carbon/icon-helpers/-/icon-helpers-10.73.0.tgz", + "integrity": "sha512-Grn/QxHZfSOdXp/WBadegVfq1LT96QwX9b977dqMU5qmxp6cxKqUGvV1oqs6GiBacKoQFrH2xgbKwlcecgSHfA==", + "requires": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "@carbon/icons-react": { + "version": "11.76.0", + "resolved": "https://registry.npmjs.org/@carbon/icons-react/-/icons-react-11.76.0.tgz", + "integrity": "sha512-FsRlufW8lcdEX/Vj0NpdloBDPmXGRCIaD6ENhYt0f42zJncs6OqNQ58plqNteHg/JMQAdA+pP8f5tSkopNhRRg==", + "requires": { + "@carbon/icon-helpers": "^10.73.0", + "@ibm/telemetry-js": "^1.5.0", + "prop-types": "^15.8.1" + } + }, + "@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true + }, + "@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "requires": {} + }, + "@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "requires": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + } + }, + "@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true + }, + "@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "requires": { + "@floating-ui/utils": "^0.2.11" + } + }, + "@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "requires": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "requires": { + "@floating-ui/dom": "^1.7.6" + } + }, + "@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" + }, + "@ibm/telemetry-js": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@ibm/telemetry-js/-/telemetry-js-1.11.0.tgz", + "integrity": "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA==" + }, + "@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "optional": true + }, + "@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "optional": true + }, + "@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "optional": true + }, + "@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "optional": true + }, + "@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "optional": true + }, + "@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "optional": true + }, + "@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "optional": true + }, + "@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "optional": true + }, + "@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "optional": true + }, + "@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "optional": true, + "requires": { + "@emnapi/runtime": "^1.7.0" + } + }, + "@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "optional": true + }, + "@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "optional": true + }, + "@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "optional": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@next/env": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==" + }, + "@next/swc-darwin-arm64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "optional": true + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "devOptional": true, + "requires": { + "playwright": "1.59.1" + } + }, + "@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + }, + "@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "requires": { + "@radix-ui/react-visually-hidden": "1.2.3" + } + }, + "@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "requires": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + } + }, + "@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "requires": {} + }, + "@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "requires": {} + }, + "@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "requires": {} + }, + "@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + } + }, + "@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "requires": {} + }, + "@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + } + }, + "@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "dependencies": { + "@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + } + } + }, + "@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "requires": { + "@radix-ui/react-primitive": "2.1.4" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "requires": { + "@radix-ui/react-slot": "1.2.4" + } + } + } + }, + "@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + } + }, + "@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "requires": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + } + }, + "@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "requires": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + } + }, + "@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "requires": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "requires": { + "@radix-ui/react-slot": "1.2.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "requires": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + } + }, + "@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "requires": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "requires": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "requires": { + "@radix-ui/react-primitive": "2.1.4" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "requires": { + "@radix-ui/react-slot": "1.2.4" + } + } + } + }, + "@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "requires": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + } + }, + "@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + }, + "@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + } + }, + "@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + } + }, + "@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "dependencies": { + "@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + } + } + }, + "@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "dependencies": { + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "requires": {} + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "requires": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "requires": { + "@radix-ui/react-use-callback-ref": "1.1.1" + } + }, + "@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "requires": { + "use-sync-external-store": "^1.5.0" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "requires": {} + }, + "@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "requires": {} + }, + "@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "requires": { + "@radix-ui/rect": "1.1.1" + } + }, + "@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "dev": true, + "optional": true + }, + "@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "requires": { + "tslib": "^2.8.0" + } + }, + "@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "requires": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "requires": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + } + }, + "@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "dev": true, + "optional": true + }, + "@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "dev": true, + "optional": true + }, + "@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "requires": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + } + } + }, + "@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "requires": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "dependencies": { + "dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + } + } + }, + "@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, + "@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "requires": {} + }, + "@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, + "@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "requires": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "requires": { + "csstype": "^3.2.2" + } + }, + "@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "requires": {} + }, + "@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "requires": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + } + }, + "@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + } + }, + "@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "requires": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + } + }, + "@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "requires": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + } + }, + "@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "requires": { + "tinyrainbow": "^2.0.0" + } + }, + "@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "requires": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + } + }, + "@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "requires": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + } + }, + "@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "requires": { + "tinyspy": "^4.0.3" + } + }, + "@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "requires": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + } + }, + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true + }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, + "ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true + } + } + }, + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true + }, + "baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true + }, + "brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, + "browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "requires": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + } + }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==" + }, + "chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, + "check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true + }, + "class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "requires": { + "clsx": "^2.1.1" + } + }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "requires": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + } + }, + "csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "requires": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + } + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true + }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true + }, + "detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + } + }, + "entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true + }, + "es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, + "expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true + }, + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + } + }, + "framer-motion": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.36.0.tgz", + "integrity": "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==", + "requires": { + "motion-dom": "^12.36.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + } + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "geist": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/geist/-/geist-1.7.0.tgz", + "integrity": "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==", + "requires": {} + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^3.1.1" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "requires": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + } + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "requires": { + "detect-libc": "^2.0.3", + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "dev": true, + "optional": true + }, + "lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "dev": true, + "optional": true + }, + "lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "dev": true, + "optional": true + }, + "lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "dev": true, + "optional": true + }, + "lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "dev": true, + "optional": true + }, + "lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "dev": true, + "optional": true + }, + "lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "dev": true, + "optional": true + }, + "lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "dev": true, + "optional": true + }, + "lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "dev": true, + "optional": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "requires": {} + }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true + }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "requires": { + "brace-expansion": "^5.0.2" + } + }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + }, + "mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "dev": true + }, + "motion-dom": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.36.0.tgz", + "integrity": "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA==", + "requires": { + "motion-utils": "^12.36.0" + } + }, + "motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==" + }, + "mp4box": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.3.0.tgz", + "integrity": "sha512-nnABYbdh4UguEYyV+uRwQBi1tbb8kXka2Fx9yKzmDKAeh8gkvRKYxoK1XDd8GQIjSfN4rvsXrW1CBo4yRQJZDA==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" + }, + "next": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", + "requires": { + "@next/env": "15.5.12", + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "sharp": "^0.34.3", + "styled-jsx": "5.1.6" + }, + "dependencies": { + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + } + } + }, + "node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true + }, + "nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "requires": { + "entities": "^6.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, + "pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + }, + "playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "devOptional": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.59.1" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + } + } + }, + "playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "devOptional": true + }, + "postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "dependencies": { + "@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + } + } + }, + "react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==" + }, + "react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "requires": { + "scheduler": "^0.27.0" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true + }, + "react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "requires": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + } + }, + "react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "requires": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + } + }, + "react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "requires": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" + } + }, + "rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "devOptional": true + }, + "sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "optional": true, + "requires": { + "@img/colour": "^1.0.0", + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "requires": {} + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "requires": { + "ansi-regex": "^6.2.2" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true + } + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "requires": { + "js-tokens": "^9.0.1" + }, + "dependencies": { + "js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + } + } + }, + "styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "requires": { + "client-only": "0.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==" + }, + "tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true + }, + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true + }, + "test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + } + }, + "tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + } + }, + "tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true + }, + "tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true + }, + "tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true + }, + "tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "requires": { + "tldts-core": "^6.1.86" + } + }, + "tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true + }, + "tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "requires": { + "tldts": "^6.1.32" + } + }, + "tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "requires": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + } + }, + "use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "requires": {} + }, + "vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + } + }, + "vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + } + }, + "vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "requires": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + } + }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, + "whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "requires": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "requires": {} + }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/apps/dreamverse/web/package.json b/apps/dreamverse/web/package.json new file mode 100644 index 0000000000..900fd92c05 --- /dev/null +++ b/apps/dreamverse/web/package.json @@ -0,0 +1,67 @@ +{ + "name": "wm-interface-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "next dev --port 5299", + "dev:devtools": "NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 next dev --port 5274", + "dev:single5s": "NEXT_PUBLIC_PRODUCT_MODE=single5s next dev --port 5274", + "build": "next build", + "build:devtools": "NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 next build", + "build:single5s": "NEXT_PUBLIC_PRODUCT_MODE=single5s next build", + "typecheck": "tsc --noEmit", + "start": "next start --port 5274", + "start:devtools": "NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 next start --port 5274", + "start:single5s": "NEXT_PUBLIC_PRODUCT_MODE=single5s next start --port 5274", + "clean": "rm -rf .next coverage", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "e2e": "playwright test", + "e2e:headed": "playwright test --headed", + "e2e:debug": "PWDEBUG=1 playwright test" + }, + "dependencies": { + "@carbon/icons-react": "^11.76.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^12.36.0", + "geist": "^1.7.0", + "lucide-react": "^0.577.0", + "mp4box": "^2.3.0", + "next": "^15.3.3", + "radix-ui": "^1.4.3", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailwindcss/postcss": "^4.2.1", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.15.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.5.2", + "@vitest/coverage-v8": "^3.2.4", + "jsdom": "^26.1.0", + "mock-socket": "^9.3.1", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/apps/dreamverse/web/playwright.config.ts b/apps/dreamverse/web/playwright.config.ts new file mode 100644 index 0000000000..064d0a8572 --- /dev/null +++ b/apps/dreamverse/web/playwright.config.ts @@ -0,0 +1,48 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright config for Dreamverse end-to-end tests. + * + * Tests assume the Dreamverse Python server is reachable at + * BACKEND_HOST:BACKEND_PORT (default 127.0.0.1:8009) and the Next.js frontend + * runs on port 5274. The webServer block boots `pnpm run dev` if no + * server is already listening so tests work both locally and in CI. + */ +export default defineConfig({ + testDir: './e2e', + // Tests share a backend, so run sequentially to avoid contention on + // the single GPU pool slot during real-generation runs. Override per + // test via test.parallel if a test is safe to run alongside others. + fullyParallel: false, + workers: 1, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? 'github' : 'list', + timeout: 120_000, + expect: { timeout: 30_000 }, + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:5274', + headless: true, + viewport: { width: 1280, height: 720 }, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: process.env.PLAYWRIGHT_SKIP_WEBSERVER + ? undefined + : { + command: 'pnpm run dev', + url: 'http://127.0.0.1:5274', + reuseExistingServer: true, + timeout: 120_000, + env: { + BACKEND_HOST: process.env.BACKEND_HOST || '127.0.0.1', + BACKEND_PORT: process.env.BACKEND_PORT || '8009', + }, + }, +}); diff --git a/apps/dreamverse/web/pnpm-lock.yaml b/apps/dreamverse/web/pnpm-lock.yaml new file mode 100644 index 0000000000..31de8cd074 --- /dev/null +++ b/apps/dreamverse/web/pnpm-lock.yaml @@ -0,0 +1,5199 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@carbon/icons-react': + specifier: ^11.76.0 + version: 11.76.0(react@19.2.4) + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.14)(react@19.2.4) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + framer-motion: + specifier: ^12.36.0 + version: 12.36.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + geist: + specifier: ^1.7.0 + version: 1.7.0(next@15.5.12(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + lucide-react: + specifier: ^0.577.0 + version: 0.577.0(react@19.2.4) + mp4box: + specifier: ^2.3.0 + version: 2.3.0 + next: + specifier: ^15.3.3 + version: 15.5.12(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: ^19.1.0 + version: 19.2.4 + react-dom: + specifier: ^19.1.0 + version: 19.2.4(react@19.2.4) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + devDependencies: + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 + '@tailwindcss/postcss': + specifier: ^4.2.1 + version: 4.2.1 + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^22.15.0 + version: 22.19.15 + '@types/react': + specifier: ^19.1.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.7.0(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1)) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + mock-socket: + specifier: ^9.3.1 + version: 9.3.1 + postcss: + specifier: ^8.5.8 + version: 8.5.8 + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@carbon/icon-helpers@10.73.0': + resolution: {integrity: sha512-Grn/QxHZfSOdXp/WBadegVfq1LT96QwX9b977dqMU5qmxp6cxKqUGvV1oqs6GiBacKoQFrH2xgbKwlcecgSHfA==} + + '@carbon/icons-react@11.76.0': + resolution: {integrity: sha512-FsRlufW8lcdEX/Vj0NpdloBDPmXGRCIaD6ENhYt0f42zJncs6OqNQ58plqNteHg/JMQAdA+pP8f5tSkopNhRRg==} + peerDependencies: + react: '>=16' + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@ibm/telemetry-js@1.11.0': + resolution: {integrity: sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA==} + hasBin: true + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@next/env@15.5.12': + resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} + + '@next/swc-darwin-arm64@15.5.12': + resolution: {integrity: sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.12': + resolution: {integrity: sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.12': + resolution: {integrity: sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.5.12': + resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.5.12': + resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.5.12': + resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.5.12': + resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.12': + resolution: {integrity: sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.1': + resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.8: + resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001779: + resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.313: + resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + framer-motion@12.36.0: + resolution: {integrity: sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + geist@1.7.0: + resolution: {integrity: sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==} + peerDependencies: + next: '>=13.2.0' + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.577.0: + resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mock-socket@9.3.1: + resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} + engines: {node: '>= 8'} + + motion-dom@12.36.0: + resolution: {integrity: sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + mp4box@2.3.0: + resolution: {integrity: sha512-nnABYbdh4UguEYyV+uRwQBi1tbb8kXka2Fx9yKzmDKAeh8gkvRKYxoK1XDd8GQIjSfN4rvsXrW1CBo4yRQJZDA==} + engines: {node: '>=20.8.1'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@15.5.12: + resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@carbon/icon-helpers@10.73.0': + dependencies: + '@ibm/telemetry-js': 1.11.0 + + '@carbon/icons-react@11.76.0(react@19.2.4)': + dependencies: + '@carbon/icon-helpers': 10.73.0 + '@ibm/telemetry-js': 1.11.0 + prop-types: 15.8.1 + react: 19.2.4 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/utils@0.2.11': {} + + '@ibm/telemetry-js@1.11.0': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.9.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@15.5.12': {} + + '@next/swc-darwin-arm64@15.5.12': + optional: true + + '@next/swc-darwin-x64@15.5.12': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.12': + optional: true + + '@next/swc-linux-arm64-musl@15.5.12': + optional: true + + '@next/swc-linux-x64-gnu@15.5.12': + optional: true + + '@next/swc-linux-x64-musl@15.5.12': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.12': + optional: true + + '@next/swc-win32-x64-msvc@15.5.12': + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/postcss@4.2.1': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + postcss: 8.5.8 + tailwindcss: 4.2.1 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.8: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.8 + caniuse-lite: 1.0.30001779 + electron-to-chromium: 1.5.313 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + cac@6.7.14: {} + + caniuse-lite@1.0.30001779: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.313: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@6.0.1: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + framer-motion@12.36.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.36.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + geist@1.7.0(next@15.5.12(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + dependencies: + next: 15.5.12(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-escaper@2.0.2: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + indent-string@4.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.6.1: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.577.0(react@19.2.4): + dependencies: + react: 19.2.4 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + min-indent@1.0.1: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.3: {} + + mock-socket@9.3.1: {} + + motion-dom@12.36.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + mp4box@2.3.0: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + next@15.5.12(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@next/env': 15.5.12 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001779 + postcss: 8.4.31 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.12 + '@next/swc-darwin-x64': 15.5.12 + '@next/swc-linux-arm64-gnu': 15.5.12 + '@next/swc-linux-arm64-musl': 15.5.12 + '@next/swc-linux-x64-gnu': 15.5.12 + '@next/swc-linux-x64-musl': 15.5.12 + '@next/swc-win32-arm64-msvc': 15.5.12 + '@next/swc-win32-x64-msvc': 15.5.12 + '@playwright/test': 1.59.1 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-releases@2.0.36: {} + + nwsapi@2.2.23: {} + + object-assign@4.1.1: {} + + package-json-from-dist@1.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + get-nonce: 1.0.1 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.2.4: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): + dependencies: + client-only: 0.0.1 + react: 19.2.4 + optionalDependencies: + '@babel/core': 7.29.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.4 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.4 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + + vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1) + vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} diff --git a/apps/dreamverse/web/postcss.config.mjs b/apps/dreamverse/web/postcss.config.mjs new file mode 100644 index 0000000000..a34a3d560d --- /dev/null +++ b/apps/dreamverse/web/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/apps/dreamverse/web/prompts/selected_ltx2_continuation_story_presets.json b/apps/dreamverse/web/prompts/selected_ltx2_continuation_story_presets.json new file mode 100644 index 0000000000..5b6e57985d --- /dev/null +++ b/apps/dreamverse/web/prompts/selected_ltx2_continuation_story_presets.json @@ -0,0 +1,95 @@ +[ + { + "id": "death_star_console_delay_lego_funny", + "label": "LEGO Stormtroopers", + "description": "Comedy skit inside the Death Star control room", + "segment_prompts": [ + "A white LEGO stormtrooper minifigure stands at a glowing control console inside a metallic LEGO Death Star control room, studded gray walls and tiled black floor reflecting cold blue panel light across the glossy plastic helmet, which sits popped off beside the console revealing a simple printed face. The LEGO stormtrooper presses blocky yellow hands against the console buttons and says \"The requisition says twelve thousand ceremonial capes,\" then adds \"We don’t even have cloth physics.\" The console emits tiny electronic beeps while a low mechanical hum vibrates through the LEGO corridor. A blinking red 'APPROVED' tile glows on the display as the LEGO stormtrooper freezes in place.", + "The LEGO stormtrooper leans forward slightly, one hinged arm lifting to rub the top of his plastic head while scanning the scrolling brick-built supply manifest. The LEGO stormtrooper says \"It’s signed by High Command,\" then adds \"In permanent marker.\" A faint intercom crackle echoes overhead and small blue translucent studs blink steadily along the wall. The LEGO stormtrooper lowers his arm and remains perfectly still, staring at the absurd order.", + "The LEGO stormtrooper straightens up with a soft click of jointed arms dropping to his sides. The LEGO stormtrooper says \"Do you know how hard it is to sit in this armor,\" then adds \"Now imagine snapping on a cape.\" Cold blue lighting reflects cleanly off the smooth white plastic torso. The LEGO stormtrooper stands rigidly, square and unmoving like a posed minifigure.", + "The camera slowly pans across the LEGO control room, gliding past studded wall panels and smooth tiled floor pieces, and settles on another white LEGO stormtrooper minifigure at a secondary console on the far side of the room, helmet also removed and resting beside him. The LEGO stormtrooper looks up calmly and says \"Maybe it’s for morale,\" then adds \"Very blocky morale.\" The steady engine hum fills the space as the pan completes and locks on the LEGO stormtrooper.", + "The LEGO stormtrooper tilts his square head slightly and folds his rigid arms across his torso in one clean motion. The LEGO stormtrooper says \"Picture the hallway breeze,\" then adds \"Full hero click.\" Blue translucent studs blink softly behind the LEGO stormtrooper and a faint mechanical hiss escapes a nearby vent brick. The LEGO stormtrooper remains posed and still.", + "The LEGO stormtrooper lowers his arms with a small plastic click and looks toward the off-screen LEGO stormtrooper. The LEGO stormtrooper says \"At least it’s not glitter,\" then adds \"Yet.\" The deep station hum continues as the camera holds steady on the LEGO stormtrooper’s simple printed face, ending in a clean, stable frame." + ] + }, + + { + "id": "cat_litter_box_clay_custom", + "label": "Clay Stop-Motion Cat", + "description": "A picky clay cat rejects every litter box spot", + "segment_prompts": [ + "Style: cute - clay stop-motion. A medium shot frames a cozy miniature clay apartment living room with pastel-blue walls, a lumpy handmade couch, and a tiny bookshelf, all surfaces showing visible fingerprint textures and sculpted imperfections in soft diffused lighting. A round-faced clay man in his late twenties with chunky brown hair tufts, big dot eyes, and an oversized green sweater stands in the center holding a small gray clay litter box in both hands, turning his head slowly to survey the room. A chubby orange tabby clay cat with a round head, stubby legs, and a perpetually unimpressed expression sits motionless on the couch cushion, watching him with half-lidded eyes. The man looks down at the cat and says 'Okay buddy, we gotta figure out where this thing goes,' his clay mouth shifting into a lopsided grin as a gentle acoustic guitar pluck plays softly in the background.", + "Style: cute - clay stop-motion. The clay man bends his knees slowly and sets the gray litter box down on the floor beside the couch with a soft plastic thud, then straightens up and gestures toward it with both stubby hands, looking at the cat expectantly. The orange tabby looks down at the litter box, blinks once, then shakes its round head side to side in three slow deliberate jerks, its tiny triangle ears wobbling with the motion. The man's dot eyes widen and his clay mouth drops into a small 'o' shape as he says 'What? Not here?' and stands still with his arms at his sides, the warm apartment light holding steady on both figures.", + "Style: cute - clay stop-motion. The clay man holds the gray litter box under one arm and looks around the living room, then sets it down slowly beside the tiny bookshelf and says 'How about right here, huh?' while gesturing toward it with one stubby hand. The orange tabby on the couch stares at the litter box for a moment, then slowly raises one paw and places it flat over its own face, covering its eyes. The man's shoulders slump and he lets out a quiet sigh, picking the litter box back up as the warm apartment light holds steady on both figures.", + "Style: cute - clay stop-motion. The scene cuts to a close medium shot of a narrow clay bathroom with stark white tiled walls, dark grout lines, a smooth pale floor, and cool blue-tinted overhead light casting hard clean shadows across every surface. A round white clay toilet and a small clay sink sit against the back wall, the space tight and minimal. A round-faced clay man in a green sweater steps into frame from the right, gray litter box in hand, and carefully places it on the pale floor beside the toilet with a gentle tap. He crouches down and adjusts it with his stubby fingers, then looks over his shoulder toward the doorway and says 'Last chance — what do you think of this spot?' as a small chubby orange tabby clay cat appears in the doorway, its round head tilting slowly to one side while the soft drip of a clay faucet echoes off the hard tiled walls.", + "Style: cute - clay stop-motion. The orange tabby waddles forward in small bouncy stop-motion steps across the pale bathroom floor, its chubby body rocking side to side under the cool blue-tinted light, and pauses at the edge of the gray litter box to sniff it with a tiny sculpted nose. After a beat, the cat lifts one stubby front leg over the rim and steps inside, then the other, settling its round body down into the box with a satisfied little wiggle. A soft purring hum begins as the cat closes its eyes halfway into a slow contented blink, and the clay man watches from a crouch beside the toilet with his dot eyes growing wide and a broad smile spreading across his round face.", + "Style: cute - clay stop-motion. A slow gentle zoom pulls back to a wider shot of the white-tiled bathroom as the clay man rises to his feet and gives a big thumbs-up with his right hand, his green sweater wrinkling at the elbow in sculpted folds. The orange tabby sits peacefully in the litter box with its eyes in a calm slow-blink, its tiny tail curling once to the side. The man whispers 'Good talk, buddy' and rests his hands on his hips, nodding with quiet satisfaction as the soft acoustic guitar returns alongside the steady gentle purr. The cool blue-tinted light settles evenly across both clay figures, fingerprint textures catching the glow on every surface, as the camera holds on the still, warm moment of agreement between man and cat." + ] + }, + { + "id": "boy_walking_dog_park_custom", + "label": "Boy & Dog in the Park", + "description": "Pixar-style walk with a golden retriever", + "segment_prompts": [ + "Style: Pixar 3D animation - vibrant and warm. A wide establishing shot of a sunlit park with lush green grass, towering oak trees, and a winding stone path stretching into the distance, the golden afternoon light filtering through the canopy and casting dappled shadows across the ground. A boy, around nine years old with messy brown hair, freckles, and bright wide eyes, wearing a red t-shirt, khaki shorts, and white sneakers, walks along the path holding a blue leash attached to a golden retriever with a fluffy coat and big brown eyes, a blue collar with a silver tag jingling softly as they stroll. Birds chirp in the treetops and the boy's sneakers scuff lightly against the stone as the dog trots beside him with its tongue out, tail swaying in a steady rhythm. The boy glances down at the dog and grins, \"Come on, Biscuit, this way,\" as the camera slowly dollies forward along the path, settling on a stable medium shot of the pair walking side by side.", + "Style: Pixar 3D animation - vibrant and warm. The boy and his golden retriever continue along the stone path as a bright orange butterfly drifts into frame from the right, fluttering just ahead of them. The dog's ears perk up and its head tilts, big brown eyes locking onto the butterfly with intense curiosity, the silver tag on its blue collar clinking as it shifts forward. The boy feels the leash tighten slightly in his hand and looks down with a raised eyebrow, \"What do you see, buddy?\" as the butterfly loops lazily through the warm air. The dog's nose twitches and its front paws lift in small eager steps, the tail wagging faster now as the boy tightens his grip on the blue leash and watches the butterfly with an amused smile, the camera holding in a steady medium shot that keeps both the boy and the dog together in frame as the butterfly hovers just out of reach.", + "Style: Pixar 3D animation - vibrant and warm. The golden retriever pulls steadily forward on the leash, its paws stepping off the stone path onto the soft grass as the boy follows a step behind, both hands gripping the taut blue leash, his white sneakers leaving the path and pressing into the green blades. \"Whoa, Biscuit, easy,\" the boy says with a nervous laugh, leaning back slightly as the dog walks briskly toward a patch of wildflowers where the orange butterfly flutters low, the dog's nose stretched forward and its tail wagging in wide sweeps. The boy's messy brown hair shifts in the breeze and his red t-shirt pulls gently at the collar as he steadies his footing, a grin spreading across his freckled face while the dog lets out a soft eager whine. Grass rustles beneath their feet and the butterfly drifts lazily upward as the camera holds in a steady medium shot from the side, both figures moving gradually across the meadow before settling to a still frame.", + "Style: Pixar 3D animation - vibrant and warm. The scene cuts to a wide shot of a calm duck pond surrounded by smooth gray stones and tall reeds, the water reflecting the soft peach and gold tones of the lowering afternoon sun, a pair of white ducks gliding silently across the glassy surface. The boy stands at the water's edge catching his breath, his red t-shirt slightly untucked, one hand on his knee and the other still gripping the blue leash as the golden retriever sits beside him panting happily, its pink tongue hanging out and its fluffy tail sweeping the ground. \"You... are... so fast,\" the boy says between breaths, shaking his head with a wide grin as the dog looks up at him with cheerful brown eyes. A gentle breeze stirs the reeds and ripples the pond surface, the soft sound of water lapping against the stones filling the air as the camera holds steady on the pair framed against the warm glowing water.", + "Style: Pixar 3D animation - vibrant and warm. The boy lowers himself to sit on a smooth flat stone at the pond's edge, his sneakers dangling just above the waterline, and the golden retriever shifts closer, leaning its warm fluffy body against the boy's side with a contented sigh. The ducks drift slowly past and the dog watches them with calm half-lidded eyes, its earlier excitement completely spent, the silver tag glinting in the amber light. The boy rests one hand on the dog's back, fingers sinking into the soft golden fur, and the other hand drops the leash loosely into his lap as he watches the water. \"Good boy, Biscuit,\" he murmurs softly, the words blending with the quiet lap of water and a distant chorus of evening crickets beginning to chirp, as the camera drifts in a slow gentle zoom toward the pair, settling into a warm medium shot.", + "Style: Pixar 3D animation - vibrant and warm. The golden hour light deepens to a rich amber glow across the pond, the sky above shifting to soft streaks of pink and lavender as the water shimmers with warm reflections. The boy wraps both arms around the golden retriever's neck in a slow gentle hug, pressing his freckled cheek against the dog's soft fur, his eyes closing with a peaceful smile. The dog's tail gives three slow thumps against the stone and it tilts its head to rest against the boy's shoulder, big brown eyes blinking contentedly. The evening crickets grow slightly louder and one of the ducks lets out a quiet quack in the distance as the breeze carries a few fallen leaves across the still water. The camera holds in a steady close-up of the boy and dog embraced in the warm fading light, perfectly still." + ] + }, + + { + "id": "butterfly_wings_dad", + "label": "Grandpa's Wing Costume", + "description": "A grandpa wearing butterfly wings worries his adult kids", + "segment_prompts": [ + "Style: realistic with warm cinematic lighting. In a medium two-shot, a woman in her early 30s with dark shoulder-length hair wearing a light blue sundress and a man in his mid-30s with short brown hair wearing a plain gray t-shirt stand facing each other in a warm sunny backyard, a wooden fence and green hedges behind them. Afternoon sunlight falls across their serious faces. The woman looks at the man with a pained expression and says in a soft, strained voice 'That's it... Dad's lost it. And we've lost Dad.' Birds chirp in the trees and a faint breeze rustles the hedge leaves.", + "The man in the gray t-shirt exhales through his nose and tilts his head, his expression flat. He says in a dry, dismissive tone 'Stop being so dramatic, Jess.' A bird calls from a nearby tree and the faint buzz of summer insects fills the air. The woman blinks hard and presses her lips together.", + "The man in the gray t-shirt glances briefly to his right, then looks back and says in a low, defensive mutter 'He's just having fun.' The woman shakes her head slowly and her eyes glisten. A lawn mower hums faintly in the distance and warm sunlight shifts through the leaves overhead.", + "The camera whip-pans fast to the right, blurring past the fence and hedges, and lands on an elderly man in his late 70s with thin white hair and deep wrinkles standing alone in the garden wearing enormous bright costume butterfly wings on his outstretched arms in a wide shot. He flaps his arms up and down and shouts 'Wheeeew! Watch this!' in a loud, raspy, gleeful voice. He holds his arms wide in a triumphant pose, his wrinkled face beaming with a wide grin in the bright afternoon sunlight.", + "The elderly man with thin white hair and bright butterfly wings lifts his arms high and flaps them with joyful energy. He grins broadly, his wrinkled face lit up, and calls out 'I'm flying! Look, I'm really flying!' in a breathless, delighted, raspy voice. A dog barks beyond the fence and a sprinkler clicks nearby. He spreads his arms wide, standing proud in the warm sunlight.", + "The elderly man with thin white hair and butterfly wings lowers his arms gently, the colorful wings resting at his sides. He says in a warm, raspy, satisfied voice 'Best Tuesday ever' and chuckles softly. A woman's voice calls out 'Dad, please!' in a strained, tearful tone nearby. He stands peacefully in the warm garden light, his wrinkled face settled into a content smile." + ] + }, + { + "id": "gaming_ban", + "label": "Gamer Gets Banned", + "description": "A teen reacts to getting banned while his friend watches", + "segment_prompts": [ + "A teenage boy with short dark hair sits at a desk in a dimly lit bedroom, the blue glow of a gaming monitor illuminating his face, LED strips casting purple light along the walls behind him. He wears a black hoodie and a gaming headset rests around his neck, his hands on a keyboard as he leans forward squinting at the screen. He mutters 'Come on, come on, almost got him' while clicking rapidly, the mechanical keyboard clacking and the hum of a PC fan filling the room. His eyes widen and his mouth drops open as a red notification flashes across the monitor, and he whispers 'Wait... what?'", + "He leans back in his gaming chair, staring at the monitor with a stunned expression. He says 'Banned? Are you serious right now?' in a sharp, incredulous tone. His jaw tightens and he shakes his head slowly. The chair creaks under his weight and the monitor casts a harsh red glow across his face.", + "He throws both hands up briefly and lets them drop onto the armrests of his chair. He says 'It was just a mod, it's not even that deep' with an exasperated huff. He slumps back and exhales loudly, the LED lights humming faintly behind him.", + "The camera whip-pans fast to the right, blurring past the desk and monitor and LED-lit wall, and lands on a second teenage boy with curly brown hair wearing a white t-shirt, leaning against the open bedroom doorframe in a medium shot. He has his arms crossed and a flat, unsurprised expression. He says 'Bro, I literally told you not to use that injector' in a calm, matter-of-fact tone. Warm hallway light spills in from behind him.", + "The boy in the white t-shirt shakes his head slowly and says 'You always do this, man. Every single game.' He shifts his weight against the doorframe and raises one eyebrow. He smirks slightly, the hallway light glowing behind him.", + "The boy in the white t-shirt uncrosses his arms and says 'Just make a new account and stop cheating this time' with a slight laugh. He shakes his head with a small grin and leans back against the doorframe, the warm hallway light settling around him." + ] + }, + { + "id": "garden_sign", + "label": "School Prank", + "description": "A student paints the principal's name on the garden sign", + "segment_prompts": [ + "A sunny school garden with raised wooden flower beds filled with marigolds and tomato plants, a hand-painted wooden sign in the center reading large messy red letters. A teenage boy in a blue t-shirt and jeans stands to the left of the sign with his hands in his pockets, and a middle-aged woman in a gray blazer stands to the right, her arms crossed, both visible in a wide shot under bright midday sun. The woman stares at the sign and says \"Tyler, what on earth is this?\" Birds chirp in the trees overhead and a light breeze rustles the garden leaves.", + "The boy shifts his weight and glances sideways at the sign, then looks back at the woman with a nervous half-smile. He says \"I thought it would be nice to name the garden after you, Mrs. Marshers.\" The woman unfolds her arms and points at the sign, a faint hum of bees buzzing near the flower beds.", + "The woman takes a slow step closer to the sign, her jaw set, and says \"Nice? You used permanent paint on school property.\" The boy winces slightly and looks down at his shoes, a sprinkler clicking softly somewhere behind the flower beds.", + "Everything blurs into streaks of green and brown as the view whips sideways, then snaps into focus on a bright classroom with rows of desks and a whiteboard on the wall. A teenage girl with dark hair in a red hoodie sits at a desk on the left side of the frame, and a boy with glasses in a green jacket sits two desks away on the right, a wide gap of empty desks between them in a wide shot under fluorescent lights. The girl leans sideways across the gap and whispers \"Did you hear Tyler painted the principal's name on the garden sign?\" A clock ticks on the wall above them.", + "The boy with glasses grins and covers his mouth, saying \"No way, in big red letters?\" The girl nods and stifles a laugh, her shoulders shaking slightly. A pencil rolls off the edge of her desk and clatters softly on the floor.", + "The girl glances toward the door and then back at the boy, saying \"She's making him do garden duty for a whole month.\" The boy shakes his head with a wide smile and says \"That's actually kind of perfect,\" the fluorescent light humming faintly above them as they settle back in their seats." + ] + }, + { + "id": "oil_strike_reporter", + "label": "Small Town Oil Strike", + "description": "A reporter covers an oil geyser erupting in Vermont", + "segment_prompts": [ + "A warm early-morning sun lights a small-town street where a news reporter in a dark suit stands centered in a medium shot, microphone in hand, yellow caution tape and cordoned-off cars visible behind him. The reporter looks directly at the camera with composed excitement and says 'Thank you, Sylvia — this morning, here in quiet New Castle, Vermont, black gold has been found!' A faint hum of distant drilling and soft chatter drifts through the air. The reporter pauses, a grin spreading across his face as golden light reflects off the camera lens.", + "The reporter grins broadly and gestures with his free hand toward the area behind him. He says 'If my cameraman can pan over, you'll see what all the excitement's about.' The drilling sound grows louder in the background and the yellow caution tape flutters in a light breeze. The reporter lowers his hand, eyes bright with anticipation.", + "The camera pans slowly to the right, revealing a construction site where workers in bright hard hats stand around a tall drilling rig. A worker near the rig cups his hands and shouts 'We got pressure building!' The morning sun glints off metal equipment and a low rumble vibrates through the ground. The camera settles on the wide view of the site, workers watching the rig with tense anticipation.", + "Dark oil bursts from the base of the drilling rig with a deep roar, spraying outward as a thick black column rises from the ground. Workers in hard hats step back and one shouts 'That's it — that's the one!' Oil mist drifts across the construction site and dark droplets spatter the dirt around the rig. The workers stand in a loose group, arms raised in celebration, framed in a wide ground-level shot with the geyser climbing behind them.", + "Workers in hard hats cheer and clap in a wide ground-level shot, oil mist drifting across the construction site as dark droplets spatter the dirt. The reporter's voice calls out 'There it is, folks — the moment New Castle will never forget!' The thick black geyser roars behind the group, spraying upward beyond the top of the frame. A worker pumps his fist and the rumble of the eruption echoes across the ground.", + "The camera pulls back at the start to a wider ground-level shot showing the full construction site, cordoned-off cars, and small-town rooftops in the distance, the black geyser rising from the rig beyond the top of the frame. The reporter says 'You are watching history unfold, right here on a Tuesday morning in Vermont.' Oil mist settles gently over the site and workers stand together near the rig, faces turned upward. The morning sun catches the drifting haze as the scene settles into the wide view of the quiet town." + ] + } +] diff --git a/apps/dreamverse/web/prompts/selected_ltx2_simple_prompts.jsonl b/apps/dreamverse/web/prompts/selected_ltx2_simple_prompts.jsonl new file mode 100644 index 0000000000..6bbf4474dc --- /dev/null +++ b/apps/dreamverse/web/prompts/selected_ltx2_simple_prompts.jsonl @@ -0,0 +1,48 @@ +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_14", "video_prompt": "An old man in his late 70s with a short grey beard and a worn brown coat is sitting with his back against the wide, rough bark of a big oak tree, legs stretched on the cool grass as three children-an 8-year-old girl with braided hair in a denim jacket, a 10-year-old boy in a red hoodie, and a 6-year-old girl in a yellow dress-are clustered close, leaning in and looking up at him under a clear, starry sky. Soft moonlight is washing the scene in pale blue while dozens of fireflies are flickering around them, casting small points of warm yellow light that pulse in time with a gentle breeze rustling the oak leaves; the breeze causes a soft, continuous rustle and occasional low creaks from the branches. The old man is speaking slowly and warmly, his weathered hand gesturing toward a knot in the tree as he tells the story; beneath his voice a quiet night soundscape is present-distant cricket chirps, mild wind through grass, and the subtle shuffle of the children as they shift closer. Old Man (deep, slow, warm): \"Long ago this tree used to hold a lantern that guided lost travelers...\" he says, pausing to tap the knot and smile, his voice carrying low and steady over the soft night sounds. Boy (bright, quick, curious): \"Did they ever find their way without the lantern?\" the boy asks, eyes wide, leaning forward and brushing a blade of grass as the fireflies flicker near his hand. Old Man (soft, amused, steady): \"Sometimes they did, sometimes they learned to follow the stars,\" he replies, chuckling softly and nodding, his words blending with the rustle of leaves; the children exhale in a small, collective whisper of wonder and a brief, delighted giggle rises as a firefly glows nearby. Throughout, the audio remains intimate and natural-clear, close-up dialogue with the old man's voice dominant, layered over gentle ambient night SFX (wind through leaves, distant insects) and occasional tiny, bright pops of light from the fireflies visually accenting beats in the conversation."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_16", "video_prompt": "A slender alien with pale green skin, sparse white tendrils for hair, and large dark eyes is crouched before an old cathode-ray TV sitting on a scratched metal crate in a dim, cluttered observation alcove; they wear a simple gray tunic and lean forward with a focused, curious expression, fingertips hovering over the TV's worn knobs as a soft amber glow from the curved screen washes across their face. The CRT displays a grainy, monochrome rotating Earth with visible scanlines and intermittent static; as the alien turns a dial the globe sharpens briefly then jitters with horizontal rolling lines, while the TV cabinet emits a low electrical hum, a steady mechanical whine, and sharp, brief pops from the speaker. The alien tilts their head, squints, and taps the side of the set, then speaks in a soft, slow, curious voice, \"That look like home?\" A crackly, low-pitched announcer voice from the TV replies in a distorted, monotone cadence, \"Signal detected: human transmissions-faint, local.\" The alien exhales a small, puzzled sound and murmurs back in a quieter, puzzled tone, \"Listen close... what are they saying?\" Tiny ambient sounds layer under the exchange: distant ship machinery humming, the gentle rattle of tools in the alcove, the TV's hiss and white noise filling pauses, and a faint, muffled snippet of Earth's city ambience-distant car horns and a passing voice-bleeding through the static as the alien leans in while the scene holds for a brief, attentive moment."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_29", "video_prompt": "A young girl with shoulder-length brown hair tied in a bouncing ponytail is running down a sunlit suburban street, wearing a knee-length pink skirt and a fitted blue t-shirt; her white sneakers slam in quick, rhythmic strikes against rough asphalt as the skirt flutters and her ponytail snaps with each stride. Soft late-afternoon light casts long, gentle shadows across the concrete curb and the texture of small pebbles in the road is visible; a few parked cars line the sidewalk and a stray leaf skitters along at her feet. Footstep SFX: rapid sneaker impacts, light skirt swish, and a short intake of breath on each stride; ambient sound: distant traffic hum, a passing car whoosh and faint bird calls. Mid-run she glances over her shoulder, pausing her pace just enough to call out in a breathy, urgent, slightly high-pitched voice, \"Wait up!\" (spoken with quick cadence), then exhales with a soft pant and pushes forward again, her arms pumping and shoes kicking up tiny dust puffs as the street ambience continues-occasional distant horn and a muted dog bark-while the sound of her footsteps and breath remain prominent and in sync with her motion."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_37", "video_prompt": "A medium close-up of a middle-aged moderator at a wooden podium, wearing a dark navy suit and thin-rim glasses, short salt-and-pepper hair slightly tousled, smiling with bright eyes as he gestures with one hand and leans slightly forward; soft overhead conference lighting casts even, neutral illumination and a large LED screen behind him shows a pulsing schematic of interconnected nodes labeled Intelligent Neural Net in plain white text. Moderator - excited, slightly breathless, fast pace, mid-high pitch: \"Moderator: (Excitedly) Finally, we have succeeded in building the most advanced super AI system, the "Intelligent Neural Net"! It will be the most powerful AI system in human history!\" He speaks the first sentence with a rising inflection while raising both hands, then taps the side of the podium on the last phrase, causing a brief synthesized chime and the LED schematic to glow brighter; a soft microphone pop precedes his voice, and immediately as he finishes the line a swell of applause and cheers rises from the off-screen audience, mixed with a low, steady server-rack hum and the faint clicking of camera shutters. Subtle ambient conference sounds (murmur, chair rustle) sit under the action while the projection pulses in time with the chime, and the moderator holds his smile, breathing slightly faster, as the applause continues."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_38", "video_prompt": "A small village of stone cottages with a mix of thatched and slate roofs is nestled in a shallow valley between low, misty hills under a serene moonlit night; pale moonlight is casting a cool silver wash over dewy grass and slate tiles while soft, warm light is spilling from leaded glass windows onto narrow cobblestone lanes. Thin wisps of mist are drifting down the hill slopes and curling through the streets as thin streams of smoke are rising from chimneys and curling up into the moonlit air; lanterns hanging from wrought-iron brackets are gently swinging and candle flames behind shutters are flickering, casting subtle shadows across textured stone walls and wooden doors. In the background, a narrow brook is murmuring over stones and a distant church bell is tolling once, while crickets are chirping steadily and an occasional owl is calling from the dark hillside; a low breeze is rustling the leaves of a lone elm and causing reeds by the water to whisper. The overall palette is cool silvers and soft blues from the moon, contrasted with warm amber glows from windows and lanterns, with visible details like moss on stones, chipped plaster, and wet cobbles reflecting scattered light, creating a calm, intimate nighttime scene."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_38", "video_prompt": "A small village of stone cottages with a mix of thatched and slate roofs is nestled in a shallow valley between low, misty hills under a serene moonlit night; pale moonlight is casting a cool silver wash over dewy grass and slate tiles while soft, warm light is spilling from leaded glass windows onto narrow cobblestone lanes. Thin wisps of mist are drifting down the hill slopes and curling through the streets as thin streams of smoke are rising from chimneys and curling up into the moonlit air; lanterns hanging from wrought-iron brackets are gently swinging and candle flames behind shutters are flickering, casting subtle shadows across textured stone walls and wooden doors. In the background, a narrow brook is murmuring over stones and a distant church bell is tolling once, while crickets are chirping steadily and an occasional owl is calling from the dark hillside; a low breeze is rustling the leaves of a lone elm and causing reeds by the water to whisper. The overall palette is cool silvers and soft blues from the moon, contrasted with warm amber glows from windows and lanterns, with visible details like moss on stones, chipped plaster, and wet cobbles reflecting scattered light, creating a calm, intimate nighttime scene."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_51", "video_prompt": "A static medium shot of a woman in her late 20s with shoulder-length dark hair, wearing a plain green shirt and a flowery midi skirt, standing against a bright white background; she is holding a white ceramic pot at chest level that contains a large plant with oversized round leaves in alternating orange and green, the leaves showing a smooth, slightly waxy texture and gentle midrib veins. Soft studio lights create even, shadow-free illumination and crisp color separation; she is lifting the pot slightly and tilting it toward the camera as the leaves shift a little from the motion. Ambient audio is a quiet studio room tone with a soft, unobtrusive acoustic guitar loop underlining the moment; as she moves there is a faint rustle of fabric and the light sound of her hands adjusting on the pot. Woman (warm, medium pace, mid pitch): \"Look at these leaves-aren't they lovely?\" she smiles and holds the tilt, then pauses to glance down and smooth a leaf with her fingertip. Woman (brightening, slightly quicker): \"The orange really pops against the green,\" she says while rotating the pot a quarter turn so the leaf edges catch the light; a subtle short reverb on her voice and a small, gentle exhale sync with her final nod as she offers the plant to the viewer."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_52", "video_prompt": "Goku in his Super Saiyan 5 form stands on a cracked rocky plain under soft overcast light; a muscular adult male with long white-silver spiky hair falling past his shoulders, teal-green eyes, and faint red fur along his forearms and shoulders, wearing a torn orange gi with a blue undershirt and blue wristbands. A static medium full shot frames him chest-up as he is powering up, fists clenched at his sides, shoulders rising and falling with heavy breaths, hair lifting and the white-silver aura with subtle purple edges pulsing and crackling outward while small rocks and dust swirl and lift from the ground. Ambient audio begins with a low earth rumble and distant wind whoosh; as the aura intensifies a rising electrical crackle and bright synth sweep build in pitch, small stones clatter and a thin metallic ringing emerges. He inhales sharply, then exhales and speaks aloud: Goku (raw, strained, rising pitch): \"Haa...!\"-he tightens his grip and the aura spikes; immediately he releases a forceful shout that syncs to a sharp air snap and percussion hit: Goku (forceful, loud, high pitch): \"Kaaah!\"-the shout launches a brief sonic burst that bends nearby dust and leaves a brief shimmer in the air. After the shout the electrical crackle falls into a sustained metallic hum and a distant thunder roll as the energy settles, leaving faint floating embers and a quiet wind rustle while the static camera holds the charged pose for the final beat."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60001", "video_prompt": "In a tight close-up, a tall clear glass filled with crushed ice, a pineapple wedge on the rim and a small paper umbrella is sitting on a weathered wooden bar top under soft late-afternoon light; a clear glass carafe is tilting above the rim and is pouring bright orange juice in a steady stream that is cascading into the glass, splashing against the ice and creating a swirling motion that lifts tiny bubbles toward the surface as the liquid level rises. As the carafe is pulled back, condensation beads are forming and slowly sliding down the glass while the umbrella trembles slightly and the pineapple wedge leans inward. The audio starts with the mid-range SFX of a liquid pour and crisp clinks of ice, layered under a low-volume tropical soundscape of distant ocean waves and soft steel-drum chords; when the juice hits the ice there is a short hollow ring of the glass and a faint fizz of bubbles, then a delicate tap as the carafe is set down and the ambient waves continue quietly in the background."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60024", "video_prompt": "In a low, first-person view hovering just above the grainy ocean floor, soft blue-green shafts of light are filtering down through rippling surface patterns, revealing ridged sand, scattered broken shells, and jagged coral; fine silt is drifting upward and tiny bioluminescent plankton are pulsing like faint motes. A thin trail of disturbed sand is marking where something has been crawling, and a small crab is scuttling left to right across the frame, its legs kicking up micro-puffs of sand while a pale starfish clings to a nearby rock. Low, muffled water thrum fills the background, distant whale calls are resonant and slow, and measured regulator breaths are audible in steady intervals with occasional single bubbles popping as they rise. A companion voice, low and amused over faint radio static, says, \"You've been crawling around the ocean floor all day,\" timed with a slight ripple of light from above; after a brief pause and a tiny sift of sand, your voice, breathy and tired, replies, \"Yeah... can't tell if it's the cold or the tide,\" followed by a soft exhale and a larger bubble that drifts up through the plankton. Nearby, the crab's shell clicks softly against shell fragments and a faint metallic beep from a dive instrument punctuates the soundscape as the plankton pulse continues to drift."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60025", "video_prompt": "Style: anime. A beautiful girl in a flowing white dress is standing in a sunlit forest glade, her long dark hair gently swaying as a soft breeze lifts the sheer skirt and lace hem; cel-shaded anime rendering with clean linework, soft pastel greens and warm brown trunks, and subtle rim lighting that makes the foliage and scattered wildflowers look slightly magical - the background feels fabulous with drifting pollen motes, pale blue bokeh orbs, and faint shafts of dappled sunlight filtering through leaves. She is standing center-frame with hands loosely clasped at her waist, large expressive eyes gazing upward and a small, wistful smile on her face, while nearby ferns glisten with tiny dewdrops. Ambient audio is layered: light leaves rustling, distant multi-voice birdsong that resolves into a single clear chirp, a faint stream trickle and a gentle wind whoosh; a delicate piano arpeggio with soft bell tones is playing quietly to lift the mood. She inhales audibly (soft breath SFX) and then speaks in a soft, reflective, slow voice: \"It's so quiet here...\" as she tilts her head and closes her eyes; after a brief pause she murmurs in a light, hopeful, slightly higher voice, \"I never thought I'd find this place,\" timed with a small smile and the dress whispering on the next breeze (fabric rustle SFX). A bright bird chirp answers in the soundscape just after her second line, and she gives a soft, airy laugh (gentle laugh SFX) as the piano bell lingers and the forest ambience continues."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60080", "video_prompt": "A medium shot of a young female brown bear named Joy standing in a small sun-dappled forest clearing, her dense brown fur catching soft afternoon light and a simple red scarf tied around her neck; she is stepping forward with gentle, deliberate paws on a leaf-strewn floor (soft padding on dry leaves), head tilted and bright eyes focused as she listens, then she leans slightly forward and extends one paw in a friendly, open gesture (scarf rustle). Background ambience is warm forest sound-distant birdsong, a faint brook burble, and a light breeze moving leaves-synchronized so the footsteps occur as she enters and the paw stretch aligns with a quiet rustle. Joy speaks in a warm, low, friendly voice, clear and patient: \"Hi, I'm Joy. I can understand you, and I'm here to help,\" followed by a soft, amused chuckle; her mouth moves in time with the words, then she nods once and offers a small reassuring smile while lowering her paw as the forest ambience continues under a final gentle exhale. "} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60113", "video_prompt": "A wide view over rolling green Welsh hills is bathed in low soft sunlight from the west, the sun is shining through a thin veil of high clouds and skimming the rounded ridges; short grass and scattered heather are bending and rippling under a steady wind that is sweeping across the slopes, and small puffs of dust are lifting from thin paths as the breeze moves; a low grey drystone wall with lichen-streaked stones is tracing the contour of a slope, its rough texture catching side light while patches of gorse with small yellow flowers are trembling and shedding a few dry petals; thin white clouds are drifting across the pale blue sky and momentarily sliding cool shadows down the hillsides as the sun reappears; audio: a constant low whoosh of wind is filling the scene as the grasses bend, layered with the near rustle of stems and the soft creak and tumble of a loose stone shifting in the wall, and a clear distant skylark is trilling a sustained phrase overhead, its high note rising and fading while the wind continues; the frame is steady, showing only natural small motions-grass leaning, flowers quivering, light moving across the slopes-conveying a calm, windy sunlit moment on the Welsh hills."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60133", "video_prompt": "In a static medium-wide shot from slightly behind and to the boy's right, a boy of about 12 with short dark hair and a navy hoodie is sitting cross-legged on a wool blanket on a low grassy knoll, watching the night sky as the Milky Way is arching overhead as a pale white band with a faint purple haze that is slowly drifting westward; soft starlight and a thin crescent moon are casting a cool, diffuse glow across his face and the textured knit of his hoodie. He is tilting his head back and tracking the shifting band with steady eyes, fingers tapping once on his knee, then shifting his weight so the blanket rustles. Ambient night sounds fill the scene: a gentle wind whispering through tall grass, steady cricket chirps, a distant owl hoot, and the soft rustle of fabric; his breath is audible as a small inhale. Boy (soft, awed, low voice): \"It's... actually moving.\" He exhales, smiles slightly, then leans forward and points with one hand toward a brighter patch of stars. Boy (quiet, slow, wonder): \"Look at that-like a river in the sky.\" A subtle, warm ambient pad underlies the natural soundscape, supporting the moment without overpowering the night ambience as the Milky Way continues its slow motion and the shot holds the quiet tableau."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60194", "video_prompt": "A tight close-up on a young person in their early 20s with short dark hair and subtle freckles, eyes wide and open to a cool wind that is ruffling short hair and moving a loose strand across the cheek; soft side light from a low sun casts gentle highlights on the skin and a faint rim light on the hair, while the out-of-focus background shows a pale grey sky and blurred treetops. Their gaze is slightly upward, pupils slowly widening as air moves across the face, eyelashes trembling and then blinking; they inhale quietly, shoulders shifting in a small, visible breath, and exhale as the wind lifts finer hairs. They speak twice in a close, intimate delivery synchronized with the breaths: They (soft, breathy, low-pitched) says, \"It's okay...\" while looking upward and letting a slow blink hold, then after a brief pause They (quieter, steady, low-pitched) says, \"I'm here,\" as the lips part and the wind pushes the lashes. Audio layers: foreground wind whooshes that vary with the hair movement, small crisp rustles of distant leaves, a subtle distant urban hum under the wind, close-mic'd inhalation and exhalation, and a single sustaining cello tone that rises gently under the second line and fades with the wind; all sound is timed to the visible breaths, hair movement, and spoken lines. Textures and details are visible in the five-second moment: moist eyes reflecting the sky, fine skin pores, soft sheen on the lips, and individual hair fibers moving against the cheek."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60198", "video_prompt": "Rendered in ultra-detailed 8k, a LEGO Super Mario minifigure (red hat with an M, blue overalls, white gloves, brown mustache, printed cheerful face) is standing on a glossy green baseplate with visible studs; soft overhead studio light and a cool rim light are creating small specular highlights on the molded plastic. He is raising his right arm and pressing a small red action button on the baseplate; precise plastic clicks and a short electronic beep occur as he moves. As he presses, a circular portal is opening behind him - a translucent ring of shifting teal and purple energy with swirling particle motes and a subtle grid-like shimmer that casts colored reflections across Mario's face; a deep harmonic drone is rising and a whoosh of wind-like synths layers with a brief chiptune arpeggio. The single camera is zooming in slowly from a medium shot toward an extreme close-up on Mario's face and the rim of the portal, tightening on the reflection of the swirling colors in his printed eyes while a quiet mechanical whirr from the zoom accompanies the sound. Mario (bright, energetic, mid-pitched, quick) says as he presses the button and leans forward, \"Let's-a go!\" - his voice is clear and playful and matches the moment the first light blooms. The portal answers with a low, resonant, echoing whisper (slow, hollow), \"Come...\" timed as tendrils of energy unfurl, and Mario blinks and inhales sharply, then exclaims (surprised, higher, short), \"Whoa!\" as the camera tightens on his expression; throughout, small plastic clacks track his movements, the portal rings with sporadic crystalline chimes, and the underlying electronic drone settles into a lower, distant hum that suggests another world beyond."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60209", "video_prompt": "In a quiet medium shot, a woman in her late 20s with chestnut hair tied in a loose bun is sitting alone on a flat rock at the edge of a calm, glassy lake, wearing a light-gray sweater, dark jeans, and scuffed brown boots; soft late-afternoon light is falling across her face and the water while the surrounding trees are full green and gently rustling. She is sitting cross-legged and leaning slightly forward, shoulders relaxed, eyes fixed on her reflection, then reaches a hand down and is lightly tracing the water surface with her fingertips, sending small concentric ripples as she breathes slowly and lets out a soft sigh. Ambient audio layers are detailed and timed to motion: steady, gentle lapping of water against stone, leaves rustling in a light breeze, distant single bird calls and a low insect hum, all underscored by a sparse piano motif-slow, single notes at low volume-that swells softly as she moves her hand; when her fingertip touches the water a delicate splash and the faint rustle of her sweater are audible. In a low, steady voice she says, \"I don't know who I'll be next,\" pausing to look at the ripples and glance up toward the treeline (a short, thoughtful silence follows), then in a softer, accepting tone she adds, \"But maybe that's okay,\" as she tilts her head, exhales audibly, and allows a small, contemplative smile to form while her gaze returns to the lake."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60225", "video_prompt": "In a medium close-up, a skeleton is seated on a low white cloud in a bright heaven, wearing a simple white linen sash over one shoulder; its bone surfaces are smooth with faint cracks and the jaw is articulated so the expression reads relaxed. Soft golden backlight and diffuse white cloud light wash the scene while distant pearly spires and floating islands sit out of focus behind. The skeleton is lifting a polished silver fork in its right hand and a carving knife in its left, slicing a medium-rare steak on a porcelain plate balanced on its lap; the steak shows a seared brown crust and a warm pink center that gives a quiet sizzle as the knife passes. As it brings a forkful to its jaw, a dry, gentle clack of bone is audible, followed by a muted, contented chewing sound; it speaks in a low, amused voice, slow and warm, \"Well, this is unexpected,\" then pauses, glancing down at the plate and answers itself in a softer, wry tone, deliberate and mid-pitch, \"Heaven could use better menus,\" while a thin harp arpeggio and a distant choral pad swell beneath, soft wind through clouds and faint bell chimes punctuating the air; knife-on-plate clink, fork lift, and the final swallow are audible as the skeleton settles back slightly, a small puff of cloud compressing under its weight."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60254", "video_prompt": "A static medium close-up of a cottage window at night, pale full moonlight is washing a cool rectangle across the weathered wooden sill and slightly wavy glass; the right-hand casement is slowly swinging outward on aged iron hinges, the chipped white paint and rough grain are visible in the moon glow. As the pane is swinging open, a low creak from the hinges is audible, then a soft scrape as the old latch releases; a thin linen curtain is fluttering inward and its edge is brushing the sill with a quiet rustle. Outside, steady cricket chirps are underscoring the moment while a distant owl hoots once and a light breeze is whispering through nearby leaves so branches are sighing faintly. The widening opening is letting more moonlight spill into the dark interior, highlighting dust motes drifting in slow arcs and casting a pale band that is sliding across the floor."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60265", "video_prompt": "In an extreme close-up, a shallow pile of whole almonds and hazelnuts rests on a matte dark slate surface under soft overhead light that picks out subtle ridges on the almond skins and the rounded texture of hazelnut shells; a thick stream of melted dark chocolate is pouring in from above and is splashing down onto the pile, coating some nuts and sending tiny droplets outward. As the chocolate is striking the nuts, a few almonds and hazelnuts are nudged and spin slightly while a small piece of hazelnut shell is flicked aside; droplets arc and hang briefly in slow motion, catching highlights on their glossy surfaces, then fall and merge into a spreading pool around the nuts. Visual emphasis is on the contrast between the glossy chocolate and the matte nuts, with one hazelnut showing a cracked interior as chocolate runs over it. Audio begins with the deep, viscous pour of chocolate-a low glug and a soft, sticky slap on contact-immediately joined by crisp, dry clacks as nuts bump one another and a faint high-frequency tinkle as tiny droplets hit the slate; a muted kitchen ambience (distant HVAC hum and soft background murmur) sits underneath, then the sound settles into a gentle wet spread and quiet drips as chocolate spreads around the almonds and hazelnuts."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60278", "video_prompt": "Dong Yuhui is standing in a vast open plain under a pale sky, soft late-afternoon light falling across the scene; he is a man in his early 30s with short black hair, the breeze is blowing through his hair and ruffling the edges of a light gray jacket over a white shirt, and he is holding an open hardcover book in his left hand with the pages slightly fluttering. His face is turned toward the horizon, eyes firm and full of hope, the corners of his mouth slightly raised in a positive, energetic smile; his posture is upright, chest lifted and shoulders back, his whole body language is full of vitality and vigor, conveying clear confidence and optimism. While the wind makes a low whoosh through the grasses and the book emits a soft paper rustle, he breathes in, glances down at the open page, then looks up and speaks in a steady, warm, confident voice, \"We can do this,\" the words timed with a small, assured nod. Underneath, a gentle single-note piano chord swells as distant bird calls punctuate the air, the wind continues to whisper, and the final sound is the quiet flutter of pages as his smile holds, leaving a calm, positive, upward feeling."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60291", "video_prompt": "A medium close-up of a man in his late 50s with short graying hair and a neat trimmed beard, seated behind a low glass console in a futuristic presidential chamber that blends dark wood, brushed steel and frosted glass; he wears a tailored dark gray suit with a high-collar shirt and a small stylized tricolor lapel pin, his expression calm and purposeful as translucent holographic maps and data widgets float a foot above the console. He reaches out and taps a hovering map, which ripples into sharper focus while a thin blue route highlights across Eurasia; his fingers move with deliberate, practiced gestures and his eyes track the route as if reading several layers of data at once. Ambient sound is a low, steady hum of climate systems and distant city traffic through thick glass, punctuated by soft glassy chimes on each tap and a brief electronic confirmation beep when he activates a layer; a sparse, subdued string motif plays quietly under the scene, rising slightly when the map refocuses. President (voice: measured, low, paced): \"Activate strategic overlay, scale to national,\" he says, then pauses, glances briefly to the side as if checking a monitor, fingers hovering above the interface. President (voice: softer, deliberate): \"Keep civilian channels open - alert level stable,\" he adds, and presses his palm once, sending a succinct confirming tone as the hologram contracts; his face tightens for a beat, then relaxes as the room returns to the steady ambient hum."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60317", "video_prompt": "A medium close-up static shot of an androgynous angelic being is hovering a few feet above a smooth pale floor, wearing a flowing white robe with thin silver embroidery and long silver-white hair falling over the shoulders, face composed and gently focused. Soft cool backlight and a warm rim light are outlining the figure as large luminescent wings are slowly unfurling behind them, each feather translucent with an inner soft glow that pulses in pale gold and cool blue while tiny motes of light are drifting off the wing tips and catching on the silk texture of the robe. As they are extending both hands forward, the aura around them is shimmering in slow, concentric waves and their robe is fluttering slightly from a light upward lift; they are tilting their head and allowing a quiet, serene smile to form. A sustained, gentle choral pad is filling the air underneath the scene, with a single bell-like glissando punctuating the moment the wings open; soft rustle of fabric and whisper of feathers are synchronized to the wing motion, a faint whoosh of displaced air accompanies the hover, and subtle harmonic overtones are rising and falling with the aura's pulse. (voice: calm, warm, mezzo) \"I am here,\" they say, voice steady and low as their palms open, the words timed to the wings' full spread; a brief pause lets the motes scatter. (voice: soft, distant, reverent) Then they adds, voice trailing as they incline their head and close their eyes briefly, \"Stay near the light,\" while the glow around them is gently pulsing one last time."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_65", "video_prompt": "A young woman in her early 20s is standing in a medium full shot on a narrow stone alley lit by paper lanterns, wearing a fitted knee-length red silk qipao with pale pink peony embroidery and smooth white stockings; soft warm lantern light grazes the silk while cool evening blue fills the alleyside shadows, the stone underfoot showing a faint sheen as if damp. She has long black hair gathered into a loose chignon with a simple silver hairpin, subtle makeup, and a calm, attentive expression as she shifts her weight onto one foot and smooths the fabric at her hip, then reaches out briefly to brush a lantern's fringe with two fingers while her skirt whispers against her thigh. Ambient alley sound settles beneath-low murmur of distant conversation, a far-off bicycle bell, a small trickle of water from a nearby drain-while a single-note erhu phrase plays softly and periodically, matching her gentle movements; each step she takes produces a soft footstep on wet stone and a quiet rustle of silk. She breathes out, then speaks in a soft, warm voice, \"The lanterns look quiet tonight,\" as she glances down and then up toward the lights, a small, thoughtful smile forming; fabric rustle and a light creak from the lantern sway punctuate the moment, and the erhu holds a final quiet phrase as she tilts her head and the scene lingers on her composed, serene posture."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_70", "video_prompt": "A medium close-up of Lucas, early 30s, clean-shaven with short dark hair slightly tousled, wearing a navy blazer over a white shirt, standing on a small stage in a dim conference hall under a soft spotlight with a faint technical schematic projected on a screen behind him; he is leaning slightly forward, eyes bright, smiling, right hand lifting in an open, inviting gesture while his left holds a handheld microphone. Lucas (eager, breathy, mid-tempo voice): \"This is it, my friends. Humanity's next step.\" As he finishes the line he steps a half pace forward and lifts both hands briefly, adding in a quick, confident tone, \"We're ready to begin,\" (confident, slightly faster) while a low audience murmur swells into a single short cheer; ambient room sound includes a soft HVAC hum, distant chair scrape, and a subtle microphone rustle when he moves the mic. The stage light casts soft shadows across his jaw and the projector's faint flicker glints on the nearest rows; his brows lift and a slight smile tightens at the corners of his mouth, the fabric of his jacket shifting as he breathes and the crowd reacts, all within a compact, energetic five-second moment."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_74", "video_prompt": "A medium wide shot of a low, pebbled shoreline in soft late-afternoon light, a boy about eight with short brown hair and a faint smudge on his cheek is walking barefoot toward the water wearing a damp navy windbreaker and tan shorts, taking small, careful steps across wet stones while his jacket sleeves brush his arms. Gentle lap of small waves provides the ambient sound, with distant gull calls and a light breeze whispering through nearby reeds; as he draws within two meters the gravel crunches under his feet, a soft fabric rustle from his jacket is audible. He slows, eyes on the moving water, inhales audibly, and in a quiet, hesitant voice says, \"Okay... here goes,\" then leans forward and steps so his toes skim the shallow edge, producing a soft splash and a brief spray; the water makes a delicate fizzing sound against his foot and he lets out a small relieved breath while the wave withdraws and pebbles settle back into place."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_76", "video_prompt": "A stationary overhead camera looks straight down through clear, shallow blue water as a large adult whale shark is gliding slowly from left to right across the frame, its broad head, pale gray-blue skin patterned with white spots and faint stripes, and a tall dorsal fin visible beneath a gently rippling surface; soft sunlight is casting moving caustic bands across its back and the sandy seafloor below, the shark's rough, ridged skin showing subtle barnacles and a few old scars. As it swims, the shark is opening its wide mouth slightly to filter-feed while its tail is undulating in smooth, powerful strokes, a dorsal fin brushing the surface and sending tiny concentric ripples; a small group of pilot fish is trailing close behind, then a nearby cluster of baitfish is scattering in a quick burst, darting away in sharp, synchronized motions. Ambient audio begins with a low, muffled ocean hum and a distant, slow whale-like call underlining the scene; as the shark passes a soft whoosh of displaced water and a deep, low rumble follow each tail flick, faint bubble pings are audible near its mouth, and the baitfish scattering produces brief, high-frequency splashes and quick metallic plinks. The overall mood is calm and observant, the overhead view holding steady as the whale shark continues gliding out of frame."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_86", "video_prompt": "A medium close-up of a young man in his mid-20s, based directly on image:sketch (1).jpeg as visual reference, standing slightly angled toward camera with his shoulders relaxed; his dark, slightly long hair is moving as a steady wind is sweeping across the scene, ruffling strands and lifting the collar of his loose grey shirt. A soft blue-white glow is surrounding him like a faint halo, the glow is pulsing gently and casting a cool rim light on the edges of his face and hair while the rest of the background remains a muted charcoal sketch texture; small ink-like particles are drifting through the air and catching the light as they float. He is breathing out slowly, exhaling as a gust of wind pushes his hair back, and his expression is calm, eyes narrowing slightly as if listening. Soundscape: a clear, close wind whoosh is present and is rising and falling with each gust, soft rustling of fabric and hair on each breath, and a low, steady electrical hum is synchronizing with the glow pulses; beneath that, a faint sketching scratch like pencil on paper is barely audible to tie to the reference image. Dialogue synced to actions: He (voice: low, steady, medium pace) says, \"I can feel it,\" as he exhales and lets his chin lift; an inner voice (voice: soft, breathy, slow) replies, \"It's starting,\" as the glow brightens for one pulse; he (voice: low, steady) answers again with a softer tone, \"Then stay with it,\" as his hair settles slightly and his shoulders shift; the inner voice (voice: airy, distant) whispers, \"I am,\" timed with the final, small gust that makes the particles drift away. The overall color palette is cool greys and blue-white light, textures are drawn-paper and soft fabric, and all motion is continuous and subtle to fit within a brief five-second moment."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_93", "video_prompt": "In a wide shot, a small round bunny with soft white fur and faint gray patches is sitting near the crest of a gently sloping hill blanketed in short green grass and scattered wildflowers-daisies, buttercups, and small bluebells-bathed in soft midday sunlight; the bunny twitches its nose and lifts slightly, then springs forward in a quick, joyful bound, ears tilting back and hind legs tucking under as it clears a patch of flowers, petals fluttering down and a few stems bending under its passage, then it lands lightly with forefeet touching first and hind feet following, sending a muted thump and a soft rustle through the grass. Ambient sound begins with a light breeze through the grass and distant birdsong, joined by a steady, close bee buzz around the blossoms; as the bunny pushes off there is a small puff of displaced air and the faint crunch of stems, and on landing the bunny lets out a short, bright chirp. Bunny (soft, high-pitched, quick): \"peep,\" synchronized with the landing and a brief head tilt; it then sniffs a daisy, nose wrinkling and whiskers brushing petals, ears flicking, while the breeze and bees continue softly in the background and a few petals settle back onto the hill."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60343", "video_prompt": "Wide shot of a small village under late afternoon light: clusters of low stone cottages with red tile roofs, a low church steeple, and a narrow cobbled lane between them; warm, soft light casts long, thin shadows and brings out the rough texture of stone and the matte clay tiles. Several groups of birds are flying across the pale sky, each flock is shifting shape as birds are soaring, banking, wheeling, splitting off, then rejoining-some individuals are gliding on outstretched wings while others are beating rapidly to gain altitude; their moving shadows skim across rooftops and the lane below as a group arcs together over the steeple. Soundscape: close, soft flapping of wings layered with overlapping bird calls-higher, quick chirps interspersed with occasional low caws-while a single distant church bell tolls slowly once, a light wind is rustling through poplar leaves, a loose wooden shutter creaks and a clay chimney pot clicks as the breeze passes, all synchronized so the wingbeats and calls rise as the flocks bank and briefly swell, then fall away as they split and move out of frame."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60350", "video_prompt": "In a medium close-up, a man in his late 20s with short dark hair and light stubble is cupping a 3-week-old black and white tuxedo kitten with blue eyes in both hands, wearing a light gray cotton shirt; soft window light is falling across their faces and a warm indoor tone is bathing the scene. He is holding the kitten gently, palms cradling its tiny body as he is stroking the soft downy fur with his thumbs; the kitten is calm and relaxed, blinking slowly, paws tucked against his palms and its tiny pink nose twitching. The man smiles and speaks in a low, warm voice, slow pace, \"Hey little one...\" (he leans in slightly, eyes soft, fabric rustle audible), then the kitten replies with a soft, high mew (quiet, brief) while blinking and beginning a steady, quiet purr that grows slightly louder as it settles against his hands. The man responds in a tender hush, gentle pace, \"You're so small, aren't you?\" as he exhales and brushes his thumb along its back; the kitten emits another faint mew and nuzzles his thumb, purring more audibly. Ambient audio layers are intimate and close: a muted distant street hum through the window, a faint clock tick, soft breathing from the man, light fabric rustle when he shifts, the kitten's small mews and continuous low purr present and clear, and a subtle room reverb to convey a small, quiet indoor space."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60360", "video_prompt": "Style: Tim Burton Style Characters. In a dim, low-lit room a lanky, slightly elongated Santa is standing beside a tall, narrow window, peering at a smartphone held in his gloved hand; he has a narrow pale face, wide dark eyes, a scraggly white beard, and a worn red coat with thin fur trim and long sleeves that brush his knuckles. Soft moonlight is filtering through the window, casting thin shadows across his coat while a small warm desk lamp on the sill gives a muted rim light; a cinematic lens with shallow depth of field is rendering the distant streetlights outside as soft bokeh and subtle film grain. The smartphone screen is glowing with a map full of clustered red dots and tiny car icons showing a traffic jam; he is zooming and tapping the screen, brow furrowing as the map shifts. Room audio is quiet: a faint, steady traffic rumble and occasional car horn from the street below, a soft creak as he shifts his weight, and a delicate notification chime from the phone. In a low, gravelly, slow voice he mutters, \"Forty-two minutes? That's too long,\" then he is tapping to try an alternate route as a neutral, clipped phone assistant voice says, \"Delay ahead: 42 minutes, suggested detour adds ten minutes,\" synchronized with the map redrawing under his thumb; he glances up through the window at the stalled tail lights, exhales, and in a short, resigned tone replies, \"All right, take the detour,\" then tucks the phone into his coat as the distant traffic hum persists and the lamp casts a soft, low shadow across his face."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60390", "video_prompt": "A wide static view of a coastal landscape on a sunny day: the sun is high in the pale sky, casting soft overhead light and subtle shadows across smooth wet sand and scattered pale pebbles while small waves are lapping at the shore and thin white foam is retreating with a gentle hiss as sunlight glints on the ripple tops. As a light breeze is bending the dune grass and a few palm fronds, several seagulls are circling low and calling with short sharp cries, and a distant sailboat is drifting near the horizon with its canvas lightly billowing. The soundscape matches the motion-soft rhythmic surf washing onto sand, the thin hiss of foam receding, wind whispering through the grass, intermittent gull calls, and an occasional soft metallic clink from the sailboat rigging, all underscored by a faint, slow acoustic guitar picking a quiet two-bar motif to add calm warmth to the scene."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60404", "video_prompt": "A medium close-up shows Eveline, a young sorceress in her early 20s with waist-length dark brown hair braided loosely over one shoulder, wearing a moss-green cloak over a simple linen dress and scuffed leather boots, kneeling on soft emerald moss in a serene sun-dappled forest; soft overhead light filters through tall oaks, casting moving leaf shadows and the scene is textured with damp bark, tiny wildflowers, and dewy spiderwebs. Ambient forest sounds-distant birdsong, a gentle breeze rustling leaves, a nearby creek's quiet trickle-form a calm background; her soft footsteps on moss and the quiet inhale of her breath punctuate the space as she is experimenting with magic, tracing a small sigil in the air with a trembling finger while murmuring an incantation, \"Focus... steady,\" (soft, hesitant, low) and tiny golden motes are gathering at her fingertip with a faint bell-like chime and gentle electrical crackle. She is tilting her head, squinting in concentration, then is pushing her palm forward and a warm pale orb of light is lifting from her hand, spinning slowly as leaves and dust begin orbiting it; the orb is emitting a subtle harmonic drone and delicate tinkling SFX as it is stabilizing. Eveline is exhaling sharply, \"It's... it's working,\" (breathless, surprised, slightly high) and she is leaning closer, eyes widening and mouth parting in visible awe while small vines at her feet are curling toward the light. She is straightening with a small, stunned smile, whispering, \"I can do this,\" (quiet, resolute, calm) as the forest ambience swells-a soft wind gust, a brief chorus of birds-and the orb is pulsing once with a warm luminous beat that is bathing her face in soft gold, capturing the brief, clear moment of realization and possibility."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60416", "video_prompt": "Style: cartoon. In a static medium-wide shot, a cartoon girl of about 10 sits on a smooth gray stone ledge at the base of a narrow waterfall, legs dangling and hands resting on the rock; she wears a teal hoodie and denim shorts, brown shoulder-length hair with a small side braid, and a soft, contented expression as she looks out at the view of a tree-lined valley beyond. Soft midday light filters through green leaves, the waterfall is drawn with clear blue-green water and white froth, and a fine mist makes tiny droplets bead on her knees; a light breeze moves her braid and the hem of her hoodie. The water is continuously audible as a steady, low roar; higher, crisp splashes hit the stones and a few bright bird calls and a faint rustle of leaves sit beneath the falls. She shifts her weight, tucks her braid behind her ear, blinks, and exhales slowly as the mist lands on her face (subtle wet SFX). Girl (soft, reflective, slow): \"It's so peaceful here...\" (her voice is intimate, slightly breathy; waterfall audio ducks gently while she speaks). She smiles, presses her palms to the stone, and says with a small laugh, Girl (light, quick): \"Maybe I should come back with a sketchbook.\" After her last word the waterfall volume returns to full, droplets patter, and the scene holds with distant birds and steady water ambience."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60454", "video_prompt": "In a static medium-wide shot, a woman in her late 20s with wind-tousled dark hair is sitting on a smooth driftwood log at the sandy shore, wearing a light gray knit sweater and worn jeans, facing the sea as soft late-afternoon light washes the scene and low sun glances off the wet sand. Small waves are lapping and pulling at the shoreline, seafoam is pulsing around scattered pebbles, and a cool breeze is ruffling her hair and sweater; distant gull calls punctuate the air and a low foghorn is sounding far off. She is watching the horizon, fingers lightly tapping the log, then draws a slow breath and says in a quiet, steady voice, \"Calmer today, huh?\" She glances down at a phone in her lap; an off-screen voice, warm and slightly amused, replies quickly, \"Yeah - you found the quiet spot.\" She lets out a small smile, tucks hair behind her ear, answers softly, \"I needed this,\" and turns her gaze back to the sea while the rhythmic hiss of waves and gentle wind continue under the brief exchange, with occasional seabird calls and a faint distant dog bark adding texture to the seaside ambience."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60489", "video_prompt": "In a medium close-up, Alexander Lukashenko, a white man in his early 70s with short gray hair and a clean-shaven face, wearing a dark navy suit and a striped tie, sits at a low podium against an out-of-focus backdrop showing a red-and-green flag; soft overhead light and a subtle rim light outline his shoulders as the scene focuses on his face. He is smiling, then leans forward slightly, then tilts his head back and throws it back as he bursts into a short, throaty laugh-his shoulders shaking and eyes crinkling-while one hand comes up briefly to his mouth and then drops to the podium. The audio layer presents a close, gravelly laugh in the foreground, layered with light applause and murmured reactions from an off-screen audience, the faint rustle of papers, and a soft microphone rustle; as the laugh begins he utters one dry, measured line in a low, even tone, \"Well, that's unexpected,\" (low, dry, slow), then follows with a quick, breathy chuckle (deep, warm) that overlaps the room noise. The camera remains steady in the medium close-up, capturing the sequence of smile, voiced remark, and sudden laugh while ambient sounds - soft clapping, hushed voices, a single camera shutter click - sit behind the laugh and then settle as the laughter fades."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60498", "video_prompt": "A static close-up frames a Spider-Man action figure standing on a worn wooden shelf, painted red and blue suit with glossy plastic highlights and visible articulated joints; soft overhead light casts mild reflections across the molded webbing and a shallow depth of field blurs the background books. He is slowly tilting his head to the left as his white eye lenses curve upward into a clear smiling expression, then he raises his right hand in a short, friendly wave while a crisp plastic joint click and a faint squeak register with each motion. Room ambience carries a distant city hum and soft indoor air movement; as his lenses tighten into the smile a brief, gentle synth twinkle accents the moment. Spider-Man (playful, slightly high-pitched, quick) says, \"Hey-ready?\" as he lifts his hand, the voice timed to the click of the wrist joint; he pauses, leans forward a hair, and the painted smile seems to deepen with a tiny plastic rasp. Spider-Man (warm, amused, medium pace) follows, \"You always are, right?\" while he tilts his head back and his shoulder rotates with a soft mechanical click. Spider-Man (confident, brisk, low) finishes, \"Okay-on three,\" as he nods once and the light catches a small scuff on the painted chest; a short, bright chime punctuates the final grin and the ambient city hum continues under the closing moment."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60508", "video_prompt": "A matte black Porsche 911 is cruising along a narrow, leaf-strewn forest road in the evening, its low headlights cutting through dim blue-gray dusk while long cool shadows from pine and birch stretch across the pavement; soft, warm red taillights smear over wet leaves as the car moves, and small sprays of leaves lift and scatter behind the rear tires. The engine is emitting a low, measured purr that rises briefly as the driver downshifts while negotiating a gentle curve, tires softly crunching on damp leaves and loose gravel beneath the chassis; occasional low branches brush the hood and send a faint metallic whisper along the bodywork. Ambient sound is a quiet forest layer-wind rustling needles and leaves in the canopy, a distant bird call, and the echo of the engine bouncing between tree trunks-then the subtle click of the turn signal and the whisper of airflow as the Porsche arcs past, remaining alone in the calm evening woods."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60528", "video_prompt": "A quaint village nestled among rolling hills and surrounded by peaceful trees is bathed in a sky of orange and pink as the sun begins to set, soft late-afternoon light warming stone cottages and small gardens while gentle rustling of leaves and the sweet melodies of birds fill the air. After two seconds, the screen slowly zooms in, then over the next three seconds the camera slowly zooms in and pans, offering a picturesque view of the clustered homes before gracefully pointing toward a nearby pond that is shimmering in the warm sunlight; the pond's surface mirrors the colored sky and surrounding trees with glinting highlights moving across the water. As the focus narrows on the pond, three vibrant orange fishes are swimming in graceful arcs just below the surface, their scales glistening as sunlight catches them while they glide in small, coordinated loops and send soft ripples outward. Audio is layered to match the motion: continuous leaf rustle and clear bird song in the foreground, then gentle water lapping and faint, synchronized splashes as each fish breaks the surface of a ripple, keeping the overall soundscape calm and harmonious with the village remaining quiet in the background."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60532", "video_prompt": "A medium close-up of a young woman in her late 20s with short black hair tucked behind her ear, wearing a light gray blouse, sitting at a small wooden table at a street-side food stall as soft late-afternoon light falls across a glossy ceramic bowl of mie ayam-yellow noodles piled with shredded braised chicken, chopped scallions and a few green vegetables, dark sauce pooling at the bottom; she is lifting a tangle of noodles with wooden chopsticks, twirling them once, then slurping them into her mouth while her free hand is scooping broth and chicken with a metal spoon, her eyes closing briefly and a small smile forming. Ambient street audio is present-distant traffic hum, a motorbike idling, low vendor chatter and occasional clink of dishes-while close SFX highlight wooden chopsticks tapping porcelain, a wet noodle slurp, the soft clink of spoon on bowl and a quiet breath; a gentle instrumental with soft percussive tones plays under the ambient mix. As she sets the chopsticks down she says in a warm, measured voice, \"Looks comforting,\" pauses and glances around, then in a soft, content voice adds, \"Just what I needed,\" and reaches for another bite."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60538", "video_prompt": "A static medium-wide shot frames a low, moss-covered rock ledge and the cool pool below, soft afternoon light filtering through a leafy canopy and casting dappled patches across dark green water; the rock is slick with thin rivulets and small, pale leaves cluster at the edge. A small, smooth pebble is teetering on the lip, then tumbles and plunges into the cool pool below, breaking the mirror surface and sending concentric ripples outward while tiny droplets arc upward and scatter. Under the surface a brief ring of bubbles is rising and dissolving, and as the ripple reaches floating leaves they bob and shift. Audio: an immediate, sharp splash on impact, followed by spreading, gentle slaps of water against stone, a soft underwater gurgle as bubbles release, faint steady trickle from the rock face, distant birds calling and a low insect hum; after the initial splash the water calms and small droplets patter on leaves while the echo of the impact fades."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60546", "video_prompt": "A happy child of about eight years old with short curly hair and subtle freckles is sitting cross-legged on a small red picnic blanket in a sunlit backyard, wearing a striped t-shirt and denim shorts, holding a small soprano ukulele with a light wood grain and glossy finish; warm late-afternoon light is casting soft shadows and a few leaves are drifting in a light breeze. The child is smiling and is strumming a bright, bouncy chord progression with the right hand while the left hand is forming a simple C-to-G chord change, the ukulele strings ringing with clear plucked tones; the child is gently swaying and bobbing their head as they sing one short, cheerful line in a light, singing voice, Kid (singing, bright, slightly high): \"Sunshine on my day, play along, hey!\"-they finish the phrase with a quick, playful flourish across the strings, then laugh and look up, Kid (delighted, breathy): \"Again!\" as they tap the uke body with their thumb and launch into another upbeat strum. Audio includes close, intimate ukulele sound with crisp string attack and soft resonance, the child's voice layered slightly forward, faint backyard ambience-distant birdsong, a soft breeze through leaves, a muted lawnmower far off-and the small SFX of fingers on wood and a quick giggle; everything is synchronized so the vocal lines match the visible strums and the final tap coincides with the child's delighted exclamation."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60571", "video_prompt": "In a wide, low-angle shot from the highway shoulder, three super-fast sports cars are racing side-by-side along a wet night highway, a red low-slung coupe with a white racing stripe on the left is surging forward, a matte black aggressive supercar in the center is pulling slightly ahead with sharp LED accents cutting through light mist, and a metallic blue targa on the right is darting forward; glossy body panels catch and reflect neon from the big city glowing in the distance, soft sodium streetlamps pool on the slick asphalt, and heat shimmer ripples above each exhaust. As they accelerate, engine roars dominate the mix- the red coupe emits a high, razor-edge rev, the black car answers with a deep, throaty growl, and the blue car adds a fast turbo whistle-interleaved with rapid upshifts and short throttle blips; tires hiss and briefly squeal during tight lane corrections, small pebbles crack against wheel wells, and a sharp whoosh of wind whistles past side mirrors. Ahead, the distant city skyline of clustered towers and billboard neon provides a low, constant urban hum with faint car horns and a muted subway rumble beneath, while the Doppler shift bends each engine pitch as the cars edge forward and then tuck in, motion blur streaking headlights and building lights to emphasize raw speed for the five-second burst."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60572", "video_prompt": "Style: hyper realism 4k. A static tight close-up of a single candle sitting in an aged brass candlestick on a dark wooden surface, the creamy white wax is melting into a glossy pool that is slowly running down the stem while the blackened wick is holding a steady amber flame; 4k detail reveals the fine char texture of the wick, tiny beads of molten wax, and subtle fingerprints and patina on the brass. Soft warm light from the flame is casting gentle reflections on the candlestick and subtle, narrow shadows on the wood as a faint draft moves through the frame, making the flame flicker and lean; the wick crackles quietly and a thin metallic spark flicks at the tip as the flame thins, then a short airy whoosh is heard exactly as the flame collapses and goes out. A slender plume of blue-gray smoke is rising in a slow spiral from the still-glowing ember, the glowing tip dimming to a dull black nub while a few droplets of settling wax produce soft, muted clicks; ambient room hush underlies the moment with a low, unobtrusive room tone and a distant faint creak, and the residual sound of the extinguish - a soft breath-like hiss and the last faint pop - lingers as the scene holds on the quiet candlestick and the smoke disperses."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60589", "video_prompt": "A tight close-up of a brown-gray tabby cat with distinct black stripes and a faint white chin, lying on a soft cream knit blanket under soft side light from a nearby window; the cat blinks slowly, then leans in and presses its cheek and forehead into the blanket in a series of gentle, deliberate nuzzles while its whiskers brush the fabric and its ears flick slightly. Its green eyes remain half-closed and its shoulders shift forward as it rubs, then tilts its head and repeats another short, purposeful rub; the fur has a soft, slightly glossy texture and small tufts move with each motion. Ambient indoor room noise is quiet-a low, steady HVAC hum and distant muted street sound-then a low, steady purr begins as the cat first nuzzles, rising into a warm, continuous rumble synchronized with the rubbing. Each contact is accompanied by a soft fabric rustle and a faint breathy exhale; after the final nuzzle the cat releases and emits a brief, contented trill before settling, the purr continuing gently under the room ambience."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60601", "video_prompt": "In a medium close-up, a couple in their late 20s is sitting across a small round wooden table in a cozy cafe, the woman with shoulder-length dark hair and a cream sweater holding a white ceramic cup, the man with short brown hair, light stubble, and a navy button-down resting his forearms on the table; soft warm yellow pendant lights overhead are reflecting gently on their faces and creating small highlights on the cup and the glass vase between them, while the background is softly blurred with other patrons and warm wood tones. Low indie acoustic music is playing quietly from the cafe speakers as steady ambient murmur, occasional silverware clinks, and a distant espresso machine hiss form a continuous soundbed. She is smiling and laughing, tucks a strand of hair behind her ear, and says in a warm, laugh-tinged voice, \"I'm really glad we came out tonight,\" with a short pause and a quick smile toward him; a soft clink occurs as he sets his cup down. He is smiling back, leaning in slightly, and replies in a soft, even voice, \"Me too-it's been nice,\" pausing to meet her eyes while a chair scrapes gently in the background. She is glancing down at her cup, then looking up with a brighter smile and says in a quiet, upbeat voice, \"Let's do this again,\" as the music swells a fraction and the cafe ambience continues beneath their voices, leaving them both smiling and holding eye contact."} +{"id": "vidprom_semantic_unique_gpt-5-mini-2025-08-07_60632", "video_prompt": "A wide view of a lush forest of mixed hardwoods and fruit-bearing trees rising beside a calm lake, soft late-afternoon light filtering through green leaves and dappling mossy trunks. Low branches are heavy with red apples and yellow pears, their glossy skins catching the light as a gentle breeze is moving through the canopy-leaves are rustling and small clusters of fruit are swaying while a few water droplets bead on leaf edges. At the lake edge, reeds are bending slightly and the grey-blue surface is forming narrow ripples where the wind touches it; a dragonfly is skimming the water and a small fish is breaking the surface with a soft plop, sending a brief circular ripple outward. Ambient audio is layered and synced with action: the wind is creating a steady soft rustle through leaves, clear birdsong (quick sparrow-like chirps and a distant thrush) is punctuating the treetops, a low insect buzz is underscoring the scene, and gentle water lapping is meeting the pebbled shore; when a fruit brushes a branch a muted thud and the faint crunch of leaf litter register. The scene feels tranquil but active, natural motions continuing throughout the short clip."} diff --git a/apps/dreamverse/web/public/favicon.ico b/apps/dreamverse/web/public/favicon.ico new file mode 100644 index 0000000000..0b8b058b51 --- /dev/null +++ b/apps/dreamverse/web/public/favicon.ico @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/dreamverse/web/public/full.svg b/apps/dreamverse/web/public/full.svg new file mode 100644 index 0000000000..0b8b058b51 --- /dev/null +++ b/apps/dreamverse/web/public/full.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/dreamverse/web/public/icon-simple.svg b/apps/dreamverse/web/public/icon-simple.svg new file mode 100644 index 0000000000..4146438996 --- /dev/null +++ b/apps/dreamverse/web/public/icon-simple.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/dreamverse/web/public/k2.png b/apps/dreamverse/web/public/k2.png new file mode 100644 index 0000000000..28533a6892 Binary files /dev/null and b/apps/dreamverse/web/public/k2.png differ diff --git a/apps/dreamverse/web/public/logo-light.svg b/apps/dreamverse/web/public/logo-light.svg new file mode 100644 index 0000000000..87972d8aab --- /dev/null +++ b/apps/dreamverse/web/public/logo-light.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/dreamverse/web/public/logo.svg b/apps/dreamverse/web/public/logo.svg new file mode 100644 index 0000000000..0b8b058b51 --- /dev/null +++ b/apps/dreamverse/web/public/logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/dreamverse/web/src/app/globals.css b/apps/dreamverse/web/src/app/globals.css new file mode 100644 index 0000000000..4c2641bfd8 --- /dev/null +++ b/apps/dreamverse/web/src/app/globals.css @@ -0,0 +1,329 @@ +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap"); +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +:root { + color-scheme: light; + + --accent-blue: #356cff; + + --background: #f5f4f4; + --foreground: #0f172a; + + --card: #ffffff; + --card-foreground: #0f172a; + + --popover: #ffffff; + --popover-foreground: #0f172a; + + --primary: #0f172a; + --primary-foreground: #f8fafc; + + --secondary: #f1f5f9; + --secondary-foreground: #1e293b; + + --muted: #f1f5f9; + --muted-foreground: #64748b; + + --accent: #e2e8f0; + --accent-foreground: #0f172a; + + --destructive: #ef4444; + --destructive-foreground: #ffffff; + + --border: #e2e8f0; + --input: #cbd5e1; + --ring: #94a3b8; + + --radius: 0.5rem; +} + +.dark { + color-scheme: dark; + + --accent-blue: #356cff; + + --background: #0f172a; + --foreground: #e2e8f0; + + --card: #0f172a; + --card-foreground: #f1f5f9; + + --popover: #0f172a; + --popover-foreground: #f1f5f9; + + --primary: #f1f5f9; + --primary-foreground: #0f172a; + + --secondary: #1e293b; + --secondary-foreground: #f1f5f9; + + --muted: #1e293b; + --muted-foreground: #94a3b8; + + --accent: #1e293b; + --accent-foreground: #f1f5f9; + + --destructive: #991b1b; + --destructive-foreground: #fecaca; + + --border: #334155; + --input: #334155; + --ring: #cbd5e1; +} + +@theme inline { + --color-accent-blue: var(--accent-blue); + + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +/* ——— Resets ——— */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + min-height: 100%; +} + +html { + background: var(--background); +} + +body { + margin: 0; + overflow-y: auto; + color: var(--foreground); + font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif; + background: var(--background); +} + +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + opacity: 0; + pointer-events: none; + transition: opacity 300ms ease; + background: + radial-gradient(circle at top, rgba(56, 189, 248, 0.14), transparent 36%), radial-gradient(circle at right top, rgba(129, 140, 248, 0.12), transparent 28%), + linear-gradient(180deg, #020617 0%, #000000 100%); + background-attachment: fixed; +} + +.dark body::after { + opacity: 1; +} + +a { + color: inherit; +} + +@layer base { + button, + input, + textarea, + select { + font: inherit; + } +} + +@layer base { + img, + svg, + video, + canvas { + display: block; + max-width: 100%; + } +} + +summary { + list-style: none; +} + +summary::-webkit-details-marker { + display: none; +} + +::selection { + background: rgba(56, 189, 248, 0.25); +} + +.dark ::selection { + background: rgba(56, 189, 248, 0.35); + color: #ffffff; +} + +/* ——— Base layer ——— */ + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* ——— Utilities ——— */ + +@layer utilities { + .scrollbar-hidden { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .scrollbar-hidden::-webkit-scrollbar { + display: none; + } + + .stroke-dash-anim { + animation: stroke-dash-animation 2s linear infinite; + } +} + +@keyframes stroke-dash-animation { + from { + stroke-dashoffset: 8; + } + to { + stroke-dashoffset: 0; + } +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(200%); + } +} + +@keyframes slide-in-from-right { + from { + transform: translateX(10px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.animate-slide-in-from-right { + animation: slide-in-from-right 0.2s ease-out; +} + +@keyframes stt-pulse { + 0%, + 100% { + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); + } + 50% { + box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.1); + } +} + +.animate-stt-pulse { + animation: stt-pulse 1.4s ease-in-out infinite; +} + +/* ——— Theme transition ——— */ + +html.theme-transition, +html.theme-transition *, +html.theme-transition *::before, +html.theme-transition *::after { + transition: + background-color 300ms ease, + color 300ms ease, + border-color 300ms ease, + box-shadow 300ms ease, + fill 300ms ease, + stroke 300ms ease !important; +} + +/* —————————————— CUSTOM TAILWIND —————————————— */ + +@layer components { + .debug { + @apply border border-rose-500; + } + + .horizontal { + @apply flex flex-row; + } + + .vertical { + @apply flex flex-col; + } + + .horizontal.center-v { + @apply items-center; + } + + .horizontal.center-h { + @apply justify-center; + } + + .horizontal.center { + @apply justify-center items-center; + } + + .vertical.center-v { + @apply justify-center; + } + + .vertical.center-h { + @apply items-center; + } + + .vertical.center { + @apply justify-center items-center; + } + + .space-between { + @apply justify-between; + } +} diff --git a/apps/dreamverse/web/src/app/internal/f8a3991c/replica-monitor/page.tsx b/apps/dreamverse/web/src/app/internal/f8a3991c/replica-monitor/page.tsx new file mode 100644 index 0000000000..7b9327613d --- /dev/null +++ b/apps/dreamverse/web/src/app/internal/f8a3991c/replica-monitor/page.tsx @@ -0,0 +1,5 @@ +import MonitorPage from '@/components/MonitorPage'; + +export default function ReplicaMonitorPage() { + return ; +} diff --git a/apps/dreamverse/web/src/app/layout.tsx b/apps/dreamverse/web/src/app/layout.tsx new file mode 100644 index 0000000000..b0770eaade --- /dev/null +++ b/apps/dreamverse/web/src/app/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next'; +import { GeistSans } from 'geist/font/sans'; +import { GeistMono } from 'geist/font/mono'; + +import { Toaster } from '@/components/ui/sonner'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Dreamverse', + icons: { + icon: '/icon-simple.svg', + shortcut: '/icon-simple.svg', + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +