Compare commits

..
159 changed files with 21151 additions and 5725 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ if [[ $machine_arch =~ ^(aarch64|arm64)$ ]]; then
npm run e2e -- \
--project=chromium \
--project=mobile-chromium \
--grep-invert='streams, plays, and surfaces a downloadable clip|starts a new project and switches back to the prior session|saved projects persist across a page reload'
--grep-invert='streams, plays, and surfaces a downloadable clip|starts a new project and switches back to the prior session|saved projects persist across a page reload|sends validated assets and plays a clearly labeled sample'
else
BACKEND_HOST=127.0.0.1 BACKEND_PORT="$BACKEND_PORT" CI=1 \
npm run e2e -- \
+1 -1
View File
@@ -2,4 +2,4 @@
# Canonical Slurm CI selection for the VMoBA-inference lane.
set -euo pipefail
exec python fastvideo/tests/inference/vmoba/test_vmoba_inference.py
exec pytest ./fastvideo/tests/inference/vmoba/test_vmoba_inference.py -vs
+5 -2
View File
@@ -9,9 +9,12 @@ mkdir -p "$PERF_REPORTS_DIR"
if [[ ${BUILDKITE_PULL_REQUEST:-false} =~ ^[1-9][0-9]*$ ]]; then
export PERF_RUN_SOURCE=pr
export PERF_UPLOAD_POLICY=pass
export PERF_UPLOAD_POLICY=never
elif [ "${BUILDKITE_BRANCH:-}" = main ] \
&& { [ "${BUILDKITE_SOURCE:-}" = schedule ] || [ "${TEST_SCOPE:-}" = full ]; }; then
&& { [ "${BUILDKITE_SOURCE:-}" = schedule ] || [ "${TEST_SCOPE:-}" = full ] \
|| { [ "${BUILDKITE_SOURCE:-}" = api ] \
&& [ "${BUILDKITE_PIPELINE_SLUG:-}" = fastvideo-performance-lane ] \
&& [ "${TEST_SCOPE:-}" = direct ]; }; }; then
export PERF_RUN_SOURCE=scheduled_main
export PERF_UPLOAD_POLICY=always
elif [ "${TEST_SCOPE:-}" = direct ]; then
+4 -4
View File
@@ -104,9 +104,9 @@ SAFE_PATTERNS = (
"comfyui/**",
"docs/**",
"examples/**",
"mkdocs.yml",
"requirements-mkdocs.in",
"requirements-mkdocs.txt",
"docs-site/**",
"requirements-docs.in",
"requirements-docs.txt",
"scripts/**",
"tests/__init__.py",
"tests/local_tests/**",
@@ -321,7 +321,7 @@ def classify_paths(paths: list[str], removed_paths: Iterable[str] = ()) -> Merge
plan.require_all("changed-file API failed; failing closed")
continue
if path in {"requirements-mkdocs.in", "requirements-mkdocs.txt"}:
if path in {"requirements-docs.in", "requirements-docs.txt"}:
plan.reasons.append(f"documentation dependencies need no GPU integration: {path}")
continue
@@ -0,0 +1,73 @@
name: Scheduled Performance
on:
schedule:
# Sunday 23:00 America/Los_Angeles, including daylight saving time.
- cron: "0 6 * * 1"
- cron: "0 7 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
trigger:
if: github.repository == 'hao-ai-lab/FastVideo' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Select the Pacific-time schedule
id: schedule
env:
EVENT_NAME: ${{ github.event_name }}
SCHEDULE_EXPR: ${{ github.event.schedule }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == schedule ]]; then
expected='0 7 * * 1'
if [[ "$(TZ=America/Los_Angeles date +%z)" == -0700 ]]; then
expected='0 6 * * 1'
fi
if [[ "$SCHEDULE_EXPR" != "$expected" ]]; then
echo 'run=false' >> "$GITHUB_OUTPUT"
exit 0
fi
fi
echo 'run=true' >> "$GITHUB_OUTPUT"
- name: Trigger weekly performance on Slinky Slurm
if: steps.schedule.outputs.run == 'true'
env:
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
SOURCE_SHA: ${{ github.sha }}
SOURCE_BRANCH: ${{ github.event.repository.default_branch }}
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
BK_PIPELINE: fastvideo-performance-lane
run: |
set -euo pipefail
if [[ ${#SOURCE_SHA} -ne 40 || "$SOURCE_SHA" == *[!0-9a-f]* ]]; then
echo 'SOURCE_SHA must be a full immutable commit SHA' >&2
exit 1
fi
payload=$(python3 - <<'PY'
import json
import os
print(json.dumps({
"commit": os.environ["SOURCE_SHA"],
"branch": os.environ["SOURCE_BRANCH"],
"message": "Weekly performance on Slinky Slurm",
"ignore_pipeline_branch_filters": True,
"env": {
"TEST_SCOPE": "direct",
"FULL_SUITE": "false",
"TEST_TYPE": "performance",
"PR_NUMBER": "false",
"PR_TITLE": "Scheduled performance",
},
}))
PY
)
curl -sS --fail-with-body --connect-timeout 10 --max-time 60 -X POST \
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds" \
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
-H "Content-Type: application/json" \
--data-raw "$payload"
-34
View File
@@ -1,34 +0,0 @@
name: Close Stale Issues and PRs
on:
schedule:
# Daily at 1:30 AM UTC
- cron: '30 1 * * *'
jobs:
stale:
if: github.repository == 'hao-ai-lab/FastVideo'
permissions:
issues: write
pull-requests: write
actions: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
operations-per-run: 500
exempt-draft-pr: true
exempt-issue-labels: 'keep-open,pinned,security,Bug,RFC'
exempt-pr-labels: 'keep-open,pinned'
labels-to-add-when-unstale: 'unstale'
labels-to-remove-when-stale: 'unstale'
days-before-issue-stale: 90
days-before-issue-close: 30
stale-issue-label: 'stale'
days-before-pr-stale: 60
days-before-pr-close: 14
stale-pr-label: 'stale'
+48 -12
View File
@@ -7,9 +7,11 @@ on:
- 'docs/**'
- 'examples/**'
- 'scripts/inference/**'
- 'mkdocs.yml'
- 'requirements-mkdocs.in'
- 'requirements-mkdocs.txt'
- 'docs-site/**'
- 'fastvideo/**'
- 'README.md'
- 'requirements-docs.in'
- 'requirements-docs.txt'
- 'scripts/check_docs_links.py'
- '.github/workflows/infra-docs.yml'
pull_request:
@@ -18,9 +20,11 @@ on:
- 'docs/**'
- 'examples/**'
- 'scripts/inference/**'
- 'mkdocs.yml'
- 'requirements-mkdocs.in'
- 'requirements-mkdocs.txt'
- 'docs-site/**'
- 'fastvideo/**'
- 'README.md'
- 'requirements-docs.in'
- 'requirements-docs.txt'
- 'scripts/check_docs_links.py'
- '.github/workflows/infra-docs.yml'
@@ -35,6 +39,7 @@ concurrency:
jobs:
build:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -51,7 +56,7 @@ jobs:
uses: astral-sh/setup-uv@v3
- name: Install dependencies
run: uv pip install --system -r requirements-mkdocs.txt
run: uv pip install --system -r requirements-docs.txt
- name: Setup Pages
uses: actions/configure-pages@v4
@@ -59,10 +64,16 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: '24'
cache: npm
cache-dependency-path: |
docs/package-lock.json
docs-site/package-lock.json
- name: Install cookbook dependencies
run: npm ci --prefix docs
run: |
npm ci --prefix docs
npm ci --prefix docs-site
- name: Validate and generate cookbook catalogs
run: |
@@ -71,8 +82,25 @@ jobs:
python docs/tests/test_cookbook_assets.py
npm run build:catalog --prefix docs
- name: Build documentation
run: mkdocs build
- name: Check site types and formatting
run: |
npm run check --prefix docs-site
npm run format:check --prefix docs-site
- name: Build and validate documentation
run: |
npm run build --prefix docs-site
npm run validate --prefix docs-site
npm run test:content --prefix docs-site
python -m unittest discover -s docs-site/tests -p 'test_*.py'
python docs/check_cookbook_assets.py docs-site/dist
- name: Install test browser
working-directory: docs-site
run: npx playwright install --with-deps chromium
- name: Test documentation and cookbook interactions
run: npm test --prefix docs-site
- name: Check docs links
run: python scripts/check_docs_links.py
@@ -80,7 +108,15 @@ jobs:
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./site
path: ./docs-site/dist
- name: Upload browser failure traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: docs-browser-results
path: docs-site/test-results
if-no-files-found: ignore
deploy:
environment:
+1 -3
View File
@@ -52,9 +52,8 @@ dist/
eggs/
.eggs/
# MkDocs documentation
# Generated documentation
site/
docs/assets/cookbook-serving.json
docs/assets/cookbook-config/
examples/serving/clients/node_modules/
docs/node_modules/
@@ -63,7 +62,6 @@ docs/examples/
docs/inference/examples/
docs/training/examples/
docs/distillation/examples/
!requirements-mkdocs.txt
# VSCode
.vscode/
+1
View File
@@ -21,6 +21,7 @@ repos:
rev: v0.43.0
hooks:
- id: yapf
require_serial: true
args: [--in-place, --verbose]
language_version: python3.12
additional_dependencies: [toml] # TODO: Remove when yapf is upgraded
+1 -1
View File
@@ -6,7 +6,7 @@
- Tests:
- `fastvideo/tests/` for package-level tests (dataset, encoders, inference, training, SSIM, workflow).
- `tests/local_tests/` for additional local/component checks.
- Docs and guides: `docs/` (MkDocs source), with contributor docs in `docs/contributing/`.
- Docs and guides: canonical Markdown in `docs/`, the Blume app in `docs-site/`, and contributor docs in `docs/contributing/`.
- Runnable examples and scripts: `examples/` and `scripts/`.
- Static assets: `assets/` (including `assets/images/`, `assets/videos/`, and `assets/prompts/`) and `comfyui/assets/`.
@@ -3,6 +3,37 @@ import { stat } from 'node:fs/promises';
import { test, expect, type WebSocket as PWWebSocket } from '@playwright/test';
test.describe('mock-backed generation smoke', () => {
test('leave dialog blocks navigation and keeps cancel and confirm usable', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(body.service !== 'ltx2-streaming-mock-server', 'Requires the local mock runtime.');
await page.goto('/');
await page.getByLabel('Continuation prompt').fill('A paper fox walks through a sunlit forest.');
await page.getByRole('button', { name: 'Generate', exact: true }).click();
const leave = page.getByRole('button', { name: 'Leave', exact: true });
await expect(leave).toBeEnabled();
await leave.click();
const dialog = page.getByRole('dialog', { name: 'Leave session?' });
await expect(dialog).toBeVisible();
expect(await dialog.evaluate((element) => element.closest('.isolate') === null)).toBe(true);
await expect(page.getByRole('button', { name: 'Toggle sidebar' }).click({ trial: true, timeout: 1000 })).rejects.toThrow();
for (let index = 0; index < 4; index++) {
await page.keyboard.press('Tab');
await expect.poll(() => dialog.evaluate((element) => element.contains(document.activeElement))).toBe(true);
}
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click();
await expect(dialog).toHaveCount(0);
await expect(leave).toBeFocused();
await leave.click();
await page.keyboard.press('Escape');
await expect(dialog).toHaveCount(0);
await expect(leave).toBeFocused();
await leave.click();
await dialog.getByRole('button', { name: 'Leave session', exact: true }).click();
await expect(dialog).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Generate', exact: true })).toBeVisible();
});
test('accepts a custom prompt and enters the streaming state', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
+1 -1
View File
@@ -2739,7 +2739,7 @@ export default function Page() {
<Header timeLeft={headerTimeLeft} formatTime={formatTime} onToggleSidebar={() => setSidebarOpen((prev) => !prev)} />
<div className={cn(
"relative flex flex-1 min-h-0 flex-col px-4 pb-2 sm:px-6 sm:pb-12",
"relative isolate flex flex-1 min-h-0 flex-col px-4 pb-2 sm:px-6 sm:pb-12",
!isViewingMode && !showActiveProject && generationMode !== "t2va"
? "justify-start overflow-y-auto pt-4"
: "justify-center",
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
@@ -19,6 +20,7 @@ export default function LeaveSessionModal({
onConfirmLeave = () => {},
}: LeaveSessionModalProps) {
const [suppress, setSuppress] = useState(false);
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
if (open) setSuppress(false);
@@ -36,43 +38,58 @@ export default function LeaveSessionModal({
}
return (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/45 px-4 backdrop-blur-[3px]">
<div
role="dialog"
aria-modal="true"
aria-labelledby="leave-session-title"
className="w-full max-w-md rounded-3xl border border-border/70 bg-card/95 p-6 shadow-2xl"
>
<div className="space-y-3">
<div className="space-y-1">
<h2 id="leave-session-title" className="text-lg font-semibold text-foreground">
Leave session?
</h2>
<p className="text-sm text-muted-foreground">
This will end your current session. You will need to queue again for a GPU to start a new project.
</p>
<DialogPrimitive.Root open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose(); }}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay asChild>
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/45 px-4 backdrop-blur-[3px]">
<DialogPrimitive.Content
asChild
onInteractOutside={(event) => event.preventDefault()}
onOpenAutoFocus={() => {
previousFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
}}
onCloseAutoFocus={(event) => {
event.preventDefault();
if (previousFocus.current?.isConnected) previousFocus.current.focus();
}}
>
<div
className="w-full max-w-md rounded-3xl border border-border/70 bg-card/95 p-6 shadow-2xl"
>
<div className="space-y-3">
<div className="space-y-1">
<DialogPrimitive.Title className="text-lg font-semibold text-foreground">
Leave session?
</DialogPrimitive.Title>
<DialogPrimitive.Description className="text-sm text-muted-foreground">
This will end your current session. You will need to queue again for a GPU to start a new project.
</DialogPrimitive.Description>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="suppress-leave-warning"
checked={suppress}
onCheckedChange={(v) => setSuppress(v === true)}
/>
<Label htmlFor="suppress-leave-warning" className="text-sm text-muted-foreground cursor-pointer">
Do not warn again
</Label>
</div>
</div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button variant="destructive" onClick={handleConfirm}>
Leave session
</Button>
</div>
</div>
</DialogPrimitive.Content>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="suppress-leave-warning"
checked={suppress}
onCheckedChange={(v) => setSuppress(v === true)}
/>
<Label htmlFor="suppress-leave-warning" className="text-sm text-muted-foreground cursor-pointer">
Do not warn again
</Label>
</div>
</div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button variant="destructive" onClick={handleConfirm}>
Leave session
</Button>
</div>
</div>
</div>
</DialogPrimitive.Overlay>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
}
+9
View File
@@ -0,0 +1,9 @@
node_modules/
.blume/
dist/
.env.local
public/
test-results/
playwright-report/
.generated/
.venv/
+10
View File
@@ -0,0 +1,10 @@
node_modules
.blume
dist
public
package-lock.json
test-results
playwright-report
.generated
.venv
migration-anchors.json
+117
View File
@@ -0,0 +1,117 @@
# Documentation and cookbook plan
Readers should be able to choose a model, find their hardware path, generate a
result, and then customize or train it without searching several sections for
the same instructions.
## One home for each kind of information
| Area | Question it answers | Owns |
| --------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Fast Models | Which FastVideo release should I choose? | Our released families, checkpoint exports, collections, short capability descriptions, links to recipes |
| Support matrix | What else can FastVideo run? | The wider supported model set and optimization compatibility |
| Hardware and runtimes | Where can I run it? | NVIDIA RTX/workstation and data center CUDA paths, DGX Spark, Apple MLX, AMD ROCm, platform requirements and limits |
| Documentation | How does FastVideo work? | Installation, the first generation, configuration, CLI, parallelism, offloading, integrations, training, architecture and API reference |
| Cookbook | How do I run this model for this task? | One family home, runtime choices, runnable recipes, model-specific configuration, troubleshooting and evidence |
Fast Models highlights our models without implying that FastVideo only supports
those releases. CUDA and ROCm are runtimes; RTX, data center GPUs and DGX Spark
are hardware paths. MLX is the Apple Silicon runtime. These distinctions should
be visible before readers choose an installation command.
## Included now
- One Hardware and runtimes overview links the five hardware paths. AMD is
explicitly source-backed support with incomplete installation/device coverage,
rather than a promise that CUDA recipes or kernels work on every AMD GPU.
- The getting-started sidebar contains Quick start, Installation, Hardware and
runtimes, and Python API basics.
- Run inference contains shared tasks. Hardware and Integrations are expandable
groups; individual hardware recipes do not interrupt onboarding.
- Model-specific references sit beside their family recipes in the Cookbook
sidebar. There is no separate Model guides list competing with the cookbook.
Existing LTX, Kandinsky and FastH3 reference URLs remain intact.
- Fast Models points FastH3 and FastWan readers to their cookbook families.
FastMetal also has a direct MLX setup entry.
- Generated examples have one visible page title, with the source link below it.
Their previous heading fragments remain available.
## The intended reader flow
1. Start at **Fast Models** to choose a released model, or the support matrix for
the broader catalog.
2. Check **Hardware and runtimes**, then follow the matching installation guide.
3. Open the model's **Cookbook family**. Choose the task and runtime, configure
a recipe, and run it.
4. Follow links to shared configuration, offloading or optimization docs when
more explanation is needed.
5. Move to training, distillation or contributor guides for development work.
The quick start remains a short route for readers who want a first result before
browsing the catalog.
## Next editorial pass
1. **Consolidate onboarding.** Keep one first-video tutorial. Remove repeated
installation blocks from the inference quick start after preserving its unique
image-conditioning examples. Make Installation a platform chooser and keep
installation commands in platform guides.
2. **Give every model one family home.** Expand the cookbook family page with a
short capability overview, runtime/task recipes, checkpoint choices, limits,
troubleshooting and next steps. Fold overlapping model-specific guides into
that page or link a detailed reference from it. Preserve old routes with
redirects before deleting any page.
3. **Separate explanation from recipes.** Hardware owns environment setup;
cookbook owns model commands; shared docs explain the options. The RTX PRO
6000 page's kernel internals and rejected experiments belong in research or
contributor material, separate from its runnable recipe.
4. **Reduce pointer pages and duplicate references.** Replace the two-sentence
ComfyUI pointer with a useful integration guide, or link directly to its
maintained README. Reconcile the two attention-backend development guides.
Let Architecture own the package map instead of repeating it in the agent guide.
5. **Split long optimization material by task.** Keep attention selection,
quantization, distributed execution and offloading cross-linked, with one
owner for each setting. Keep STA and mask search clearly archived.
6. **Fill the ROCm evidence gap.** Document a complete AMD installation and model
run with GPU architecture, ROCm/PyTorch versions, model revision, attention
backend, expected output and measured memory. Source implementation alone
should not turn into a broad device-support claim.
## Per-page structure
Task guides follow: outcome → requirements → runnable example → expected output
→ configuration → troubleshooting → next step. A model recipe adds its checkpoint,
workload, runtime, hardware evidence and measured limitations. Detailed reference
and implementation history should not interrupt setup commands.
See the [authoring guide](../docs/contributing/documentation.md). Examples and
Python reference stay source-generated and searchable; they do not need hundreds
of sidebar entries.
## Publishing
Blume replaces MkDocs through the existing GitHub Pages workflow at
`https://haoailab.com/FastVideo/`. Only `main` deploys. The migration manifest
checks previous routes and heading fragments. This PR changes navigation and
presentation while keeping the technical content available; the consolidation
steps above remain editorial follow-ups.
## Cookbook model pages
The cookbook catalog includes the maintained video, image, audio, and world
models, with category and family filters. FastH3 appears first. Each checkpoint
has its own page; related task or hardware recipes become variants on that page.
Family groups hold the pages alongside their broader runbooks.
FastH3 8-Step V2, FastWan2.1 1.3B, Wan2.1 I2V 14B, and LTX2 Distilled use the five
existing editable serving deployments. Their pages have a model header, YAML
actions, generated commands, and a compact hardware/options matrix. Other models
publish existing source commands and YAML without inventing editable settings.
Hardware-fit validation remains unverified. New quantization, encoder, or runtime
choices require a backend recipe before becoming selectable.
The retired page-copy and tab handlers, cookbook JavaScript, and theme styles are
removed. Canonical callouts now use Blume syntax, so builds no longer execute a
migration codemod. Existing family URLs show React model cards with their setup
and troubleshooting content. The homepage adds a direct FastH3 recipe link while
keeping the existing logo, news, project cards, typography, and backgrounds.
+134
View File
@@ -0,0 +1,134 @@
# FastVideo documentation
Blume builds the documentation and React cookbook at
[haoailab.com/FastVideo](https://haoailab.com/FastVideo/). This PR replaces the
previous MkDocs site using the existing GitHub Pages publishing workflow.
![Documentation homepage](docs-preview.jpg)
![Fast Models catalog](fast-models-preview.jpg)
## Develop and validate
Read [Documenting FastVideo](../docs/contributing/documentation.md) for setup,
commands, source ownership, and the per-page writing template. Use Node 24 and
Python 3.12. `npm run dev --prefix docs-site` serves under `/FastVideo`.
`npm run build --prefix docs-site` prepares the content and writes `dist/`.
`npm run preview --prefix docs-site` serves that static build.
Edit canonical guides under `docs/`, the welcome page under `content/`, the model
catalog in `lib/fast-models.ts`, and sidebar grouping in `navigation.json`.
Preparation regenerates `.generated/` and `public/`; neither is an editable copy.
The [documentation structure plan](INFORMATION_ARCHITECTURE.md) records the new
navigation and proposed page consolidations. URLs stay stable in this PR.
Hardware and integrations sit beneath Run inference. Model-specific references
sit beside their family recipes in the Cookbook tab. Hardware and runtimes gives
readers a single starting point for CUDA, MLX and ROCm.
## Build architecture
- `scripts/prepare-python-docs.py` uses Griffe to extract modules, classes,
signatures, docstrings, and source directly from Python files. It never imports
FastVideo or needs PyTorch. Only Griffe, Markdown, and PyYAML are installed from
`requirements-docs.txt`. It also regenerates examples and serving metadata.
- `scripts/transfer-docs.mjs` rewrites links, preserves fragments, copies
assets, and overlays the handwritten application pages. Canonical guides use
native Blume callouts and React family cards; no migration converter runs during builds.
- `migration-anchors.json` is a frozen compatibility contract for the previous
site's URLs and heading fragments. Tests check the published output against it.
Do not regenerate it to hide missing routes or anchors.
- `scripts/prepare-content.mjs` builds and copies cookbook catalogs and assets.
React model cards replace the old family selectors. Source commands and
serving configurations remain owned by their checked-in recipes.
- `.github/workflows/infra-docs.yml` validates and uploads `docs-site/dist/` with
the existing Pages actions. Deployment runs only on `main`. The domain,
environment and `/FastVideo` prefix are unchanged.
## Homepage and catalog
The welcome page uses Blume frame mode for its centered logo and accessible
heading. News comes from the first two dated entries under `NEWS` in the root
README; projects come from its Awesome work section. The original introduction
and key features come from canonical `docs/index.md`.
The star button fetches GitHub's public repository count on load and every five
minutes while visible. Failed initial requests show "Star"; failed refreshes
retain the previous count. No credentials are used. The Trendshift badge loads
from Trendshift. The lab mark in `assets/hao-ai-lab.png` is the supplied image.
Fast Models uses family cards, setup links, family filters, search, and consistent
checkpoint rows. The curated links were checked against the public Hugging Face
API on October 16, 2026. Published weights do not establish runtime or hardware
compatibility. Check the support matrix and matching setup guide.
## Extend the cookbook
`islands/Cookbook.tsx` owns the React UI and its loading/edit state. Shared
configuration behavior stays in `docs/js/cookbook-config.mjs`. The typed adapter
in `lib/cookbook.ts` describes the part of that existing contract used by this UI.
Read [the API contract](../docs/cookbook/design.md) and
[recipe authoring guide](../docs/contributing/cookbook_configuration.md) before
adding fields or recipes.
- Keep native serving YAMLs and `docs/cookbook/recipes/` as the source of truth.
Do not add another recipe database or hardcode field lists in the UI.
- Use `getOptions` for field metadata and `resolveConfig` for validated YAML,
server commands, and client instructions. Clear generated output when invalid.
- The URL stores the selected deployment. Edit values stay local to the page;
URL serialization of all edited values is follow-up work.
- Reset restores the baseline. Missing fields remain inherited. Explicit `false`,
`0`, and `null` keep their meanings. Switching deployments discards edits.
- Installation and deployment guides resolve against the current origin and
deployment prefix. Family runbook URLs remain available in the Blume site.
- VRAM fit is unknown. Add a calculator or a verified-run badge only with the
measurements and evidence contract described in #1949.
Blume builds the documentation shell with Astro. Components in `islands/` are
ordinary React; `client = "load"` enables hydration. `components/ui/` contains
editable shadcn components, `components.json` configures the CLI, and `theme.css`
maps their tokens to Blume's light/dark palette. Blume supplies Tailwind and reads
`tsconfig.json` aliases. Add components from this directory:
```bash
npx shadcn@4.21.5 add dialog
```
The two npm overrides keep Astro's React integration and React plugin aligned
with the versions used by Blume 2.2.3. Without them, npm's resolver cycles between
the integration's newer plugin and Blume's pinned plugin. Recheck and remove the
overrides when upgrading Blume.
The initial dependency audit reports 10 transitive advisories in Blume's dependency
tree, including its unused Vercel adapter and Markdown build dependencies. The
ordinary `npm audit fix` does not clear them; review upstream fixes before
production deployment. Do not apply `npm audit fix --force`, whose proposed
replacement is `blume@0.0.0`.
## Verify UI changes
```bash
npm run format:check --prefix docs-site
npm run check --prefix docs-site
npm run test:cookbook --prefix docs
npm run build --prefix docs-site
npm run validate --prefix docs-site
npm run test:content --prefix docs-site
cd docs-site
npx playwright install chromium
npm test
```
The browser suite starts a production preview. It checks edits and downloads,
invalid-output suppression, reset, switching runtimes, retry, obsolete loads,
keyboard editing, and mobile overflow under `/FastVideo`. Keep shared API tests
in `docs/tests/`; add React interaction tests here. No serving process is launched.
## Review and follow-up
This PR switches the existing Pages artifact from MkDocs to Blume when merged.
The original routes and fragments are checked against the frozen migration
manifest. Review CI and the catalog/sidebar on desktop and mobile before merging.
The remaining cookbook design and editor coverage in #1949 can continue without
keeping a second documentation build. The editorial consolidation proposals are
listed separately in `INFORMATION_ARCHITECTURE.md`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+30
View File
@@ -0,0 +1,30 @@
import { defineConfig } from "blume";
import { readFileSync } from "node:fs";
export default defineConfig({
title: "FastVideo",
description: "A unified inference and post-training framework for accelerated video generation.",
content: { root: ".generated/content" },
logo: { image: "/icon.svg", text: "FastVideo" },
theme: { accent: "blue", background: { light: "#ffffff", dark: "#1c1d20" }, mode: "system" },
search: {
popular: [
{ label: "Quick start", href: "/getting_started/quick_start", icon: "rocket" },
{ label: "Fast Models", href: "/fast-models", icon: "boxes" },
{ label: "Installation", href: "/getting_started/installation", icon: "download" },
{ label: "Supported models", href: "/inference/support_matrix", icon: "video" },
{ label: "Python API", href: "/api", icon: "code" },
],
},
navigation: {
actions: [{ label: "GitHub", href: "https://github.com/hao-ai-lab/FastVideo" }],
tabs: [
{ label: "Documentation", path: "/" },
{ label: "Cookbook", path: "/cookbook" },
],
sidebar: { items: JSON.parse(readFileSync(new URL("./.generated/navigation.json", import.meta.url), "utf8")) },
},
// Keep the production prefix in local builds so asset-path regressions are visible.
deployment: { site: "https://haoailab.com", base: "/FastVideo" },
seo: { robots: true, sitemap: true, og: { enabled: false } },
});
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "theme.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
@@ -0,0 +1,57 @@
import { useState } from "react";
/** A UI experiment, deliberately separated from the serving schema and generated YAML. */
export default function ComponentPlan({ onDraftChange }: { onDraftChange?: (draft: boolean) => void }) {
const [selection, setSelection] = useState<Record<string, string>>({});
const groups = [
{ name: "Weights", values: ["Recipe default", "FP8", "NVFP4"] },
{ name: "Text encoder", values: ["Recipe default", "NVFP4 encoder"] },
{ name: "Video decoder", values: ["Recipe default", "FP16 VAE", "NVFP4 VAE"] },
];
return (
<details className="component-plan">
<summary>
Explore components <span>Design preview</span>
</summary>
<div className="component-plan-fields">
{groups.map((group) => (
<div key={group.name}>
<span className="component-plan-label">{group.name}</span>
<div className="option-chips" role="group" aria-label={group.name}>
{group.values.map((value) => (
<button
key={value}
type="button"
aria-pressed={(selection[group.name] ?? "Recipe default") === value}
onClick={() => {
const next = { ...selection, [group.name]: value };
setSelection(next);
onDraftChange?.(Object.values(next).some((value) => value !== "Recipe default"));
}}
>
{value}
</button>
))}
</div>
</div>
))}
</div>
<p>
Preview only. Alternate components need a compatible recipe; some releases are planned. These choices do not
change YAML.
</p>
{Object.values(selection).some((value) => value !== "Recipe default") && (
<button
type="button"
className="component-plan-reset"
onClick={() => {
setSelection({});
onDraftChange?.(false);
}}
>
Restore recipe components
</button>
)}
</details>
);
}
@@ -0,0 +1,94 @@
import { useEffect, useId, useState } from "react";
import { Input } from "@/components/ui/input";
export default function HardwarePicker({
backend,
options,
value,
onChange,
}: {
backend: string;
options: string[][];
value: string;
onChange: (value: string) => void;
}) {
const [search, setSearch] = useState("");
const [adding, setAdding] = useState(false);
const [custom, setCustom] = useState("");
useEffect(() => {
const requested = new URL(window.location.href).searchParams.get("hardware");
setCustom(requested?.startsWith("custom:") ? requested.slice(7) : "");
}, []);
const id = useId();
const known = options.filter((option) => option.join(" ").toLowerCase().includes(search.toLowerCase()));
function add() {
const name = search.trim();
if (!name) return;
setCustom(name);
onChange(`custom:${name}`);
setSearch("");
setAdding(false);
}
return (
<div className="hardware-picker">
<fieldset className="hardware-matrix">
<legend className="sr-only">Hardware target</legend>
{known.map(([key, title, detail]) => (
<label key={key} data-selected={value === key}>
<input
type="radio"
name={`hardware-${id}`}
checked={value === key}
onChange={() => onChange(key)}
value={key}
/>
<span>
<strong>{title}</strong>
<small>{detail}</small>
</span>
</label>
))}
{custom && (
<label data-selected={value === `custom:${custom}`}>
<input
type="radio"
name={`hardware-${id}`}
checked={value === `custom:${custom}`}
onChange={() => onChange(`custom:${custom}`)}
/>
<span>
<strong>{custom}</strong>
<small>Custom device</small>
</span>
</label>
)}
</fieldset>
<button type="button" className="hardware-add" aria-expanded={adding} onClick={() => setAdding(!adding)}>
+ Find or add {backend === "mlx" ? "a Mac" : "a GPU"}
</button>
{adding && (
<form
className="hardware-search"
onSubmit={(event) => {
event.preventDefault();
add();
}}
>
<label htmlFor={id}>Device name</label>
<Input
id={id}
autoFocus
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder={backend === "mlx" ? "e.g. M4 Max" : "e.g. RTX 4090"}
maxLength={80}
/>
<p>Search the list or add your device. Memory lookup and fit checks are not connected yet.</p>
<button type="submit" disabled={!search.trim()}>
Add device
</button>
</form>
)}
</div>
);
}
@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import { sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
export default function ModelHeader({ model, actions }: { model: DirectoryModel; actions?: ReactNode }) {
return (
<>
<a className="recipe-back" href={sitePath("/cookbook")}>
← All recipes
</a>
<header className="recipe-model-header">
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="48" height="48" />
<div>
<p className="recipe-publisher">{model.provider}</p>
<h1>{model.title}</h1>
<p className="recipe-model-task">{model.tags.join(" · ")}</p>
{!model.model.startsWith("./") && (
<a href={`https://huggingface.co/${model.model}`}>View on Hugging Face ↗</a>
)}
</div>
<div className="recipe-model-actions">{actions}</div>
</header>
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import { Slot } from "radix-ui";
const badgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span";
return (
<Comp data-slot="badge" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
+61
View File
@@ -0,0 +1,61 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import { Slot } from "radix-ui";
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
+20
View File
@@ -0,0 +1,20 @@
import * as React from "react";
import { cn } from "cn";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
}
export { Input };
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react";
import { cn } from "cn";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
}
export { Textarea };
+19
View File
@@ -0,0 +1,19 @@
---
title: Cookbook
description: Recipes for video, image, audio, and world generation.
mode: wide
---
<a id="inference-cookbook"></a>
<a id="planned-families-heading"></a>
<a id="video-models-heading"></a>
<a id="image-models-heading"></a>
<a id="audio-models-heading"></a>
<a id="world-models-heading"></a>
<Cookbook />
<noscript>
Interactive configuration needs JavaScript. Browse the{" "}
<a href="https://github.com/hao-ai-lab/FastVideo/tree/main/examples">runnable examples</a> directly.
</noscript>
@@ -0,0 +1,7 @@
---
title: LTX2 Distilled
description: Configure incremental video delivery with the native WebSocket server.
mode: frame
---
<Cookbook modelId="ltx2-distilled" />
@@ -0,0 +1,7 @@
---
title: FastH3 8-Step V2
description: Configure distilled video generation with synchronized audio on CUDA or Apple Silicon.
mode: frame
---
<Cookbook modelId="fasth3-8step" />
@@ -0,0 +1,7 @@
---
title: FastH3 Trim
description: Configure FastH3 Trim NVFP4 and copy inference or serving commands.
mode: frame
---
<Cookbook modelId="fasth3-trim" />
@@ -0,0 +1,7 @@
---
title: FastWan2.1 1.3B
description: Configure distilled text-to-video generation on CUDA.
mode: frame
---
<Cookbook modelId="fastwan21" />
@@ -0,0 +1,7 @@
---
title: Wan2.1 I2V 14B
description: Configure image-to-video generation with a reference image.
mode: frame
---
<Cookbook modelId="wan21-i2v" />
+10
View File
@@ -0,0 +1,10 @@
---
title: Fast Models
description: Explore FastVideo's model releases and inference recipes.
mode: wide
---
<FastModels />
Model links reviewed October 16, 2026. Browse all releases on
[FastVideo's Hugging Face organization](https://huggingface.co/FastVideo).
+65
View File
@@ -0,0 +1,65 @@
---
title: Welcome to FastVideo
description: Run, train, and accelerate video generation models with FastVideo.
mode: frame
---
<div className="home-page">
<header className="home-hero not-prose">
<h1 id="welcome-to-fastvideo" className="home-logo">
<img src="/assets/logos/logo.svg" alt="Welcome to FastVideo" width={252} height={105} />
</h1>
<p className="home-description">Run, train, and accelerate video generation models with FastVideo.</p>
<nav className="home-project-links" aria-label="About the project">
<GitHubStars />
<a className="home-lab-link" href="https://haoailab.com/">
<img src="/assets/hao-ai-lab.png" width={40} height={24} alt="" aria-hidden="true" />
Hao AI Lab
</a>
<a className="home-models-link" href="/fast-models">Fast Models <span aria-hidden="true">→</span></a>
</nav>
<div className="home-trendshift">
<a href="https://trendshift.io/repositories/14509?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-14509" target="_blank" rel="noopener noreferrer">
<img src="https://trendshift.io/api/badge/repositories/14509" alt="hao-ai-lab/FastVideo | Trendshift" width={250} height={55} />
</a>
</div>
</header>
<include>/_snippets/home-description.mdx</include>
<a className="home-featured-model not-prose" href="/cookbook/minimax-h3/fasth3-8step">
<img src="/assets/logos/fastvideo.webp" alt="" width={40} height={40} />
<span>
<small>Start with a FastVideo model</small>
<strong>FastH3 8-Step V2</strong>
</span>
<span className="home-featured-action">Configure a recipe →</span>
</a>
<include>/_snippets/home-news.mdx</include>
<CardGroup cols={2}>
<Card title="Quick start" href="/getting_started/quick_start" icon="rocket">
Generate your first video with the Python API.
</Card>
<Card title="Installation" href="/getting_started/installation" icon="download">
Set up FastVideo on NVIDIA GPUs or Apple Silicon.
</Card>
<Card title="Cookbook" href="/cookbook" icon="book-open">
Choose a model recipe, customize its settings, and download a serving configuration.
</Card>
<Card title="Training and distillation" href="/training/overview" icon="layers">
Prepare data, fine-tune models, and build faster generators.
</Card>
</CardGroup>
<include>/_snippets/home-details.mdx</include>
<a id="documentation"></a>
## Projects and research
Explore work using FastVideo's code or the attention research behind it.
<include>/_snippets/home-projects.mdx</include>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+835
View File
@@ -0,0 +1,835 @@
import { useEffect, useId, useState, type ReactNode } from "react";
import { Tabs } from "radix-ui";
import { modelDirectory, recipePages, sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import SourceRecipe from "./SourceRecipe";
import ComponentPlan from "@/components/cookbook/ComponentPlan";
import HardwarePicker from "@/components/cookbook/HardwarePicker";
import ModelHeader from "@/components/cookbook/ModelHeader";
import { configAndLaunch, inferenceOutput, sdkCommand } from "@/lib/cookbook-commands";
import {
api,
guideUrl,
parseField,
types,
type Catalog,
type Field,
type Index,
type Result,
type Value,
} from "@/lib/cookbook";
export const client = "load";
function groupTitle(path: string) {
const titles: Record<string, string> = {
server: "Server",
"generator.engine": "GPU resources",
generator: "Checkpoint settings",
"generator.engine.parallelism": "Parallelism",
"generator.engine.offload": "CPU offload",
"generator.engine.compile": "Compilation",
"default_request.sampling": "Request defaults",
streaming: "Streaming",
};
return titles[path] ?? path.split(".").at(-1)?.replaceAll("_", " ");
}
function errorText(error: unknown) {
return error instanceof Error ? error.message : "Unable to load this recipe.";
}
function FieldControl({
field,
edits,
update,
failure,
}: {
field: Field;
edits: Record<string, Value>;
update: (path: string, raw: string | undefined, field: Field) => void;
failure?: string;
}) {
const id = useId();
const kind = types(field.schema);
const changed = Object.hasOwn(edits, field.path);
const value = changed ? edits[field.path] : field.value;
const label = field.schema.title ?? field.path;
const choices = field.schema.enum ?? (kind.includes("boolean") ? [true, false] : undefined);
const nullable = kind.includes("null");
// Keep unfinished numeric and JSON input visible while output is invalid.
const [raw, setRaw] = useState<string | undefined>();
const displayed = raw ?? (value == null ? "" : typeof value === "string" ? value : JSON.stringify(value));
const props = {
id,
"aria-describedby": `${id}-hint${failure ? ` ${id}-error` : ""}`,
"aria-invalid": !!failure,
};
function edit(text: string) {
setRaw(text);
update(field.path, text, field);
}
return (
<div className="matrix-field">
<div className="matrix-field-label">
{choices ? <span>{label}</span> : <label htmlFor={id}>{label}</label>}
{changed && (
<button
type="button"
aria-label={`Use baseline for ${label}`}
onClick={() => {
setRaw(undefined);
update(field.path, undefined, field);
}}
>
Reset
</button>
)}
</div>
{choices ? (
<div role="group" aria-label={label} className="option-chips">
{value === undefined && (
<button type="button" aria-pressed={!changed} onClick={() => update(field.path, undefined, field)}>
Default
</button>
)}
{choices.map((choice) => (
<button
key={JSON.stringify(choice)}
type="button"
aria-pressed={value === choice}
onClick={() => update(field.path, JSON.stringify(choice), field)}
>
{typeof choice === "boolean" ? (choice ? "On" : "Off") : String(choice)}
</button>
))}
{nullable && !choices.includes(null) && (
<button
type="button"
aria-pressed={value === null}
onClick={() => update(field.path, "null", { ...field, schema: { ...field.schema, enum: [null] } })}
>
null
</button>
)}
</div>
) : kind.includes("object") || kind.includes("array") ? (
<Textarea
{...props}
value={displayed}
onChange={(event) => edit(event.target.value)}
placeholder="Inherited; enter JSON"
/>
) : (
<div className={field.path === "generator.engine.num_gpus" ? "resource-stepper" : ""}>
{field.path === "generator.engine.num_gpus" && (
<button
type="button"
aria-label="Remove a GPU"
disabled={Number(value) <= (field.schema.minimum ?? 1)}
onClick={() => edit(String(Number(value) - 1))}
>
−
</button>
)}
<Input
{...props}
type={kind.includes("integer") || kind.includes("number") ? "number" : "text"}
value={displayed}
placeholder={value === undefined ? "Inherited" : value === null ? "null" : ""}
min={field.schema.minimum}
max={field.schema.maximum}
step={field.schema.multipleOf ?? (kind.includes("integer") ? 1 : "any")}
onChange={(event) => edit(event.target.value)}
/>
{field.path === "generator.engine.num_gpus" && (
<button
type="button"
aria-label="Add a GPU"
disabled={Number(value) >= (field.schema.maximum ?? Infinity)}
onClick={() => edit(String(Number(value) + 1))}
>
+
</button>
)}
</div>
)}
{nullable && !choices && (
<button
className="matrix-null"
type="button"
aria-label={`Set ${label} to null`}
onClick={() => {
setRaw(undefined);
update(field.path, "null", { ...field, schema: { ...field.schema, enum: [null] } });
}}
>
Use null
</button>
)}
<p id={`${id}-hint`} className="sr-only">
{field.schema.description}
{value === undefined ? " Inherited from the runtime." : ""}
</p>
{failure && (
<p id={`${id}-error`} className="matrix-field-error" role="alert">
{failure}
</p>
)}
</div>
);
}
function Editor({
catalog,
deployments,
selectRuntime,
}: {
catalog: Catalog;
deployments: Index["models"][number]["deployments"];
selectRuntime: (id: string) => void;
}) {
const [advanced, setAdvanced] = useState(false);
const [componentDraft, setComponentDraft] = useState(false);
const [runMode, setRunMode] = useState("serving");
const [sourceImage, setSourceImage] = useState("");
const sourceImageId = useId();
const [showEnv, setShowEnv] = useState(false);
const [showClient, setShowClient] = useState(false);
const [outputTab, setOutputTab] = useState("commands");
const [hardware, setHardware] = useState(
() =>
new URL(window.location.href).searchParams.get("hardware") ??
(catalog.id.startsWith("fasth3-trim/") ? "spark" : "unspecified"),
);
const [edits, setEdits] = useState<Record<string, Value>>({});
const [parseErrors, setParseErrors] = useState<Record<string, string>>({});
const [reset, setReset] = useState(0);
const [notice, setNotice] = useState("");
let result: Result | undefined;
let output: ReturnType<typeof inferenceOutput> | undefined;
let failure = "";
let failedPath: string | undefined;
try {
if (componentDraft)
throw new Error("Component preview selected. A compatible serving recipe is needed before generating YAML.");
if (Object.keys(parseErrors).length) throw new Error("Correct the highlighted fields to generate a configuration.");
result = api.resolveConfig(catalog, edits);
output =
runMode === "serving" ? result : inferenceOutput(result, catalog.workload === "i2v" ? sourceImage : undefined);
} catch (error) {
failure = errorText(error);
failedPath = (error as { path?: string }).path;
}
const fields = api.getOptions(catalog, catalog.base_config, edits);
const launchCommand =
output &&
(runMode === "sdk"
? output.command.replace(/fastvideo generate --config config\.yaml$/, sdkCommand)
: output.command);
const groups = [...new Set(fields.map((field) => field.path.split(".").slice(0, -1).join(".")))];
function update(path: string, raw: string | undefined, field: Field) {
setNotice("");
const next = { ...edits };
const errors = { ...parseErrors };
delete errors[path];
if (raw === undefined) delete next[path];
else {
try {
next[path] = parseField(field, raw);
} catch {
errors[path] = "Enter a valid number or JSON value for this field.";
}
}
setEdits(next);
setParseErrors(errors);
}
async function copy(text: string) {
try {
await navigator.clipboard.writeText(text);
setNotice("Copied to clipboard.");
} catch {
setNotice("Copy failed. Select and copy the text below.");
}
}
function download() {
if (!output) return;
const url = URL.createObjectURL(new Blob([output.yaml], { type: "application/yaml" }));
const link = document.createElement("a");
link.href = url;
link.download = "config.yaml";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
setNotice("Downloaded config.yaml.");
}
const mlx = catalog.runtime.backend === "mlx";
const generator = catalog.base_config.generator as { engine?: { quantization?: { transformer_quant?: string } } };
const nvfp4 = generator?.engine?.quantization?.transformer_quant === "NVFP4";
const hardwareOptions = (
mlx
? [["apple", "Apple Silicon", "Unified memory"]]
: [
["b200", "B200", "Data center"],
["gb200", "GB200", "Data center"],
["h100", "H100", "Data center"],
["rtx5090", "RTX 5090", "GeForce"],
["rtx", "RTX PRO 6000", "Workstation"],
["spark", "DGX Spark", "GB10"],
]
).filter(([id]) => !nvfp4 || id !== "h100");
const selectedHardware = hardwareOptions.find(([value]) => value === hardware) ?? hardwareOptions[0];
const model = modelDirectory.find((model) => model.id === catalog.id.split("/")[0])!;
const renderField = (field: Field) => (
<FieldControl
key={`${reset}-${field.path}`}
field={field}
edits={edits}
update={update}
failure={parseErrors[field.path] || (field.path === failedPath ? failure : undefined)}
/>
);
function chooseHardware(value: string) {
setHardware(value);
const url = new URL(window.location.href);
url.searchParams.set("hardware", value);
window.history.replaceState(null, "", url);
}
const advancedGroups = groups.filter(
(group) => group === "server" || group.includes("parallelism") || group.startsWith("streaming"),
);
const standardGroups = groups.filter((group) => group !== "generator.engine" && !advancedGroups.includes(group));
const groupFields = (group: string) =>
fields.filter((field) => field.path.split(".").slice(0, -1).join(".") === group);
return (
<>
<ModelHeader
model={model}
actions={
<>
<Button variant="outline" disabled={!output} onClick={() => output && copy(output.yaml)}>
Copy YAML
</Button>
<Button disabled={!output} onClick={download}>
Download YAML
</Button>
</>
}
/>
{
<div className="recipe-run-modes option-chips" role="group" aria-label="Run mode">
{(mlx ? ["serving"] : ["sdk", "inference", "serving"]).map((mode) => (
<button
type="button"
key={mode}
aria-pressed={runMode === mode}
onClick={() => {
setRunMode(mode);
setNotice("");
}}
>
{mode === "sdk" ? "Python SDK" : mode === "inference" ? "CLI" : "Serving"}
</button>
))}
</div>
}
<section className="recipe-command-box" aria-label="Generated configuration">
<Tabs.Root value={outputTab} onValueChange={setOutputTab}>
<div className="recipe-command-toolbar">
<span className="recipe-unverified">Unverified recipe</span>
<div className="recipe-command-tools">
<button
type="button"
disabled={!output}
onClick={() => output && copy(configAndLaunch(output.yaml, launchCommand!))}
>
Copy
</button>
{runMode === "serving" && (
<button
type="button"
disabled={!result}
aria-expanded={showClient}
aria-controls={`client-${catalog.id.replaceAll("/", "-")}`}
onClick={() => {
setOutputTab("commands");
setShowClient(!showClient);
}}
>
cURL
</button>
)}
{Object.keys(catalog.env).length > 0 && (
<button type="button" aria-pressed={showEnv} onClick={() => setShowEnv(!showEnv)}>
Env
</button>
)}
</div>
<Tabs.List className="recipe-tabs" aria-label="Output format">
<Tabs.Trigger value="commands">Commands</Tabs.Trigger>
<Tabs.Trigger value="yaml">config.yaml</Tabs.Trigger>
</Tabs.List>
</div>
{showEnv && (
<pre className="recipe-environment" tabIndex={0} aria-label="Environment variables">
<code>
{Object.entries(catalog.env)
.map(([name, value]) => `${name}=${value}`)
.join("\n")}
</code>
</pre>
)}
{failure && (
<div role="alert" className="recipe-error">
<p>{failure}</p>
<button type="button" onClick={() => setAdvanced(true)}>
Review settings ↓
</button>
</div>
)}
<Tabs.Content value="commands" className="recipe-command-content">
{result && output && (
<>
<div className="recipe-code-heading">
<span>{runMode === "sdk" ? "Python SDK" : runMode === "inference" ? "Generate" : "Serve"}</span>
</div>
<pre
tabIndex={0}
aria-label={
runMode === "sdk" ? "SDK command" : runMode === "inference" ? "Inference command" : "Server command"
}
>
<code>{launchCommand}</code>
</pre>
<p className="matrix-note">
Includes your settings and writes config.yaml. Run from an installed FastVideo checkout.
</p>
{runMode === "serving" && (
<details
className="recipe-request-code"
id={`client-${catalog.id.replaceAll("/", "-")}`}
open={showClient}
onToggle={(event) => setShowClient(event.currentTarget.open)}
>
<summary>{result.websocketUrl ? "Health check and WebSocket" : "Example request"}</summary>
<button type="button" onClick={() => copy(result.clientCommand)}>
Copy client command
</button>
<pre tabIndex={0} aria-label="Client command">
<code>{result.clientCommand}</code>
</pre>
{result.websocketUrl && (
<p>
WebSocket URL: <code>{result.websocketUrl}</code>
</p>
)}
</details>
)}
</>
)}
</Tabs.Content>
<Tabs.Content value="yaml" className="recipe-command-content">
{output && (
<pre tabIndex={0} aria-label="Generated YAML">
<code>{output.yaml}</code>
</pre>
)}
</Tabs.Content>
</Tabs.Root>
<p role="status" className="recipe-copy-status">
{notice}
</p>
</section>
<details className="recipe-install">
<summary>
Installation <span>{catalog.runtime.label.replace(/\s*REST/gi, "")}</span>
</summary>
{catalog.requirements?.length ? (
<ul>
{catalog.requirements.map((note) => (
<li key={note}>{note}</li>
))}
</ul>
) : null}
<div className="recipe-guide-links">
{catalog.runtime.install_url && <a href={guideUrl(catalog.runtime.install_url)}>Installation guide ↗</a>}
{catalog.guide && <a href={guideUrl(catalog.guide.url)}>Model guide ↗</a>}
{catalog.runtime.client_guide_url && <a href={guideUrl(catalog.runtime.client_guide_url)}>Client guide ↗</a>}
</div>
</details>
<section className="recipe-matrix" aria-label="Configuration settings">
<MatrixRow title="Hardware">
<div className="option-chips hardware-backends" role="group" aria-label="Hardware backend">
{deployments.map((deployment) => (
<button
key={deployment.id}
type="button"
aria-pressed={catalog.id === deployment.id}
onClick={() => {
if (catalog.id !== deployment.id) selectRuntime(deployment.id);
}}
>
{deployment.runtime.includes("mlx") ? "MLX" : "CUDA"}
</button>
))}
</div>
<div className="hardware-resource-grid">
<HardwarePicker
backend={mlx ? "mlx" : "nvidia"}
options={hardwareOptions}
value={hardware.startsWith("custom:") ? hardware : selectedHardware[0]}
onChange={chooseHardware}
/>
<div className="matrix-resources">
<span className="matrix-resource-title">Resources</span>
{fields.filter((field) => field.path === "generator.engine.num_gpus").map(renderField)}
{mlx && <p>1 Apple Silicon host</p>}
<a
href={sitePath(
mlx
? "/getting_started/installation/mlx"
: selectedHardware[0] === "spark"
? "/getting_started/installation/spark"
: "/getting_started/installation/gpu",
)}
>
Setup guide ↗
</a>
</div>
</div>
<p className="matrix-note">Hardware fit has not been verified. GPU count updates the YAML.</p>
</MatrixRow>
{catalog.workload === "i2v" && runMode !== "serving" && (
<MatrixRow title="Inputs">
<div className="matrix-field">
<label htmlFor={sourceImageId} className="matrix-field-label">
Source image path
</label>
<Input
id={sourceImageId}
value={sourceImage}
required
aria-invalid={!sourceImage.trim()}
aria-describedby={`${sourceImageId}-hint`}
placeholder="/path/to/first-frame.png"
onChange={(event) => {
setSourceImage(event.target.value);
setNotice("");
}}
/>
<p id={`${sourceImageId}-hint`} className="matrix-note">
Path to an image on the machine running FastVideo. Included as request.inputs.image_path.
</p>
</div>
</MatrixRow>
)}
{model.family === "FastH3" && (
<MatrixRow title="Components">
{model.id === "fasth3-trim" && (
<div className="option-chips" aria-label="Recipe components">
{["NVFP4 transformer", "50-layer NVFP4 encoder", "Lightweight video VAE", "H3 audio VAE"].map(
(name) => (
<span className="option-chip-static" key={name}>
{name}
</span>
),
)}
</div>
)}
<ComponentPlan key={reset} onDraftChange={setComponentDraft} />
</MatrixRow>
)}
{standardGroups.map((group) => (
<MatrixRow title={groupTitle(group) ?? group} key={group}>
<div className="matrix-options-grid">{groupFields(group).map(renderField)}</div>
</MatrixRow>
))}
<details
className="matrix-advanced"
open={advanced}
onToggle={(event) => setAdvanced(event.currentTarget.open)}
>
<summary>
Advanced <span>Server, topology, and runtime settings</span>
</summary>
{advancedGroups.map((group) => (
<MatrixRow title={groupTitle(group) ?? group} key={group}>
<div className="matrix-options-grid">{groupFields(group).map(renderField)}</div>
{group.includes("parallelism") && (
<button
className="matrix-auto"
type="button"
onClick={() => {
const next = { ...edits };
for (const field of fields.filter((field) => /parallelism\.(tp_size|sp_size)$/.test(field.path)))
next[field.path] = -1;
setEdits(next);
setReset(reset + 1);
setParseErrors(
Object.fromEntries(Object.entries(parseErrors).filter(([path]) => !path.includes("parallelism"))),
);
setComponentDraft(false);
setNotice("Parallelism set to automatic.");
}}
>
Use automatic parallelism
</button>
)}
</MatrixRow>
))}
</details>
</section>
<div className="recipe-bottom-actions">
<span>Copy config and launch together, or download config.yaml first.</span>
<Button
variant="ghost"
onClick={() => {
setEdits({});
setSourceImage("");
setComponentDraft(false);
setParseErrors({});
setReset(reset + 1);
setNotice("Restored the baseline configuration.");
}}
>
Reset changes
</Button>
</div>
</>
);
}
function MatrixRow({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="matrix-row">
<h2>{title}</h2>
<div className="matrix-row-content">{children}</div>
</div>
);
}
export default function Cookbook({ modelId }: { modelId?: string }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const [index, setIndex] = useState<Index>();
const [selected, setSelected] = useState("");
const [catalog, setCatalog] = useState<Catalog>();
const [error, setError] = useState("");
const [retry, setRetry] = useState(0);
useEffect(() => {
const controller = new AbortController();
setError("");
const url = new URL(
`${import.meta.env.BASE_URL.replace(/\/$/, "")}/assets/cookbook-config/index.json`,
window.location.origin,
);
api
.loadIndex(url.href, { signal: controller.signal })
.then((data) => {
if (!data.models.some((model) => model.deployments.length)) {
throw new Error("No cookbook deployments are available. Check the catalog and retry.");
}
setIndex(data);
const requested = new URL(window.location.href).searchParams.get("deployment");
const requestedModel = data.models.find((model) =>
model.deployments.some((deployment) => deployment.id === requested),
);
if (!modelId && requestedModel) {
const page = recipePages.find((page) => page.id === requestedModel.id);
if (page) {
window.location.replace(`${sitePath(page.path)}${window.location.search}`);
return;
}
}
const deployments = data.models
.filter((model) => !modelId || model.id === modelId)
.flatMap((model) => model.deployments);
setSelected(deployments.some((item) => item.id === requested) ? requested! : (deployments[0]?.id ?? ""));
})
.catch((error) => {
if (!controller.signal.aborted) setError(errorText(error));
});
return () => controller.abort();
}, [retry, modelId]);
useEffect(() => {
if (!index || !selected || !modelId || !recipePages.some((page) => page.id === modelId)) return;
const controller = new AbortController();
setCatalog(undefined);
setError("");
api
.loadDeployment(index, selected, { signal: controller.signal })
.then((data) => {
if (!controller.signal.aborted) setCatalog(data);
})
.catch((error) => {
if (!controller.signal.aborted) setError(errorText(error));
});
return () => controller.abort();
}, [index, selected, modelId]);
function select(value: string) {
setSelected(value);
setCatalog(undefined);
setError("");
const url = new URL(window.location.href);
url.searchParams.set("deployment", value);
window.history.replaceState(null, "", url);
}
const model = index?.models.find((model) => model.id === modelId);
if (!modelId) return <RecipeDirectory index={index} error={error} retry={() => setRetry(retry + 1)} />;
const directoryModel = modelDirectory.find((model) => model.id === modelId);
if (directoryModel && !directoryModel.configurable)
return mounted ? (
<SourceRecipe model={directoryModel} />
) : (
<div className="cookbook not-prose recipe-detail">
<ModelHeader model={directoryModel} />
<p>{directoryModel.description}</p>
<p className="recipe-loading">Loading recipe…</p>
<a href={`https://github.com/hao-ai-lab/FastVideo/blob/main/${directoryModel.recipes[0].source}`}>
View maintained example ↗
</a>
</div>
);
return (
<div className="cookbook not-prose recipe-detail">
{error ? (
<div role="alert" className="space-y-3 rounded-lg border p-4">
<p>{error}</p>
<Button
variant="outline"
onClick={() => {
setCatalog(undefined);
setIndex(undefined);
setRetry(retry + 1);
}}
>
Retry loading recipes
</Button>
</div>
) : catalog?.id === selected ? (
<Editor key={catalog.id} catalog={catalog} deployments={model?.deployments ?? []} selectRuntime={select} />
) : (
<div className="recipe-loading">
{directoryModel && <ModelHeader model={directoryModel} />}
<div className="option-chips" role="group" aria-label="Hardware backend">
{model?.deployments.map((deployment) => (
<button
key={deployment.id}
type="button"
aria-pressed={selected === deployment.id}
onClick={() => select(deployment.id)}
>
{deployment.runtime.includes("mlx") ? "MLX" : "CUDA"}
</button>
))}
</div>
<p role="status">Loading deployment settings…</p>
</div>
)}
</div>
);
}
function RecipeDirectory({ index, error, retry }: { index?: Index; error: string; retry: () => void }) {
const [query, setQuery] = useState("");
const [category, setCategory] = useState("All models");
const [family, setFamily] = useState("All families");
const visible = modelDirectory.filter(
(model) =>
(category === "All models" || model.category === category) &&
(family === "All families" || model.family === family) &&
`${model.title} ${model.family} ${model.tags.join(" ")}`.toLowerCase().includes(query.trim().toLowerCase()),
);
return (
<div className="cookbook not-prose recipe-directory">
<div className="recipe-directory-tools">
<div className="recipe-family-filters" aria-label="Filter recipes by category">
{["All models", "Video", "Image", "Audio", "World"].map((value) => (
<button
key={value}
type="button"
aria-pressed={category === value}
onClick={() => {
setCategory(value);
setFamily("All families");
}}
>
{value}
</button>
))}
</div>
<Input
aria-label="Search recipes"
type="search"
placeholder="Search models…"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<div className="recipe-directory-subbar">
<p role="status">{visible.length} models</p>
<select aria-label="Model family" value={family} onChange={(event) => setFamily(event.target.value)}>
<option>All families</option>
{[
...new Set(
modelDirectory
.filter((model) => category === "All models" || model.category === category)
.map((model) => model.family),
),
].map((family) => (
<option key={family}>{family}</option>
))}
</select>
</div>
{error && (
<div role="alert" className="recipe-error">
<p>{error}</p>
<Button variant="outline" onClick={retry}>
Retry loading recipes
</Button>
</div>
)}
<div className="recipe-card-grid">
{visible.map((model) => (
<a className="recipe-card" key={model.id} href={sitePath(model.path)}>
<div className="recipe-card-top">
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="40" height="40" />
<span className="recipe-card-kicker">{model.provider}</span>
{model.featured && <span className="recipe-featured">Featured</span>}
</div>
<h2>{model.title}</h2>
<div className="recipe-tags">
<span>{model.category}</span>
{model.tags.slice(0, 2).map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
<footer>
<span>
{model.platforms.map((platform) => (platform === "Apple Silicon" ? "MLX" : platform)).join(" · ")}
</span>
<span>{model.configurable ? "Configure →" : "View recipe →"}</span>
</footer>
</a>
))}
</div>
{!visible.length && (
<div className="recipe-empty">
<h2>No matching recipes</h2>
<Button
variant="outline"
onClick={() => {
setQuery("");
setCategory("All models");
setFamily("All families");
}}
>
Clear filters
</Button>
</div>
)}
</div>
);
}
+114
View File
@@ -0,0 +1,114 @@
import { useId, useState } from "react";
import { families } from "@/lib/fast-models";
export const client = "load";
const local = (path: string) => `${import.meta.env.BASE_URL.replace(/\/$/, "")}${path}`;
export default function FastModels() {
const [family, setFamily] = useState("All models");
const [query, setQuery] = useState("");
const id = useId();
const visible = families
.map((group) => ({
...group,
models: group.models.filter(
(model) =>
(family === "All models" || family === group.name) &&
`${group.name} ${group.summary} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()),
),
}))
.filter((group) => group.models.length);
const count = visible.reduce((total, group) => total + group.models.length, 0);
return (
<div className="models-page not-prose">
<section className="model-families" aria-label="Model families">
{families.map((group) => (
<article className="model-family" key={group.name}>
<a href={group.collection} className="model-family-visual" aria-label={`${group.name} collection`}>
<span aria-hidden="true">{group.mark}</span>
<img src={local("/icon.svg")} alt="" width="24" height="24" />
</a>
<div className="model-family-body">
<h2>{group.name}</h2>
<p>{group.summary}</p>
<p className="model-family-detail">{group.detail}</p>
<div className="model-family-links">
<a href={group.collection}>
View collection <span aria-hidden="true">↗</span>
</a>
<a href={local(group.guide)}>
{group.name === "FastMetal" ? "MLX setup" : "Recipes and setup"} <span aria-hidden="true">→</span>
</a>
</div>
</div>
</article>
))}
</section>
<section className="model-catalog" aria-labelledby="checkpoint-catalog">
<header className="model-catalog-heading">
<div>
<h2 id="checkpoint-catalog">Browse checkpoints</h2>
<p>Find the export your runtime needs.</p>
</div>
<a href="https://huggingface.co/collections/FastVideo/fastvideo-fasth3">
Earlier FastH3 releases <span aria-hidden="true">↗</span>
</a>
</header>
<div className="model-catalog-controls">
<div className="model-filters" role="group" aria-label="Filter by model family">
{["All models", ...families.map((group) => group.name)].map((name) => (
<button key={name} type="button" aria-pressed={family === name} onClick={() => setFamily(name)}>
{name}
</button>
))}
</div>
<div className="model-search">
<label htmlFor={id}>Search checkpoints</label>
<input
id={id}
type="search"
placeholder="Name, precision, or runtime…"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</div>
</div>
<p className="model-result-count" role="status">
{count} {count === 1 ? "checkpoint" : "checkpoints"}
</p>
{visible.map((group) => (
<section className="model-checkpoints" key={group.name} aria-labelledby={`catalog-${group.name}`}>
<h3 id={`catalog-${group.name}`}>{group.name}</h3>
<ul>
{group.models.map((model) => (
<li key={model.id}>
<div>
<a href={model.url}>{model.id.replace(/^FastVideo-/, "").replace(/-Diffusers$/, "")}</a>
<code>FastVideo/{model.id}</code>
</div>
<a className="model-checkpoint-link" href={model.url} aria-label={`Open ${model.id} on Hugging Face`}>
Hugging Face <span aria-hidden="true">↗</span>
</a>
</li>
))}
</ul>
</section>
))}
{!count && (
<div className="model-empty">
<p>No checkpoints match these filters.</p>
<button
type="button"
onClick={() => {
setQuery("");
setFamily("All models");
}}
>
Clear filters
</button>
</div>
)}
</section>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
import { useEffect, useState } from "react";
export const client = "load";
const repository = "https://github.com/hao-ai-lab/FastVideo";
const endpoint = "https://api.github.com/repos/hao-ai-lab/FastVideo";
const compact = new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 });
const exact = new Intl.NumberFormat("en");
export default function GitHubStars() {
const [stars, setStars] = useState<number | null>(null);
useEffect(() => {
const controller = new AbortController();
async function refresh() {
try {
const response = await fetch(endpoint, {
headers: { Accept: "application/vnd.github+json" },
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
});
if (!response.ok) return;
const data = await response.json();
if (Number.isSafeInteger(data.stargazers_count) && data.stargazers_count >= 0 && !controller.signal.aborted) {
setStars(data.stargazers_count);
}
} catch {
// Keep the link usable when offline or when the public API is rate limited.
}
}
void refresh();
const timer = setInterval(
() => {
if (document.visibilityState === "visible") void refresh();
},
5 * 60 * 1000,
);
return () => {
controller.abort();
clearInterval(timer);
};
}, []);
const label = stars === null ? "Star FastVideo on GitHub" : `FastVideo on GitHub, ${exact.format(stars)} stars`;
return (
<a className="home-github-stars" href={repository} aria-label={label} title={label}>
<svg width="17" height="17" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.49c-2.23.48-2.7-1.08-2.7-1.08-.36-.92-.89-1.17-.89-1.17-.73-.5.06-.49.06-.49.8.06 1.22.82 1.22.82.71 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.96 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.65 7.65 0 0 1 8 3.72c.68 0 1.36.09 2 .27 1.53-1.03 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.28.82 2.15 0 3.08-1.87 3.75-3.65 3.95.29.25.54.74.54 1.49v2.21c0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" />
</svg>
<svg className="home-star" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="m12 2.5 2.94 5.96 6.58.96-4.76 4.64 1.12 6.55L12 17.52l-5.88 3.09 1.12-6.55L2.48 9.42l6.58-.96L12 2.5Z" />
</svg>
<span>{stars === null ? "Star" : compact.format(stars).toLowerCase()}</span>
</a>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { modelDirectory, sitePath } from "@/lib/cookbook-pages";
/** Family runbooks link to the same model pages as the cookbook catalog. */
export default function RecipeFamily({ family }: { family: string }) {
const models = modelDirectory.filter(
(model) =>
model.recipes.some((recipe) => recipe.family === family) ||
(family === "minimax_h3" && model.family === "FastH3"),
);
return (
<div className="recipe-directory not-prose">
<a className="recipe-back" href={sitePath("/cookbook")}>
← All models
</a>
<div className="recipe-card-grid">
{models.map((model) => (
<a className="recipe-card" key={model.id} href={sitePath(model.path)}>
<div className="recipe-card-top">
<img src={sitePath(`/assets/logos/${model.logo}.webp`)} alt="" width="40" height="40" />
<span className="recipe-card-kicker">{model.provider}</span>
</div>
<h2>{model.title}</h2>
<div className="recipe-tags">
<span>{model.category}</span>
{model.tags.slice(0, 2).map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
<footer>
<span>
{model.platforms.map((platform) => (platform === "Apple Silicon" ? "MLX" : platform)).join(" · ")}
</span>
<span>{model.configurable ? "Configure →" : "View recipe →"}</span>
</footer>
</a>
))}
</div>
</div>
);
}
+526
View File
@@ -0,0 +1,526 @@
import { useEffect, useState } from "react";
import { Tabs } from "radix-ui";
import { parse, stringify } from "yaml";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import ModelHeader from "@/components/cookbook/ModelHeader";
import HardwarePicker from "@/components/cookbook/HardwarePicker";
import { configAndLaunch, fileAndLaunch, sdkCommand, pythonSource } from "@/lib/cookbook-commands";
import { sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
import { api } from "@/lib/cookbook";
interface Control {
name: string;
label: string;
value: string | number | boolean;
spans?: number[][];
path?: string[];
flag?: string;
integer?: boolean;
booleanOptional?: boolean;
choices?: (string | number | { value: string | number; label: string })[];
}
interface Example {
source: string;
controls: Control[];
}
const labels: Record<string, string> = {
prompt: "Prompt",
num_gpus: "GPU count",
height: "Height",
width: "Width",
num_frames: "Frames",
seed: "Seed",
num_inference_steps: "Inference steps",
guidance_scale: "Guidance scale",
fps: "FPS",
host: "Host",
port: "Port",
served_model_name: "Served model name",
};
/** Every source page uses the model editor; settings modify the maintained example itself. */
export default function SourceRecipe({ model }: { model: DirectoryModel }) {
const [selected, setSelected] = useState(model.recipes[0].id);
const [mode, setMode] = useState("sdk");
const [loaded, setLoaded] = useState<{ url: string; example: Example }>();
const [notice, setNotice] = useState("");
const [showEnv, setShowEnv] = useState(false);
const [showClient, setShowClient] = useState(false);
const [outputTab, setOutputTab] = useState("commands");
const [error, setError] = useState("");
const [edits, setEdits] = useState<Record<number, string | number | boolean>>({});
const [hardware, setHardware] = useState("unspecified");
useEffect(() => {
setHardware(new URL(window.location.href).searchParams.get("hardware") ?? "unspecified");
}, []);
const recipe = model.recipes.find((recipe) => recipe.id === selected)!;
const yamlSource =
mode === "serving" ? recipe.serving?.source : /\.ya?ml$/.test(recipe.source) ? recipe.source : undefined;
const sourceUrl = sitePath(
yamlSource
? `/assets/cookbook-sources/${recipe.id}-${mode === "serving" ? "serving" : "inference"}.yaml`
: `/assets/cookbook-examples/${recipe.id}.json`,
);
const example = loaded?.url === sourceUrl ? loaded.example : undefined;
useEffect(() => {
const controller = new AbortController();
setError("");
fetch(sourceUrl, { signal: controller.signal })
.then(async (response) => {
if (!response.ok) throw new Error(`Example could not be loaded: HTTP ${response.status}.`);
let data: Example;
if (!yamlSource) data = await response.json();
else {
const source = await response.text();
const config = parse(source);
const controls: Control[] = [];
function visit(value: Record<string, unknown>, path: string[] = []) {
for (const [name, item] of Object.entries(value)) {
if (item && typeof item === "object" && !Array.isArray(item))
visit(item as Record<string, unknown>, [...path, name]);
else if (labels[name] && ["string", "number", "boolean"].includes(typeof item))
controls.push({
name,
label: labels[name],
value: item as Control["value"],
integer: !["guidance_scale", "audio_end_in_s"].includes(name),
path: [...path, name],
});
}
}
visit(config);
data = { source, controls };
}
if (!yamlSource) {
for (const knob of recipe.knobs ?? []) {
const field = data.controls.find((field) => field.name === knob.key);
if (field) field.choices = knob.options;
else
data.controls.push({
name: knob.key,
label: labels[knob.key] ?? knob.label,
value: knob.default,
flag: knob.flag,
spans: [],
choices: knob.options,
});
}
}
if (!controller.signal.aborted) setLoaded({ url: sourceUrl, example: data });
})
.catch((error) => {
if (!controller.signal.aborted) setError(error.message);
});
return () => controller.abort();
}, [sourceUrl, yamlSource, recipe]);
function selectMode(next: string) {
setMode(next);
if ((next === "serving") !== (mode === "serving")) setEdits({});
setNotice("");
}
async function copy(value: string) {
try {
await navigator.clipboard.writeText(value);
setNotice("Copied to clipboard.");
} catch {
setNotice("Copy failed. Select and copy the text below.");
}
}
let source = example?.source ?? "";
let failure = "";
for (const [key, value] of Object.entries(edits)) {
const field = example?.controls[Number(key)];
if (
field &&
typeof field.value === "number" &&
(!Number.isFinite(value) ||
((field.integer ?? Number.isInteger(field.value)) && !Number.isInteger(value)) ||
(field.name === "num_gpus" && Number(value) < 1))
)
failure = "Enter valid numeric settings before copying the example.";
}
if (example && yamlSource) {
const config = parse(source);
for (const [key, value] of Object.entries(edits)) {
const path = example.controls[Number(key)].path!;
const parent = path.slice(0, -1).reduce((item, name) => item[name], config);
parent[path.at(-1)!] = value;
}
source = stringify(config);
} else if (example) {
source = pythonSource(source, example.controls, edits);
}
const baselineCommand =
mode === "serving" && recipe.serving
? `${recipe.serving.env ? `${recipe.serving.env} ` : ""}fastvideo serve --config ${recipe.serving.source}`
: recipe.command;
const localFile = recipe.source.replace(/[^/]+$/, `cookbook_${recipe.id.replaceAll("-", "_")}.py`);
const command = yamlSource
? baselineCommand.replace(/--config\s+\S+/, "--config config.yaml")
: baselineCommand.replace(recipe.source, localFile) +
Object.entries(edits)
.filter(([key]) => example?.controls[Number(key)].flag)
.map(([key, value]) => {
const field = example!.controls[Number(key)];
return typeof value === "boolean" && field.booleanOptional
? ` ${value ? field.flag : field.flag!.replace(/^--/, "--no-")}`
: ` ${field.flag} '${String(value).replaceAll("'", "'\"'\"'")}'`;
})
.join("");
const sdk = mode === "sdk" && !!yamlSource;
const config = sdk && source ? parse(source) : undefined;
const displayedSource = config
? stringify({ generator: config.generator, request: config.request ?? config.default_request })
: source;
const launch = sdk ? command.replace(/fastvideo generate --config config\.yaml$/, sdkCommand) : command;
const ready = !!example && !failure;
const clientCommand =
mode === "serving" && ready
? api.sampleRequest(
{
runtime: { interface: "rest", server_defaults: { host: "127.0.0.1", port: 8000 } },
model: { id: model.model },
workload: "t2v",
},
parse(source),
).command
: undefined;
const launchScript = yamlSource
? configAndLaunch(displayedSource, launch)
: fileAndLaunch(localFile, displayedSource, launch);
const environment = [...baselineCommand.matchAll(/(?:^|\s)([A-Z][A-Z0-9_]*=(?:'[^']*'|"[^"]*"|[^\s]+))/g)]
.map((match) => match[1])
.join("\n");
const mlx = recipe.gpu_types.includes("Apple Silicon");
const devices = mlx
? [["apple", "Apple Silicon", "Unified memory"]]
: [
["b200", "B200", "Data center"],
["h100", "H100", "Data center"],
["rtx5090", "RTX 5090", "GeForce"],
["rtx", "RTX PRO 6000", "Workstation"],
["spark", "DGX Spark", "GB10"],
];
const chooseHardware = (value: string) => {
setHardware(value);
const url = new URL(window.location.href);
url.searchParams.set("hardware", value);
window.history.replaceState(null, "", url);
};
const control = (field: Control, index: number) => {
const value = Object.hasOwn(edits, index) ? edits[index] : field.value;
return (
<div className="matrix-field" key={index}>
{field.choices || typeof value === "boolean" ? (
<span className="matrix-field-label">{field.label}</span>
) : (
<label htmlFor={`source-${index}`} className="matrix-field-label">
{field.label}
</label>
)}
{field.choices ? (
<div role="group" aria-label={field.label} className="option-chips">
{field.choices.map((option) => {
const choice = typeof option === "object" ? option.value : option;
return (
<button
type="button"
key={String(choice)}
aria-pressed={value === choice}
onClick={() => setEdits({ ...edits, [index]: choice })}
>
{typeof option === "object" ? option.label : String(option)}
</button>
);
})}
</div>
) : typeof value === "boolean" ? (
<div role="group" aria-label={field.label} className="option-chips">
{[true, false].map((choice) => (
<button
type="button"
key={String(choice)}
aria-pressed={value === choice}
onClick={() => setEdits({ ...edits, [index]: choice })}
>
{choice ? "On" : "Off"}
</button>
))}
</div>
) : (
<Input
id={`source-${index}`}
type={typeof field.value === "number" ? "number" : "text"}
value={typeof value === "number" && Number.isNaN(value) ? "" : String(value)}
step={typeof field.value === "number" && (field.integer ?? Number.isInteger(field.value)) ? 1 : "any"}
onChange={(event) =>
setEdits({
...edits,
[index]:
typeof field.value === "number"
? event.target.value === ""
? NaN
: Number(event.target.value)
: event.target.value,
})
}
/>
)}
</div>
);
};
return (
<div className="cookbook not-prose recipe-detail">
<ModelHeader
model={model}
actions={
<>
<Button variant="outline" disabled={!ready} onClick={() => copy(displayedSource)}>
Copy {yamlSource ? "YAML" : "Python"}
</Button>
<Button
disabled={!ready}
onClick={() => {
const url = URL.createObjectURL(new Blob([displayedSource], { type: "text/plain" }));
const link = document.createElement("a");
link.href = url;
link.download = yamlSource ? "config.yaml" : localFile.split("/").at(-1)!;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}}
>
Download {yamlSource ? "YAML" : "Python"}
</Button>
</>
}
/>
<div className="recipe-run-modes option-chips" role="group" aria-label="Run mode">
{["sdk", "inference", ...(recipe.serving ? ["serving"] : [])].map((entry) => (
<button type="button" key={entry} aria-pressed={mode === entry} onClick={() => selectMode(entry)}>
{entry === "sdk" ? "Python SDK" : entry === "inference" ? "CLI" : "Serving"}
</button>
))}
</div>
<section className="recipe-command-box" aria-label="Source recipe">
<Tabs.Root value={outputTab} onValueChange={setOutputTab}>
<div className="recipe-command-toolbar">
<span className="recipe-unverified">Unverified recipe</span>
<div className="recipe-command-tools">
<button type="button" disabled={!ready} onClick={() => copy(launchScript)}>
Copy
</button>
{mode === "serving" && (
<button
type="button"
disabled={!clientCommand}
aria-expanded={showClient}
aria-controls="source-client-command"
onClick={() => {
setOutputTab("commands");
setShowClient(!showClient);
}}
>
cURL
</button>
)}
{environment && (
<button type="button" aria-pressed={showEnv} onClick={() => setShowEnv(!showEnv)}>
Env
</button>
)}
</div>
<Tabs.List className="recipe-tabs" aria-label="Output format">
<Tabs.Trigger value="commands">Commands</Tabs.Trigger>
<Tabs.Trigger value="source">{yamlSource ? "config.yaml" : "Python example"}</Tabs.Trigger>
</Tabs.List>
</div>
{showEnv && (
<pre className="recipe-environment" tabIndex={0} aria-label="Environment variables">
<code>{environment}</code>
</pre>
)}
{(error || failure) && (
<p className="recipe-error" role="alert">
{error || failure}
</p>
)}
<Tabs.Content value="commands" className="recipe-command-content">
<div className="recipe-code-heading">
<span>{mode === "serving" ? "Serve" : mode === "sdk" ? "Python SDK" : "Generate"}</span>
</div>
<pre tabIndex={0} aria-label={mode === "serving" ? "Server command" : "Inference command"}>
<code>{launch}</code>
</pre>
<p className="matrix-note">Includes your settings. Run from an installed FastVideo checkout.</p>
{clientCommand && (
<details
className="recipe-request-code"
id="source-client-command"
open={showClient}
onToggle={(event) => setShowClient(event.currentTarget.open)}
>
<summary>Example request</summary>
<button type="button" onClick={() => copy(clientCommand)}>
Copy client command
</button>
<pre tabIndex={0} aria-label="Client command">
<code>{clientCommand}</code>
</pre>
</details>
)}
</Tabs.Content>
<Tabs.Content value="source" className="recipe-command-content">
<pre tabIndex={0} aria-label="Source example">
<code>{displayedSource || "Loading example…"}</code>
</pre>
</Tabs.Content>
</Tabs.Root>
<p role="status" className="recipe-copy-status">
{notice}
</p>
</section>
<details className="recipe-install">
<summary>
Installation and setup <span>{mlx ? "MLX" : "CUDA"}</span>
</summary>
{mode === "serving" && recipe.serving && (
<pre>
<code>{recipe.serving.install}</code>
</pre>
)}
{mode === "serving" && recipe.serving?.prepare && (
<>
<pre>
<code>{recipe.serving.prepare}</code>
</pre>
<Button variant="outline" onClick={() => copy(recipe.serving!.prepare!)}>
Copy preparation commands
</Button>
</>
)}
<a href={sitePath(model.familyPath)}>Model setup guide ↗</a>
</details>
<section className="recipe-matrix" aria-label="Configuration settings">
<div className="matrix-row">
<h2>Hardware</h2>
<div className="matrix-row-content">
<div className="hardware-backends option-chips" role="group" aria-label="Hardware backend">
{[
...new Set(model.recipes.map((entry) => (entry.gpu_types.includes("Apple Silicon") ? "MLX" : "CUDA"))),
].map((platform) => (
<button
type="button"
key={platform}
aria-pressed={platform === (mlx ? "MLX" : "CUDA")}
onClick={() => {
if (platform === (mlx ? "MLX" : "CUDA")) return;
setNotice("");
const target = model.recipes.find((entry) =>
entry.gpu_types.includes(platform === "MLX" ? "Apple Silicon" : "NVIDIA"),
)!;
setSelected(target.id);
setMode("sdk");
setEdits({});
}}
>
{platform}
</button>
))}
</div>
<div className="hardware-resource-grid">
<HardwarePicker
backend={mlx ? "mlx" : "nvidia"}
options={devices}
value={
hardware.startsWith("custom:")
? hardware
: (devices.find(([id]) => id === hardware)?.[0] ?? devices[0][0])
}
onChange={chooseHardware}
/>
<div className="matrix-resources">
<span className="matrix-resource-title">Resources</span>
{example?.controls.map((field, i) => (field.name === "num_gpus" ? control(field, i) : null))}
{!example?.controls.some((field) => field.name === "num_gpus") && (
<p>{mlx ? "1 Apple Silicon host" : `${recipe.hardware?.gpu_count ?? 1} GPU in the source example`}</p>
)}
</div>
</div>
<p className="matrix-note">
Hardware fit has not been verified. Settings edit the source example; device choice is a preference.
</p>
</div>
</div>
{model.recipes.filter((entry) => entry.gpu_types.includes(mlx ? "Apple Silicon" : "NVIDIA")).length > 1 && (
<div className="matrix-row">
<h2>Recipe</h2>
<div className="matrix-row-content option-chips" role="group" aria-label="Recipe variant">
{model.recipes
.filter((entry) => entry.gpu_types.includes(mlx ? "Apple Silicon" : "NVIDIA"))
.map((entry) => (
<button
type="button"
key={entry.id}
aria-pressed={selected === entry.id}
onClick={() => {
setNotice("");
setSelected(entry.id);
setMode("sdk");
setEdits({});
}}
>
{entry.label}
</button>
))}
</div>
</div>
)}
<div className="matrix-row">
<h2>Generation settings</h2>
<div className="matrix-row-content">
<div className="matrix-options-grid">
{example?.controls.map((field, i) =>
field.name !== "num_gpus" && field.path?.[0] !== "server" ? control(field, i) : null,
)}
</div>
{example && !example.controls.length && (
<p className="matrix-note">
This example uses its model defaults. Open the Python example to customize its pipeline.
</p>
)}
</div>
</div>
{example?.controls.some((field) => field.path?.[0] === "server") && (
<details className="matrix-advanced">
<summary>
Advanced <span>Server settings</span>
</summary>
<div className="matrix-row">
<h2>Server</h2>
<div className="matrix-row-content matrix-options-grid">
{example.controls.map((field, i) => (field.path?.[0] === "server" ? control(field, i) : null))}
</div>
</div>
</details>
)}
</section>
<div className="recipe-bottom-actions">
<a
href={`https://github.com/hao-ai-lab/FastVideo/blob/main/${mode === "serving" ? recipe.serving?.source : recipe.source}`}
>
View original example ↗
</a>
<Button
variant="ghost"
onClick={() => {
setEdits({});
setNotice("Restored the baseline example.");
}}
>
Reset changes
</Button>
</div>
</div>
);
}
+71
View File
@@ -0,0 +1,71 @@
import { stringify } from "yaml";
import type { Result, Value } from "./cookbook";
export function pythonSource(
source: string,
controls: { value: string | number | boolean; spans?: number[][] }[],
edits: Record<number, string | number | boolean> = {},
) {
const replacements = controls.flatMap((field, index) =>
(field.spans ?? []).map(([start, end]) => {
const value = Object.hasOwn(edits, index) ? edits[index] : field.value;
return { start, end, text: typeof value === "boolean" ? (value ? "True" : "False") : JSON.stringify(value) };
}),
);
for (const replacement of replacements.sort((a, b) => b.start - a.start))
source = source.slice(0, replacement.start) + replacement.text + source.slice(replacement.end);
return source;
}
/** Quoted heredocs preserve prompts literally, including shell substitutions. */
export function configAndLaunch(yaml: string, command: string) {
return fileAndLaunch("config.yaml", yaml, command);
}
export function fileAndLaunch(filename: string, content: string, command: string) {
let delimiter = "FASTVIDEO_CONFIG";
while (content.split("\n").includes(delimiter)) delimiter += "_";
return `(\nset -e\ncat > ${filename} <<'${delimiter}'\n${content.trimEnd()}\n${delimiter}\n${command}\n)`;
}
export const sdkCommand = `python - <<'FASTVIDEO_SDK'
import yaml
from fastvideo import VideoGenerator
with open("config.yaml") as file:
config = yaml.safe_load(file)
generator = VideoGenerator.from_config(config["generator"])
try:
generator.generate(config["request"])
finally:
generator.shutdown()
FASTVIDEO_SDK`;
/** The CUDA CLI uses the same generator with request instead of server defaults. */
export function inferenceOutput(result: Result, imagePath?: string) {
const defaults = (result.config.default_request ?? {}) as Record<string, Value>;
const output = (defaults.output ?? {}) as Record<string, Value>;
const generator = result.config.generator as Record<string, Value>;
const pipeline = generator.pipeline as Record<string, Value> | undefined;
const inputs = { ...((defaults.inputs ?? {}) as Record<string, Value>) };
if (imagePath !== undefined) inputs.image_path = imagePath.trim();
const images = Array.isArray(inputs.image_path) ? inputs.image_path : [inputs.image_path];
if (pipeline?.workload_type === "i2v" && !images.some((image) => typeof image === "string" && image.trim())) {
throw new Error("Enter a source image path before copying a CLI or Python SDK example.");
}
return {
yaml: stringify({
generator: result.config.generator,
request: {
...defaults,
...(Object.keys(inputs).length ? { inputs } : {}),
prompt: defaults.prompt ?? "A river flowing through a peaceful forest",
output: { ...output, save_video: true, return_frames: false, output_path: "outputs/cookbook/" },
},
}),
command: result.command.replace(
/\bfastvideo serve --config config\.yaml$/,
"fastvideo generate --config config.yaml",
),
};
}
+151
View File
@@ -0,0 +1,151 @@
import sourceCatalog from "../../docs/assets/cookbook-recipes.json" with { type: "json" };
/** Presentation metadata only. Deployments and editable fields come from the cookbook backend. */
export const recipePages = [
{
id: "fasth3-8step",
family: "FastH3",
title: "FastH3 8-Step V2",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-8step",
description: "Distilled MiniMax-H3 video generation with synchronized audio.",
tags: ["Text to video", "Audio", "8-step"],
featured: true,
},
{
id: "fasth3-trim",
family: "FastH3",
title: "FastH3 Trim",
mark: "H3",
path: "/cookbook/minimax-h3/fasth3-trim",
description: "The smaller FastH3 model for eight-step video generation with synchronized audio.",
tags: ["Text to video", "Audio", "NVFP4", "8-step"],
},
{
id: "fastwan21",
family: "Wan",
title: "FastWan2.1 1.3B",
mark: "Wan",
path: "/cookbook/wan/fastwan21",
description: "A compact, distilled Wan model for text-to-video generation.",
tags: ["Text to video", "1.3B", "3-step"],
},
{
id: "wan21-i2v",
family: "Wan",
title: "Wan2.1 I2V 14B",
mark: "Wan",
path: "/cookbook/wan/wan21-i2v",
description: "Turn a reference image into video with Wan2.1.",
tags: ["Image to video", "14B", "480p"],
},
{
id: "ltx2-distilled",
family: "LTX",
title: "LTX2 Distilled",
mark: "LTX",
path: "/cookbook/ltx/ltx2-distilled",
description: "Generate video with incremental delivery over WebSocket.",
tags: ["Text to video", "Streaming"],
},
];
export type RecipePage = (typeof recipePages)[number];
export function sitePath(path: string) {
return `${import.meta.env.BASE_URL.replace(/\/$/, "")}${path}`;
}
export interface SourceRecipe {
id: string;
family: string;
task: string;
label: string;
summary: string;
model: string;
source: string;
command: string;
gpu_types: string[];
hardware?: { gpu_count?: number; platform?: string; accelerator?: string };
serving?: { source: string; install: string; env?: string; prepare?: string };
knobs?: {
key: string;
label: string;
hint?: string;
flag: string;
default: string | number;
options: (string | number | { value: string | number; label: string })[];
}[];
}
export const sourceRecipes = sourceCatalog.recipes as SourceRecipe[];
const familyInfo: Record<string, { title: string; path: string; logo: string; category: string }> = {
minimax_h3: { title: "MiniMax H3", path: "minimax-h3", logo: "minimax", category: "Video" },
wan: { title: "Wan", path: "wan", logo: "wan-ai", category: "Video" },
ltx2: { title: "LTX", path: "ltx", logo: "ltx", category: "Video" },
hunyuan: { title: "Hunyuan", path: "hunyuan", logo: "tencent-hunyuan", category: "Video" },
kandinsky5: { title: "Kandinsky 5", path: "kandinsky5", logo: "kandinsky", category: "Video" },
kandinsky6: { title: "Kandinsky 6", path: "kandinsky6", logo: "kandinsky", category: "Video" },
flux: { title: "FLUX", path: "flux", logo: "black-forest-labs", category: "Image" },
sd35: { title: "Stable Diffusion", path: "stable-diffusion", logo: "stabilityai", category: "Image" },
stable_audio: { title: "Stable Audio", path: "stable-audio", logo: "stabilityai", category: "Audio" },
mmaudio: { title: "MMAudio", path: "mmaudio", logo: "fastvideo", category: "Audio" },
cosmos: { title: "Cosmos", path: "cosmos", logo: "nvidia", category: "World" },
matrixgame: { title: "Matrix Game", path: "matrix-game", logo: "fastvideo", category: "World" },
};
const backendModels: Record<string, string> = {
"FastVideo/FastVideo-FastH3-8-Step-V2": "fasth3-8step",
"FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4": "fasth3-trim",
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers": "fastwan21",
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": "wan21-i2v",
"FastVideo/LTX2-Distilled-Diffusers": "ltx2-distilled",
};
export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.model))]
.map((model) => {
const recipes = sourceRecipes.filter((recipe) => recipe.model === model);
const first = recipes[0];
const family = familyInfo[first.family];
const backend = recipePages.find((page) => page.id === backendModels[model]);
const ownModel = model.startsWith("FastVideo/Fast");
const logo = ownModel ? "fastvideo" : family.logo;
const providers: Record<string, string> = {
fastvideo: "FastVideo",
minimax: "MiniMax",
"wan-ai": "Wan AI",
ltx: "Lightricks",
"tencent-hunyuan": "Tencent",
kandinsky: "Kandinsky Lab",
"black-forest-labs": "Black Forest Labs",
stabilityai: "Stability AI",
nvidia: "NVIDIA",
};
return {
id: backend?.id ?? first.id,
model,
recipes,
title:
backend?.title ??
(model.includes("FastH3-4-step")
? "FastH3 4-Step V1"
: model.includes("FastH3-Preview-v0.2")
? "FastH3 Preview V0.2"
: model === "MiniMaxAI/MiniMax-H3"
? "MiniMax H3"
: first.label.split(" on ")[0]),
family: first.family === "minimax_h3" ? "FastH3" : (backend?.family ?? family.title),
familyPath: `/cookbook/${family.path}`,
path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`,
logo,
provider: providers[logo],
category: family.category,
description: backend?.description ?? first.summary,
tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))],
platforms: [...new Set(recipes.flatMap((recipe) => recipe.gpu_types))],
configurable: !!backend,
featured: backend?.featured ?? false,
};
})
.sort(
(a, b) =>
Number(b.featured) - Number(a.featured) ||
Number(b.family === "FastH3") - Number(a.family === "FastH3") ||
Number(b.configurable) - Number(a.configurable),
);
export type DirectoryModel = (typeof modelDirectory)[number];
+107
View File
@@ -0,0 +1,107 @@
// The public JavaScript contract lives in docs/cookbook/design.md.
// @ts-expect-error The shared ES module has no TypeScript declarations.
import * as shared from "../../docs/js/cookbook-config.mjs";
export type Value = null | boolean | number | string | Value[] | { [key: string]: Value };
export interface Schema {
title?: string;
description?: string;
type?: string | string[];
enum?: Value[];
anyOf?: Schema[];
minimum?: number;
maximum?: number;
multipleOf?: number;
}
export interface Field {
path: string;
schema: Schema;
value?: Value;
}
export interface Index {
url: string;
models: {
id: string;
title: string;
model_id: string;
deployments: {
id: string;
label: string;
runtime: string;
workload: string;
}[];
}[];
}
export interface Catalog {
id: string;
title: string;
summary?: string;
workload: string;
env: Record<string, string>;
base_config: Record<string, Value>;
runtime: {
label: string;
backend: string;
interface: string;
install_url?: string;
client_guide_url?: string;
};
requirements?: string[];
guide?: { url: string; label?: string };
}
export interface Result {
config: Record<string, Value>;
yaml: string;
command: string;
clientCommand: string;
websocketUrl?: string;
}
interface Api {
sampleRequest(
catalog: {
runtime: { interface: string; server_defaults: { host: string; port: number } };
model: { id: string };
workload: string;
},
config: Record<string, Value>,
): { command: string };
loadIndex(url: string, options?: { signal?: AbortSignal }): Promise<Index>;
loadDeployment(index: Index, id: string, options?: { signal?: AbortSignal }): Promise<Catalog>;
getOptions(catalog: Catalog, config?: Catalog["base_config"], edits?: Record<string, Value>): Field[];
resolveConfig(catalog: Catalog, edits?: Record<string, Value>): Result;
}
export const api = shared as Api;
export function types(schema: Schema): string[] {
return [
...new Set([
...(Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []),
...(schema.anyOf?.flatMap(types) ?? []),
]),
];
}
export function parseField(field: Field, raw: string): Value {
const kind = types(field.schema);
if (field.schema.enum || kind.includes("boolean") || kind.includes("object") || kind.includes("array")) {
return JSON.parse(raw) as Value;
}
if (kind.includes("number") || kind.includes("integer")) {
if (!raw.trim() || !Number.isFinite(Number(raw))) throw new Error("Enter a number.");
return Number(raw);
}
return raw;
}
/** Keep catalog guide links on the current site and its deployment prefix. */
export function guideUrl(path: string): string {
const url = new URL(
path,
new URL(`${import.meta.env.BASE_URL.replace(/\/$/, "")}/assets/cookbook-config/index.json`, window.location.origin),
);
if (url.origin === "https://haoailab.com" && url.pathname.startsWith("/FastVideo/")) {
url.host = window.location.host;
url.protocol = window.location.protocol;
}
return url.href;
}
+3
View File
@@ -0,0 +1,3 @@
interface ImportMeta {
readonly env: { readonly BASE_URL: string };
}
+104
View File
@@ -0,0 +1,104 @@
export const families = [
{
name: "FastH3",
mark: "H3",
summary: "Distilled MiniMax-H3 video with synchronized audio.",
collection: "https://huggingface.co/collections/FastVideo/fasth3-v2",
guide: "/cookbook/minimax-h3",
detail: "8-step V2 \u00b7 FP8 and NVFP4 exports",
models: [
{
id: "FastVideo-FastH3-8-Step-V2",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2",
},
{
id: "FastVideo-FastH3-8-Step-V2-NVFP4",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4",
},
{
id: "FastVideo-FastH3-8-Step-V2-FP8",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2-FP8",
},
{
id: "FastVideo-FastH3-8-Step-V2-NVFP4-Consumer",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer",
},
{
id: "FastVideo-FastH3-Trim-8-Step",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-8-Step",
},
{
id: "FastVideo-FastH3-Trim-8-Step-NVFP4",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4",
},
{
id: "FastVideo-FastH3-Trim-8-Step-FP8",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-8-Step-FP8",
},
{
id: "FastVideo-FastH3-8-Step-V2-NVFP4-Comfy",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Comfy",
},
{
id: "FastVideo-FastH3-Trim-Comfy",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-Comfy",
},
{
id: "FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree",
url: "https://huggingface.co/FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree",
},
],
},
{
name: "FastWan",
mark: "Wan",
summary: "Distilled Wan models for text-to-video and image-to-video.",
collection: "https://huggingface.co/collections/FastVideo/fastwan",
guide: "/cookbook/wan",
detail: "1.3B, 5B, and 14B releases",
models: [
{
id: "FastWan2.1-T2V-1.3B-Diffusers",
url: "https://huggingface.co/FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
},
{
id: "FastWan2.1-T2V-14B-Diffusers",
url: "https://huggingface.co/FastVideo/FastWan2.1-T2V-14B-Diffusers",
},
{
id: "FastWan2.2-TI2V-5B-FullAttn-Diffusers",
url: "https://huggingface.co/FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
},
{
id: "FastWan-QAD-FP8-1.3B",
url: "https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B",
},
{
id: "CausalWan2.2-I2V-A14B-Preview-Diffusers",
url: "https://huggingface.co/FastVideo/CausalWan2.2-I2V-A14B-Preview-Diffusers",
},
],
},
{
name: "FastMetal",
mark: "Metal",
summary: "Quantized Wan checkpoints for Apple Silicon and MLX.",
collection: "https://huggingface.co/collections/FastVideo/fastmetal",
guide: "/getting_started/installation/mlx",
detail: "INT8 QAD \u00b7 1.3B, 5B, and 14B",
models: [
{
id: "FastMetal-1.3B-QAD",
url: "https://huggingface.co/FastVideo/FastMetal-1.3B-QAD",
},
{
id: "FastMetal-5B-QAD",
url: "https://huggingface.co/FastVideo/FastMetal-5B-QAD",
},
{
id: "FastMetal-14B-QAD",
url: "https://huggingface.co/FastVideo/FastMetal-14B-QAD",
},
],
},
];
File diff suppressed because one or more lines are too long
+499
View File
@@ -0,0 +1,499 @@
[
{
"label": "Welcome",
"href": "/"
},
{
"label": "Fast Models",
"href": "/fast-models",
"icon": "boxes"
},
{
"label": "Get started",
"icon": "rocket",
"items": [
{
"label": "Quick start",
"href": "/getting_started/quick_start"
},
{
"label": "Installation",
"href": "/getting_started/installation"
},
{
"label": "Hardware and runtimes",
"href": "/getting_started/hardware"
},
{
"label": "Python API basics",
"href": "/getting_started/v1_api"
}
]
},
{
"label": "Run inference",
"items": [
{
"label": "Inference guide",
"href": "/inference/inference_quick_start"
},
{
"label": "Configuration",
"href": "/inference/configuration"
},
{
"label": "CLI",
"href": "/inference/cli"
},
{
"label": "Supported models",
"href": "/inference/support_matrix"
},
{
"label": "Memory and offloading",
"href": "/inference/offloading"
},
{
"label": "Performance and attention",
"href": "/inference/optimizations"
},
{
"label": "LoRA adapters",
"href": "/utilities/lora"
},
{
"label": "Hardware",
"collapsed": true,
"items": [
{
"label": "Choose your runtime",
"href": "/getting_started/hardware"
},
{
"label": "NVIDIA GPU setup",
"href": "/getting_started/installation/gpu"
},
{
"label": "Apple Silicon / MLX",
"href": "/getting_started/installation/mlx"
},
{
"label": "NVIDIA DGX Spark",
"href": "/getting_started/installation/spark"
},
{
"label": "Two DGX Sparks",
"href": "/getting_started/installation/spark_pair"
},
{
"label": "RTX PRO 6000 / FastH3",
"href": "/inference/fasth3_rtx_pro_6000"
},
{
"label": "DGX Spark tuning",
"href": "/getting_started/installation/spark_performance"
},
{
"label": "AMD / ROCm",
"href": "/getting_started/installation/amd"
}
],
"display": "group"
},
{
"label": "Integrations",
"collapsed": true,
"items": [
{
"label": "ComfyUI",
"href": "/inference/comfyui"
},
{
"label": "Server and API clients",
"href": "/cookbook/openai-api"
}
],
"display": "group"
},
{
"label": "Examples",
"href": "/inference/examples/examples_inference_index"
}
]
},
{
"label": "Cookbook",
"collapsed": true,
"items": [
{
"label": "All recipes",
"href": "/cookbook"
},
{
"label": "H3 server and playground",
"href": "/cookbook/openai-api"
},
{
"label": "FastH3",
"display": "group",
"collapsed": false,
"items": [
{
"label": "FastH3 8-Step V2",
"href": "/cookbook/minimax-h3/fasth3-8step"
},
{
"label": "FastH3 Trim",
"href": "/cookbook/minimax-h3/fasth3-trim"
},
{
"label": "Recipes and setup",
"href": "/cookbook/minimax-h3"
},
{
"label": "Checkpoint schedules",
"href": "/inference/fasth3-distilled"
}
]
},
{
"label": "Wan",
"display": "group",
"collapsed": false,
"items": [
{
"label": "FastWan2.1 1.3B",
"href": "/cookbook/wan/fastwan21"
},
{
"label": "Wan2.1 I2V 14B",
"href": "/cookbook/wan/wan21-i2v"
},
{
"label": "Family guide",
"href": "/cookbook/wan"
}
]
},
{
"label": "LTX",
"display": "group",
"collapsed": true,
"items": [
{
"label": "LTX2 Distilled",
"href": "/cookbook/ltx/ltx2-distilled"
},
{
"label": "Recipes and setup",
"href": "/cookbook/ltx"
},
{
"label": "LTX-2.5 reference",
"href": "/inference/ltx2_5"
}
]
},
{
"label": "Hunyuan",
"href": "/cookbook/hunyuan"
},
{
"label": "Kandinsky 5",
"href": "/cookbook/kandinsky5"
},
{
"label": "Kandinsky 6",
"display": "group",
"collapsed": true,
"items": [
{
"label": "Recipes and setup",
"href": "/cookbook/kandinsky6"
},
{
"label": "Generation reference",
"href": "/inference/kandinsky6"
},
{
"label": "Upscaling reference",
"href": "/inference/kandinsky6_sr"
}
]
},
{
"label": "FLUX",
"href": "/cookbook/flux"
},
{
"label": "Stable Diffusion",
"href": "/cookbook/stable-diffusion"
},
{
"label": "Stable Audio",
"href": "/cookbook/stable-audio"
},
{
"label": "MMAudio",
"href": "/cookbook/mmaudio"
},
{
"label": "Cosmos",
"href": "/cookbook/cosmos"
},
{
"label": "Matrix Game",
"href": "/cookbook/matrix-game"
}
],
"display": "group",
"root": "/cookbook"
},
{
"label": "Train and fine-tune",
"display": "group",
"collapsed": true,
"items": [
{
"label": "Overview",
"href": "/training/overview"
},
{
"label": "Training Infrastructure",
"href": "/training/train_infra"
},
{
"label": "Experiment Tracking",
"href": "/training/trackers"
},
{
"label": "Data Preprocessing",
"href": "/training/data_preprocess"
},
{
"label": "Fine-tuning",
"href": "/training/finetune"
},
{
"label": "Attn-QAT Training",
"href": "/training/attn_qat"
},
{
"label": "HunyuanVideo 1.5 T2V",
"href": "/training/hunyuan15_overfit"
},
{
"label": "Examples",
"items": [
{
"label": "Training Index",
"href": "/training/examples/examples_training_index"
},
{
"label": "Wan T2V 1.3B",
"href": "/training/examples/wan_t2v_1.3B"
},
{
"label": "Wan I2V 14B 480p",
"href": "/training/examples/wan_i2v_14B_480p"
},
{
"label": "Wan2.1 Fun 1.3B InP",
"href": "/training/examples/Wan2.1-Fun-1.3B-InP"
},
{
"label": "Wan2.1 VSA",
"href": "/training/examples/Wan2.1-VSA"
},
{
"label": "Fine-tuning",
"href": "/training/examples/finetune"
}
],
"collapsed": true,
"display": "group"
}
]
},
{
"label": "Distill models",
"display": "group",
"collapsed": true,
"items": [
{
"label": "Data Preprocessing",
"href": "/distillation/data_preprocess"
},
{
"label": "DMD",
"href": "/distillation/dmd"
},
{
"label": "AnyFlow",
"href": "/distillation/anyflow"
}
]
},
{
"label": "Attention research",
"display": "group",
"collapsed": true,
"items": [
{
"label": "Overview",
"href": "/attention"
},
{
"label": "Video Sparse Attention",
"href": "/attention/vsa"
},
{
"label": "Sliding Tile Attention (Archived)",
"href": "/attention/sta"
},
{
"label": "Backend Development",
"href": "/contributing/attention_backend"
}
]
},
{
"label": "Contribute",
"display": "group",
"collapsed": true,
"items": [
{
"label": "Overview",
"href": "/contributing/overview"
},
{
"label": "Developer Environment",
"items": [
{
"label": "Index",
"href": "/contributing/developer_env"
},
{
"label": "Docker",
"href": "/contributing/developer_env/docker"
},
{
"label": "RunPod",
"href": "/contributing/developer_env/runpod"
}
],
"collapsed": true,
"display": "group"
},
{
"label": "Pull Requests",
"href": "/contributing/pull_requests"
},
{
"label": "CI/CD Architecture",
"href": "/contributing/ci_architecture"
},
{
"label": "Testing",
"href": "/contributing/testing"
},
{
"label": "Environment Variables",
"href": "/contributing/env_vars"
},
{
"label": "Performance Benchmarks",
"href": "/contributing/performance_benchmarks"
},
{
"label": "Profiling",
"href": "/contributing/profiling"
},
{
"label": "Activation Trace",
"href": "/contributing/activation_trace"
},
{
"label": "Coding Agents",
"href": "/contributing/coding_agents"
},
{
"label": "Dreamverse Development",
"href": "/contributing/dreamverse-development"
},
{
"label": "Attention Backend Development",
"href": "/contributing/attention_backend"
},
{
"label": "Add a pipeline",
"href": "/inference/add_pipeline"
},
{
"label": "Debugging",
"href": "/utilities/debugging"
},
{
"label": "Documenting FastVideo",
"href": "/contributing/documentation"
}
]
},
{
"label": "Architecture",
"display": "group",
"collapsed": true,
"items": [
{
"label": "Overview",
"href": "/design/overview"
},
{
"label": "Training Architecture",
"href": "/design/training_architecture"
},
{
"label": "Server Contracts",
"items": [
{
"label": "Overview",
"href": "/design/server_contracts"
},
{
"label": "OpenAI HTTP",
"href": "/design/server_contracts/openai"
},
{
"label": "Streaming WebSocket",
"href": "/design/server_contracts/streaming"
},
{
"label": "Dynamo Integration",
"href": "/design/server_contracts/dynamo"
}
],
"collapsed": true,
"display": "group"
}
]
},
{
"label": "API reference",
"collapsed": true,
"items": [
{
"label": "Overview",
"href": "/api"
},
{
"label": "Python package",
"href": "/api/fastvideo"
}
],
"display": "group"
},
{
"label": "Contact",
"href": "/contact"
}
]
+13588
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "fastvideo-docs-site",
"private": true,
"type": "module",
"scripts": {
"prepare:content": "node scripts/prepare-content.mjs",
"predev": "npm run prepare:content",
"dev": "blume dev",
"prebuild": "npm run prepare:content",
"build": "blume build",
"preview": "blume preview",
"prevalidate": "npm run prepare:content",
"validate": "blume validate --strict",
"check": "tsc --noEmit",
"test": "playwright test",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test:content": "node --test tests/content.test.mjs tests/commands.test.mjs"
},
"dependencies": {
"blume": "2.2.3",
"class-variance-authority": "0.7.1",
"cn": "^0.4.0",
"radix-ui": "^1.8.1",
"react": "19.3.1",
"react-dom": "19.3.1",
"yaml": "2.9.1"
},
"devDependencies": {
"@playwright/test": "1.64.1",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"prettier": "3.9.9",
"typescript": "~5.9.3"
},
"engines": {
"node": ">=22.19.0"
},
"overrides": {
"@astrojs/react": "7.0.0",
"@vitejs/plugin-react": "6.1.1"
},
"prettier": {
"printWidth": 120
}
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
testMatch: "**/*.spec.ts",
use: { baseURL: "http://127.0.0.1:4327", trace: "retain-on-failure" },
webServer: {
command: "npm run preview -- --host 127.0.0.1 --port 4327",
url: "http://127.0.0.1:4327/FastVideo/cookbook",
reuseExistingServer: !process.env.CI,
},
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+38
View File
@@ -0,0 +1,38 @@
import { sourceRecipes } from "../lib/cookbook-pages.ts";
/** Prepare the docs transfer and publish the existing cookbook catalogs. */
import { execFileSync } from "node:child_process";
import { cp, mkdir, rm } from "node:fs/promises";
import { transferDocs } from "./transfer-docs.mjs";
await transferDocs();
const repo = new URL("../../", import.meta.url);
execFileSync("npm", ["run", "build:catalog", "--prefix", "docs"], {
cwd: repo,
stdio: "inherit",
});
const publicDir = new URL("../public/", import.meta.url);
await mkdir(publicDir, { recursive: true });
const destination = new URL("assets/cookbook-config/", publicDir);
await rm(destination, { recursive: true, force: true });
await cp(new URL("docs/assets/cookbook-config/", repo), destination, {
recursive: true,
});
await cp(new URL("docs/assets/logos/icon_simple.svg", repo), new URL("icon.svg", publicDir));
await cp(new URL("../assets/hao-ai-lab.png", import.meta.url), new URL("assets/hao-ai-lab.png", publicDir));
await cp(new URL("../.generated/source-examples/", import.meta.url), new URL("assets/cookbook-examples/", publicDir), {
recursive: true,
});
// Publish exact checked-in YAML for inference-only model pages. No settings are synthesized.
const sourceDir = new URL("assets/cookbook-sources/", publicDir);
await rm(sourceDir, { recursive: true, force: true });
await mkdir(sourceDir, { recursive: true });
for (const recipe of sourceRecipes) {
if (/\.ya?ml$/.test(recipe.source))
await cp(new URL(recipe.source, repo), new URL(`${recipe.id}-inference.yaml`, sourceDir));
if (recipe.serving?.source)
await cp(new URL(recipe.serving.source, repo), new URL(`${recipe.id}-serving.yaml`, sourceDir));
}
+291
View File
@@ -0,0 +1,291 @@
"""Build source-only Python reference pages with Griffe, without importing FastVideo."""
from __future__ import annotations
import html
import ast
import json
import re
import runpy
import shutil
import shlex
from pathlib import Path
from urllib.parse import urljoin, urlsplit, urlunsplit
import griffe
import markdown
import yaml
ROOT = Path(__file__).resolve().parents[2]
SITE = ROOT / "docs-site"
GENERATED = SITE / ".generated"
def source_controls(source: str) -> list[dict]:
"""Expose literal settings in maintained examples without importing them."""
labels = {
"prompt": "Prompt", "num_gpus": "GPU count", "height": "Height", "width": "Width",
"num_frames": "Frames", "seed": "Seed", "num_inference_steps": "Inference steps",
"guidance_scale": "Guidance scale", "fps": "FPS", "audio_end_in_s": "Audio duration",
"text_encoder_cpu_offload": "Offload text encoder", "vae_cpu_offload": "Offload VAE",
"dit_cpu_offload": "Offload transformer", "inference_torch_compile": "Compile transformer",
"video_decode_backend": "Video decoder",
}
lines = source.splitlines(keepends=True)
offsets = [0]
for line in lines:
offsets.append(offsets[-1] + len(line))
def position(line, column):
prefix = source[:offsets[line - 1]] + lines[line - 1].encode()[:column].decode()
return len(prefix.encode("utf-16-le")) // 2
settings = {}
def add(name, value):
if name not in labels or not isinstance(value, ast.Constant) or type(value.value) not in (str, int, float, bool):
return
key = json.dumps([name, value.value])
entry = settings.setdefault(key, {"name": name, "label": labels[name], "value": value.value,
"integer": type(value.value) is int, "spans": []})
entry["spans"].append([position(value.lineno, value.col_offset), position(value.end_lineno, value.end_col_offset)])
for node in ast.walk(ast.parse(source)):
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
if node.func.attr in {"from_pretrained", "generate_video"}:
for keyword in node.keywords:
add(keyword.arg, keyword.value)
elif node.func.attr == "add_argument":
flags = [arg.value for arg in node.args if isinstance(arg, ast.Constant) and isinstance(arg.value, str)]
flag = next((flag for flag in flags if flag.startswith("--")), "")
for keyword in node.keywords:
if keyword.arg == "default":
add(flag.removeprefix("--").replace("-", "_"), keyword.value)
key = json.dumps([flag.removeprefix("--").replace("-", "_"), getattr(keyword.value, "value", None)])
if key in settings:
settings[key]["flag"] = flag
if any(item.arg == "type" and isinstance(item.value, ast.Name) and item.value.id == "float"
for item in node.keywords):
settings[key]["integer"] = False
settings[key]["booleanOptional"] = any(
item.arg == "action" and isinstance(item.value, ast.Attribute) and item.value.attr == "BooleanOptionalAction"
for item in node.keywords)
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id.isupper():
add(target.id.lower(), node.value)
result = list(settings.values())
seen = {}
for field in result:
seen[field["label"]] = seen.get(field["label"], 0) + 1
if seen[field["label"]] > 1:
field["label"] += f' ({seen[field["label"]]})'
return result
def signature(obj) -> str:
"""Preserve parameter kinds, annotations, defaults, and return annotations."""
if obj.is_attribute:
annotation = f": {obj.annotation}" if obj.annotation else ""
value = f" = {obj.value}" if obj.value is not None else ""
return f"{obj.name}{annotation}{value}"
parameters = list(getattr(obj, "parameters", []))
parts = []
keyword_separator = False
for i, param in enumerate(parameters):
if param.name in {"self", "cls"}:
continue
kind = param.kind.value
if kind == "keyword-only" and not keyword_separator:
parts.append("*")
keyword_separator = True
prefix = "**" if kind == "variadic keyword" else "*" if kind == "variadic positional" else ""
if prefix == "*":
keyword_separator = True
text = prefix + param.name
if param.annotation is not None:
text += f": {param.annotation}"
if param.default is not None and not prefix:
text += f" = {param.default}"
parts.append(text)
if kind == "positional-only" and (i + 1 == len(parameters) or parameters[i + 1].kind.value != kind):
parts.append("/")
result = f"{obj.name}({', '.join(parts)})"
if obj.is_function and obj.returns is not None:
result += f" -> {obj.returns}"
return result
def code_html(text: str) -> str:
return html.escape(text).replace("[", "&#91;").replace("]", "&#93;")
def prose(text: str) -> str:
# Docstrings are trusted repository source, rendered as Markdown rather than executed.
return markdown.markdown(text, extensions=["fenced_code", "tables", "sane_lists"])
def api_overview(text: str, package) -> str:
def reference(match):
label, target = match.groups()
path = (target or label).strip("`")
obj = package if path == package.path else package[path.removeprefix(package.path + ".")]
module = obj
while not module.is_module:
module = module.parent
route = "/api/" + module.path.replace(".", "/")
return f"[{label}]({route}#{path})"
text = re.sub(r"\[([^\]\n]+)\]\[([^\]\n]*)\]", reference, text)
def guide(match):
label, target = match.groups()
url = urlsplit(target)
if url.scheme or url.netloc or not re.search(r"\.mdx?$", url.path):
return match[0]
url = urlsplit(urljoin("/api/index.md", target))
route = re.sub(r"\.mdx?$", "", url.path)
route = re.sub(r"/index$", "", route) or "/"
return f"[{label}]({urlunsplit(('', '', route, url.query, url.fragment))})"
return prose(re.sub(r"\[([^\]\n]+)\]\(([^\s)]+)\)", guide, text))
def docstring(obj, level: int) -> str:
if not obj.docstring:
return ""
output = []
for section in obj.docstring.parsed:
kind = section.kind.value
if kind == "text":
output.append(prose(section.value))
continue
title = section.title or kind.replace("_", " ").capitalize()
output.append(f"<h{level}>{html.escape(title)}</h{level}>")
if kind == "examples":
for example_kind, value in section.value:
output.append(f"<pre><code>{html.escape(value)}</code></pre>" if example_kind.value == "examples" else prose(value))
elif isinstance(section.value, list):
output.append("<dl>")
for item in section.value:
name = getattr(item, "name", "") or ""
annotation = getattr(item, "annotation", "") or ""
label = f"{name}: {annotation}" if name and annotation else str(name or annotation)
output.append(f"<dt><code>{html.escape(label)}</code></dt><dd>{prose(getattr(item, 'description', ''))}</dd>")
output.append("</dl>")
else:
output.append(prose(getattr(section.value, "description", str(section.value))))
return "\n".join(output)
def symbol(obj, path: str, level: int = 2, members: list[str] | None = None, source: bool = True) -> str:
level = min(level, 6)
output = [f'<h{level} id="{html.escape(path)}"><code>{html.escape(path)}</code></h{level}>']
output.append(f'<pre class="doc-signature"><code>{code_html(signature(obj))}</code></pre>')
output.append(docstring(obj, min(level + 1, 6)))
if obj.is_class:
constructor = obj.members.get("__init__")
if constructor is not None:
output.append(docstring(constructor, min(level + 1, 6)))
if source and not obj.is_alias:
try:
code = obj.source
except (ValueError, KeyError):
code = ""
if code:
output.append(f'<details><summary>Source</summary><pre><code>{code_html(code)}</code></pre></details>')
if obj.is_class:
for name, member in obj.members.items():
if members is not None and name not in members:
continue
if name.startswith("_") or member.is_module:
continue
if member.is_attribute and not member.docstring:
continue
try:
output.append(symbol(member, f"{path}.{name}", min(level + 1, 6), source=source))
except griffe.AliasResolutionError:
continue
return "\n".join(output)
def write_page(route: str, title: str, body: str, anchors: dict) -> None:
# Preserve incoming fragments, including historical category labels whose layout changed.
present = set(re.findall(r'id="([^"]+)"', body))
aliases = "\n".join(f'<a id="{html.escape(h["id"])}"></a>' for h in anchors.get(route, []) if h["id"] not in present)
target = GENERATED / "python-reference" / route / "index.md"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("---\n" + yaml.safe_dump({"title": title}, sort_keys=False) +
f'---\n\n<div class="python-reference">\n{aliases}\n{body}\n</div>\n')
def prepare() -> None:
GENERATED.mkdir(exist_ok=True)
examples = runpy.run_path(str(ROOT / "docs/generate_examples.py"))
examples["validate_cookbook"]()
examples["generate_examples"](generate_main_index=True)
source_dir = GENERATED / "source-examples"
shutil.rmtree(source_dir, ignore_errors=True)
source_dir.mkdir()
for recipe in json.loads((ROOT / "docs/assets/cookbook-recipes.json").read_text())["recipes"]:
path = ROOT / recipe["source"]
if path.suffix == ".py":
source = path.read_text()
controls = source_controls(source)
argv = shlex.split(recipe["command"])
for field in controls:
flag = field.get("flag")
positions = [i for i, argument in enumerate(argv[:-1]) if argument == flag]
if positions and type(field["value"]) is not bool:
converter = float if isinstance(field["value"], (int, float)) and not field["integer"] else type(field["value"])
field["value"] = converter(argv[positions[-1] + 1])
elif field.get("booleanOptional"):
if flag in argv:
field["value"] = True
elif flag.replace("--", "--no-", 1) in argv:
field["value"] = False
(source_dir / f'{recipe["id"]}.json').write_text(json.dumps({"source": source, "controls": controls}))
anchors = json.loads((SITE / "migration-anchors.json").read_text())
shutil.rmtree(GENERATED / "python-reference", ignore_errors=True)
package = griffe.load("fastvideo", search_paths=[ROOT], allow_inspection=False, resolve_aliases=True,
resolve_external=False, docstring_parser="google", docstring_options={"warnings": False})
modules = []
def visit(module):
if module.is_alias or any(part.startswith("_") or part in {"tests", "third_party"} for part in module.path.split(".")):
return
modules.append(module)
for child in module.modules.values():
visit(child)
visit(package)
for module in sorted(modules, key=lambda item: item.path):
body = [f'<a id="{module.path}"></a>', docstring(module, 2)]
for name, member in module.members.items():
if name.startswith("_"):
continue
try:
# External imports belong to their upstream reference, not FastVideo's API.
if member.is_alias and not member.target_path.startswith("fastvideo."):
continue
if member.is_module or member.is_attribute and not member.docstring:
continue
body.append(symbol(member, f"{module.path}.{name}"))
except griffe.AliasResolutionError:
continue
write_page("api/" + module.path.replace(".", "/"), module.name, "\n".join(body), anchors)
write_page("api", "Python API reference", api_overview((ROOT / "docs/api/index.md").read_text(), package), anchors)
v1 = (ROOT / "docs/getting_started/v1_api.md").read_text()
def reference(match):
path, level, members = match.groups()
return "\n" + symbol(package[path.removeprefix("fastvideo.")], path,
int(level), members.split(","), source=False) + "\n"
v1 = re.sub(r'<api-reference path="([^"]+)" heading="(\d+)" members="([^"]+)" />', reference, v1)
write_page("getting_started/v1_api", "V1 API", prose(v1.removeprefix("# V1 API\n")), anchors)
(GENERATED / "old-headings.json").write_text(json.dumps(anchors))
print(f"Rendered {len(modules) + 1} Python reference pages directly from source, without MkDocs or runtime imports.")
if __name__ == "__main__":
prepare()
+370
View File
@@ -0,0 +1,370 @@
import { modelDirectory } from "../lib/cookbook-pages.ts";
/** Rebuild the Blume view from canonical Markdown and the existing Python reference renderer. */
import { execFileSync } from "node:child_process";
import { existsSync, statSync } from "node:fs";
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { parse, stringify } from "yaml";
const site = fileURLToPath(new URL("../", import.meta.url));
const repo = resolve(site, "..");
const source = join(repo, "docs");
const generated = join(site, ".generated");
const content = join(generated, "content");
const publicDir = join(site, "public");
const attachments = new Set();
const routeFor = (file) =>
"/" +
file
.replace(/\\/g, "/")
.replace(/\.(md|mdx)$/, "")
.replace(/(^|\/)index$/, "")
.replace(/\/$/, "");
async function files(root) {
const result = [];
for (const entry of await readdir(root, { withFileTypes: true })) {
const path = join(root, entry.name);
if (entry.isDirectory()) result.push(...(await files(path)));
else result.push(path);
}
return result;
}
function splitPage(text) {
const match = /^---\n([\s\S]*?)\n---\n/.exec(text);
return match ? { data: parse(match[1]), body: text.slice(match[0].length) } : { data: {}, body: text };
}
function localTarget(target, from, pretty = false) {
if (/^(?:#|mailto:|data:|tel:)/.test(target)) return target;
let url;
try {
url = new URL(target, `https://transfer.invalid${pretty ? `${routeFor(from)}/` : `/${from}`}`);
} catch {
return target;
}
if (url.host !== "transfer.invalid") {
if (url.origin !== "https://haoailab.com" || !url.pathname.startsWith("/FastVideo/")) return target;
url = new URL(url.pathname.replace(/^\/FastVideo/, "") + url.search + url.hash, "https://transfer.invalid");
}
const path = decodeURIComponent(url.pathname).replace(/^\//, "");
// Links leaving docs/ name repository files, not files served by the docs host.
if (!pretty && !target.startsWith("/") && !/^[a-z]+:/i.test(target)) {
const actual = resolve(source, dirname(from), decodeURIComponent(target.split(/[?#]/)[0]));
if (!actual.startsWith(source + sep) && existsSync(actual)) {
return `https://github.com/hao-ai-lab/FastVideo/blob/main/${relative(repo, actual).replaceAll(sep, "/")}${url.hash}`;
}
}
if (existsSync(join(source, path)) && statSync(join(source, path)).isFile() && !/\.mdx?$/.test(path))
attachments.add(path);
if (/\.mdx?$/.test(path)) return `${routeFor(path)}${url.search}${url.hash}`;
return `/${path}${url.search}${url.hash}`;
}
function rewriteLinks(body, from) {
let fence;
return body
.split("\n")
.map((line) => {
const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
if (marker) {
if (!fence) fence = marker;
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined;
return line;
}
if (fence) return line;
return line
.replace(/<(?=[0-9=])/g, "&lt;")
.replace(/(\]\()([^\s)]+)(\))/g, (_, before, url, after) => before + localTarget(url, from) + after)
.replace(
/((?:href|src)=["'])([^"']+)(["'])/g,
(_, before, url, after) => before + localTarget(url, from, from.startsWith("cookbook/")) + after,
);
})
.join("\n");
}
function pinHeadings(body, old, mdx) {
const normalize = (text) =>
text
.replace(/<[^>]*>/g, "")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/[^\p{L}\p{N}]/gu, "")
.toLowerCase();
const candidates = [...(old ?? [])];
let fence;
let text = body
.split("\n")
.map((line) => {
const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
if (marker) {
if (!fence) fence = marker;
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined;
return line;
}
if (fence) return line;
const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
if (!heading) return line;
const clean = heading[2].replace(/\s*(?:\[#.*?\]|\{#.*?\})\s*$/, "");
const index = candidates.findIndex(
(item) => item.level === heading[1].length && normalize(item.text.replace(/¶$/, "")) === normalize(clean),
);
if (index < 0) return line;
const [original] = candidates.splice(index, 1);
if (original.id.includes("__")) return `${heading[1]} <a id="${original.id}"></a>${clean}`;
return `${heading[1]} ${clean} ${mdx ? `[#${original.id}]` : `{#${original.id}}`}`;
})
.join("\n");
const first = candidates.find((item) => item.level === 1);
if (first && !text.includes(`id="${first.id}"`)) text = `<a id="${first.id}"></a>\n\n${text}`;
return text;
}
/** Blume owns the page title, including examples whose Source link precedes their H1. */
function extractPageTitle(body, data, deduplicate = false) {
const normalize = (text) => text.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase();
let fence;
let found = false;
return body
.split("\n")
.map((line) => {
const marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
if (marker) {
if (!fence) fence = marker;
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined;
return line;
}
if (fence || (found && !deduplicate)) return line;
const heading = (deduplicate ? /^(#{1,2})\s+(.+)$/ : /^(#)\s+(.+)$/).exec(line);
if (!heading) return line;
if (heading[1].length === 2 && !data.title) return line;
const anchor = /(?:\[#([^\]]+)\]|\{#([^}]+)\})\s*$/.exec(heading[2]);
const embedded = /<a\s+id="[^"]*"><\/a>/g;
const anchors = heading[2].match(embedded) ?? [];
const title = heading[2]
.replace(/\s*(?:\[#[^\]]+\]|\{#[^}]+\})\s*$/, "")
.replace(embedded, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/[`*_]/g, "")
.trim();
if (data.title && normalize(data.title) !== normalize(title)) return line;
if (heading[1].length === 1) {
data.title ??= title;
found = true;
}
if (anchor) anchors.push(`<a id="${anchor[1] ?? anchor[2]}"></a>`);
return anchors.join("\n");
})
.join("\n");
}
export async function transferDocs() {
const localPython = join(site, ".venv/bin/python");
const python = process.env.DOCS_PYTHON || (existsSync(localPython) ? localPython : "python3");
execFileSync(python, [join(site, "scripts/prepare-python-docs.py")], { cwd: repo, stdio: "inherit" });
await rm(content, { recursive: true, force: true });
await rm(publicDir, { recursive: true, force: true });
await mkdir(content, { recursive: true });
const ignored = new Set(["js", "tests", "node_modules", "__pycache__"]);
await cp(source, content, {
recursive: true,
filter: async (path) => {
const rel = relative(source, path);
if (ignored.has(rel.split(sep)[0]) || rel === "README.md") return false;
return (await stat(path)).isDirectory() || /\.mdx?$/.test(path);
},
});
// Native callout directives and islands compile as MDX, while source links keep .md filenames.
for (const file of await files(content)) {
if (!file.endsWith(".md")) continue;
const text = await readFile(file, "utf8");
if (/^:::(?:note|tip|warning|danger|info|success)\b/m.test(text) || text.includes("<RecipeFamily ")) {
await writeFile(file.replace(/\.md$/, ".mdx"), text);
await rm(file);
}
}
const oldHeadings = JSON.parse(await readFile(join(generated, "old-headings.json"), "utf8"));
for (const path of await files(content)) {
if (!/\.mdx?$/.test(path)) continue;
const rel = relative(content, path).replaceAll(sep, "/");
const route = routeFor(rel);
const { data, body } = splitPage(await readFile(path, "utf8"));
delete data.noindex;
data.slug = route.replace(/^\//, "") || "/";
const pinned = extractPageTitle(
pinHeadings(body, oldHeadings[route.slice(1) || "."], path.endsWith(".mdx")),
data,
rel.includes("/examples/") || rel.startsWith("examples/"),
);
// GitHub's buttons remain ordinary links; no third-party script is needed in the new shell.
let converted = rewriteLinks(pinned, rel).replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, "");
converted = converted.replace(/^(\s*`{3,})slurm\b/gm, "$1bash");
if (path.endsWith(".mdx"))
converted = converted.replace(
/<style>([\s\S]*?)<\/style>/g,
(_, css) => `<style>{${JSON.stringify(css)}}</style>`,
);
{
const aliases = (oldHeadings[route.slice(1)] ?? []).filter(
(heading) =>
!converted.includes(`id="${heading.id}"`) &&
!converted.includes(`[#${heading.id}]`) &&
!converted.includes(`{#${heading.id}}`),
);
converted += "\n" + aliases.map((heading) => `<a id="${heading.id}"></a>`).join("\n");
}
await writeFile(path, `---\n${stringify(data)}---\n${converted}`);
}
// API HTML is extracted directly from Python source with Griffe.
await rm(join(content, "getting_started/v1_api.md"), { force: true });
await rm(join(content, "getting_started/v1_api.mdx"), { force: true });
await cp(join(generated, "python-reference"), content, { recursive: true });
const originalHome = splitPage(await readFile(join(content, "index.md"), "utf8")).body;
const section = originalHome.indexOf("## Key Features");
if (section < 0) throw new Error("The homepage source needs an updated section boundary.");
await mkdir(join(content, "_snippets"), { recursive: true });
await mkdir(join(content, "_snippets/clients"), { recursive: true });
for (const [file, language] of [
["video.py", "python"],
["video.mjs", "javascript"],
["video.sh", "bash"],
]) {
const code = await readFile(join(repo, "examples/serving/clients", file), "utf8");
await writeFile(join(content, "_snippets/clients", `${file}.md`), `\`\`\`${language}\n${code}\n\`\`\`\n`);
}
const asMdx = (body) =>
body
.replace(/\{#([^}]+)\}/g, "[#$1]")
.replace(/<img\b([^>]*?)\/?\s*>/g, "<img$1 />")
.replace(/ style="[^"]*"/g, "");
const description = originalHome
.slice(0, section)
.split(/\n\s*\n/)
.find((paragraph) => paragraph.trim().startsWith("FastVideo is an inference"));
if (!description) throw new Error("The homepage description paragraph needs updating.");
await writeFile(join(content, "_snippets/home-description.mdx"), asMdx(description.trim()));
const documentation = originalHome.indexOf("## Documentation", section);
if (documentation < 0) throw new Error("The homepage documentation section boundary needs updating.");
await writeFile(join(content, "_snippets/home-details.mdx"), asMdx(originalHome.slice(section, documentation)));
// The README owns announcements and the community list; the welcome page renders a short news digest.
const readme = await readFile(join(repo, "README.md"), "utf8");
const news = readme.match(/^## NEWS\n([\s\S]*?)(?=^##|^###)/m)?.[1];
const projects = readme.match(/^## Awesome work using FastVideo or our research projects\n([\s\S]*?)(?=^## )/m)?.[1];
if (!news || !projects) throw new Error("The README news or project section boundary needs updating.");
const recent = [...news.matchAll(/^- `([\d/]+)`: (.+)$/gm)].slice(0, 2).map(([, date, text]) => {
const links = [...text.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)];
const headline = text
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.split(/:\s|\.\s/)[0]
.replace(/\.$/, "");
const href = links.at(-1)?.[2] ?? "https://github.com/hao-ai-lab/FastVideo#news";
const iso = date.replaceAll("/", "-");
const label = new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC",
}).format(new Date(iso));
return `<li><time dateTime="${iso}">${label}</time><a href={${JSON.stringify(href)}}>{${JSON.stringify(headline)}}</a></li>`;
});
await writeFile(
join(content, "_snippets/home-news.mdx"),
`<section className="home-news not-prose" aria-labelledby="recent-news">\n<div className="home-news-heading"><h2 id="recent-news">Recent news</h2><a className="home-news-more" href="https://github.com/hao-ai-lab/FastVideo#news">All announcements →</a></div>\n<ul>\n${recent.join("\n")}\n</ul>\n</section>\n`,
);
await writeFile(join(content, "_snippets/home-projects.mdx"), asMdx(projects.trim()));
await rm(join(content, "index.md"));
await rm(join(content, "cookbook/index.md"), { force: true });
await rm(join(content, "cookbook/index.mdx"), { force: true });
await cp(join(site, "content"), content, { recursive: true });
// Model-specific inference pages come from the maintained recipe inventory.
for (const model of modelDirectory.filter((model) => !model.configurable)) {
const file = join(content, `${model.path.slice(1)}.mdx`);
await mkdir(dirname(file), { recursive: true });
await writeFile(
file,
`---\ntitle: ${JSON.stringify(model.title)}\nmode: frame\n---\n\n<Cookbook modelId="${model.id}" />\n`,
);
}
await cp(join(source, "assets"), join(publicDir, "assets"), {
recursive: true,
filter: (path) => !path.endsWith(".md"),
});
for (const path of attachments) {
await mkdir(dirname(join(publicDir, path)), { recursive: true });
await cp(join(source, path), join(publicDir, path));
}
const routes = new Set(
(await files(content))
.filter(
(path) =>
/\.mdx?$/.test(path) &&
!relative(content, path)
.split(sep)
.some((part) => /^[_.]/.test(part)),
)
.map((path) => routeFor(relative(content, path))),
);
const navigation = JSON.parse(await readFile(join(site, "navigation.json"), "utf8"));
function checkNavigation(items) {
for (const item of items) {
if (typeof item === "string") {
if (!routes.has(item)) throw new Error(`Navigation target is missing: ${item}`);
} else {
if (item.href && !routes.has(item.href)) throw new Error(`Navigation target is missing: ${item.href}`);
if (item.items) checkNavigation(item.items);
}
}
}
checkNavigation(navigation);
// Keep generated modules discoverable without putting all 880 entries in every page's sidebar.
for (const path of await files(join(content, "api"))) {
if (!path.endsWith(".md")) continue;
const { data, body } = splitPage(await readFile(path, "utf8"));
const folder = dirname(path);
const children = (await readdir(folder, { withFileTypes: true })).filter((item) => item.isDirectory());
const links = children
.map(
(item) =>
`<li><a href="${routeFor(relative(content, join(folder, item.name, "index.md")))}">${item.name}</a></li>`,
)
.join("\n");
await writeFile(
path,
`---\n${stringify(data)}---\n${body.replaceAll("{", "&#123;").replaceAll("}", "&#125;")}${links ? `\n<h2>Submodules</h2>\n<ul>\n${links}\n</ul>\n` : ""}`,
);
}
function addModelPages(items) {
for (const item of items) {
const entries = modelDirectory.filter((model) => !model.configurable && model.familyPath === item.href);
if (entries.length && !/Family guide|Recipes and setup/.test(item.label)) {
const guide = { label: "Family guide", href: item.href };
delete item.href;
item.display = "group";
item.collapsed = true;
item.items = [...entries.map((model) => ({ label: model.title, href: model.path })), guide];
} else if (item.items) {
const guide = item.items.find((child) => /Family guide|Recipes and setup/.test(child.label));
if (guide) {
const extras = modelDirectory.filter((model) => !model.configurable && model.familyPath === guide.href);
item.items.splice(
item.items.indexOf(guide),
0,
...extras.map((model) => ({ label: model.title, href: model.path })),
);
}
addModelPages(item.items);
}
}
}
addModelPages(navigation);
await writeFile(join(generated, "navigation.json"), JSON.stringify(navigation));
await writeFile(join(generated, "routes.json"), JSON.stringify([...routes].sort()));
console.log(
`Transferred ${routes.size} documentation routes. Canonical guide routes and cookbook runbooks are published together.`,
);
}
+100
View File
@@ -0,0 +1,100 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { parse } from "yaml";
import { configAndLaunch, inferenceOutput, pythonSource, sdkCommand } from "../lib/cookbook-commands.ts";
test("every generated Python example remains valid after its source-backed settings are applied", () => {
const directory = new URL("../.generated/source-examples/", import.meta.url);
const examples = readdirSync(directory)
.filter((name) => name.endsWith(".json"))
.map((name) => {
const data = JSON.parse(readFileSync(new URL(name, directory), "utf8"));
const edits = Object.fromEntries(
data.controls.map((field, i) => [
i,
typeof field.value === "number"
? 2
: typeof field.value === "boolean"
? false
: "A bowl 🥣 with 'quotes' and $(literal text)",
]),
);
return {
name,
baseline: pythonSource(data.source, data.controls),
edited: pythonSource(data.source, data.controls, edits),
};
});
assert.ok(examples.length > 25);
execFileSync(
"python3",
[
"-c",
"import ast,json,sys\nfor example in json.load(sys.stdin):\n for key in ('baseline', 'edited'):\n ast.parse(example[key], filename=example['name'])",
],
{ input: JSON.stringify(examples) },
);
});
test("SDK launch commands have valid shell syntax without executing model code", () => {
execFileSync("bash", ["-n"], {
input: configAndLaunch("generator: {}\nrequest: {}\n", `FASTVIDEO_FA4=0 ${sdkCommand}`),
});
});
test("copy-and-launch writes literal YAML before invoking the command", () => {
const root = mkdtempSync(join(tmpdir(), "fastvideo-copy-command-"));
try {
const yaml = 'prompt: "$(touch injected) `touch injected2`"\nFASTVIDEO_CONFIG\n';
execFileSync("bash", ["-c", configAndLaunch(yaml, "cp config.yaml received.yaml")], { cwd: root });
assert.equal(readFileSync(join(root, "received.yaml"), "utf8"), yaml);
assert.throws(() => readFileSync(join(root, "injected")));
assert.throws(() => readFileSync(join(root, "injected2")));
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("inference output preserves edited settings and hidden generator fields", () => {
const config = {
generator: {
model_path: "FastVideo/Trim",
engine: { num_gpus: 2 },
pipeline: { experimental: { VSA_sparsity: 0.8 } },
},
server: { port: 9000 },
default_request: { prompt: "A ceramic bowl", sampling: { num_frames: 243, seed: 0 } },
};
const result = inferenceOutput({ config, command: "FASTVIDEO_FA4=0 fastvideo serve --config config.yaml" });
const saved = parse(result.yaml);
assert.deepEqual(saved.generator, config.generator);
assert.deepEqual(saved.request.sampling, config.default_request.sampling);
assert.equal(saved.request.prompt, "A ceramic bowl");
assert.equal(saved.request.output.save_video, true);
assert.equal(saved.server, undefined);
assert.equal(result.command, "FASTVIDEO_FA4=0 fastvideo generate --config config.yaml");
assert.ok(config.server);
});
test("Wan I2V inference requires a source image and preserves the serving baseline", () => {
const config = parse(
readFileSync(new URL("../../examples/serving/openai_wan21_i2v_14b.yaml", import.meta.url), "utf8"),
);
const baseline = structuredClone(config);
const result = { config, command: "fastvideo serve --config config.yaml" };
assert.throws(() => inferenceOutput(result), /source image path/);
assert.throws(() => inferenceOutput(result, " "), /source image path/);
const saved = parse(inferenceOutput(result, " /data/first-frame.png ").yaml);
assert.equal(saved.request.inputs.image_path, "/data/first-frame.png");
assert.deepEqual(saved.request.sampling, config.default_request.sampling);
assert.deepEqual(config, baseline);
assert.equal(config.default_request.inputs, undefined);
const withDefaults = structuredClone(config);
withDefaults.default_request.inputs = { image_path: "/data/existing.png", video_path: "/data/reference.mp4" };
const existing = parse(inferenceOutput({ ...result, config: withDefaults }).yaml);
assert.deepEqual(existing.request.inputs, withDefaults.default_request.inputs);
});
+76
View File
@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import { readFile, access } from "node:fs/promises";
import { test } from "node:test";
const site = new URL("../", import.meta.url);
const routes = JSON.parse(await readFile(new URL(".generated/routes.json", site), "utf8"));
const old = JSON.parse(await readFile(new URL("migration-anchors.json", site), "utf8"));
test("every migrated route and heading anchor survives the cutover", async () => {
const failures = [];
for (const [path, headings] of Object.entries(old)) {
const route = path === "." ? "/" : `/${path}`;
if (!routes.includes(route)) {
failures.push(`Missing route: ${route}`);
continue;
}
const html = await readFile(new URL(`dist/${path === "." ? "" : `${path}/`}index.html`, site), "utf8");
for (const heading of headings) {
const escaped = heading.id.replaceAll("&", "&amp;").replaceAll('"', "&quot;");
if (!html.includes(`id="${escaped}"`) && !html.includes(`id='${escaped}'`))
failures.push(`${route}#${heading.id}`);
}
}
assert.deepEqual(failures, []);
});
test("converted guides render admonitions, preserve content, and publish local images", async () => {
const quickStart = await readFile(new URL("dist/getting_started/quick_start/index.html", site), "utf8");
assert.ok(quickStart.includes("Need more control?"));
assert.ok(!quickStart.includes("!!! tip"));
const home = await readFile(new URL("dist/index.html", site), "utf8");
assert.ok(home.includes("Welcome to FastVideo"));
assert.ok(home.includes("/FastVideo/assets/logos/logo.svg"));
assert.ok(home.includes("FastVideo is an inference and post-training framework for diffusion models."));
assert.ok(!home.includes("About FastVideo and performance overview"));
await readFile(new URL("dist/assets/logos/logo.svg", site));
});
test("Python API and generated examples contain the actual rendered source documentation", async () => {
const api = await readFile(new URL("dist/api/fastvideo/configs/pipelines/base/index.html", site), "utf8");
assert.ok(api.includes('id="fastvideo.configs.pipelines.base.PipelineConfig"'));
assert.ok(api.includes("from_pretrained"));
assert.ok(api.includes("python-reference"));
const examples = routes.filter((route) => route.startsWith("/inference/examples/"));
assert.ok(examples.length > 5);
const example = await readFile(new URL("dist/inference/examples/basic/index.html", site), "utf8");
assert.ok(example.includes("VideoGenerator"));
});
test("generated examples show their page title only once", async () => {
const failures = [];
for (const route of routes.filter((item) => item.includes("/examples/") || item.startsWith("/examples/"))) {
const html = await readFile(new URL(`dist${route}/index.html`, site), "utf8");
const headings = [...html.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/g)].map((match) => ({
level: Number(match[1]),
text: match[2]
.replace(/<[^>]+>/g, "")
.replace(/[^\p{L}\p{N}]/gu, "")
.toLowerCase(),
}));
const title = headings.find((heading) => heading.level === 1)?.text;
if (!title || headings.filter((heading) => heading.text === title).length !== 1) failures.push(route);
}
assert.deepEqual(failures, []);
});
test("the deployment artifact contains no retired site scripts or styles", async () => {
for (const name of ["custom.js", "custom.css", "copy-page.js", "cookbook.js", "cookbook-runbooks.css"]) {
await assert.rejects(access(new URL(`dist/assets/${name}`, site)));
}
const home = await readFile(new URL("dist/index.html", site), "utf8");
assert.ok(home.includes("home-featured-model"));
const family = await readFile(new URL("dist/cookbook/wan/index.html", site), "utf8");
assert.ok(!family.includes("legacy-cookbook"));
assert.ok(family.includes("recipe-card"));
});
+392
View File
@@ -0,0 +1,392 @@
import { expect, test, type Page } from "@playwright/test";
const route = "/FastVideo/cookbook/minimax-h3/fasth3-8step";
async function settings(page: Page, section: string) {
await expect(page.locator(".recipe-matrix")).toBeVisible();
if (section === "Serving" || section === "Hardware") {
const advanced = page.locator(".matrix-advanced");
if ((await advanced.getAttribute("open")) === null) await advanced.locator("summary").first().click();
}
}
async function backend(page: Page, name: string) {
await page
.getByRole("group", { name: "Hardware backend", exact: true })
.getByRole("button", { name, exact: true })
.click();
}
async function chip(page: Page, group: string, name: string) {
await page.getByRole("group", { name: group, exact: true }).getByRole("button", { name, exact: true }).click();
}
async function output(page: Page, section: string) {
await page.getByRole("tablist", { name: "Output format" }).getByRole("tab", { name: section, exact: true }).click();
}
test("edits generate YAML and client instructions, invalid edits clear output, reset restores baseline", async ({
page,
}) => {
await page.goto(route);
await output(page, "config.yaml");
const yaml = page.getByLabel("Generated YAML", { exact: true });
await settings(page, "Serving");
await expect(page.getByLabel("Port", { exact: true })).toHaveValue("8000");
const baseline = await yaml.innerText();
await page.getByLabel("Port", { exact: true }).fill("9000");
await expect(yaml).toContainText('"port": 9000');
await output(page, "Commands");
await expect(page.getByLabel("Client command", { exact: true })).toContainText(":9000/");
await output(page, "config.yaml");
await settings(page, "Model");
await chip(page, "Offload VAE to CPU", "Off");
await expect(yaml).toContainText('"vae": false');
await expect(yaml).toContainText('"num_gpus": 4');
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download YAML", exact: false }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("config.yaml");
const { readFile } = await import("node:fs/promises");
const downloaded = await readFile((await download.path())!, "utf8");
expect(downloaded).toContain('"port": 9000');
await settings(page, "Serving");
await page.getByLabel("Port", { exact: true }).fill("70000");
await expect(page.getByRole("button", { name: "Download YAML", exact: false })).toBeDisabled();
await expect(yaml).toHaveCount(0);
await expect(page.getByLabel("Port", { exact: true })).toHaveAttribute("aria-invalid", "true");
await page.getByRole("button", { name: "Reset changes", exact: true }).click();
await expect(yaml).toHaveText(baseline);
});
test("copy buttons use their own generated output", async ({ page, context }) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.goto(route);
await output(page, "config.yaml");
const yaml = await page.getByLabel("Generated YAML", { exact: true }).innerText();
await page.getByRole("button", { name: "Copy YAML", exact: true }).click();
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(yaml);
await output(page, "Commands");
await page.locator(".recipe-request-code > summary").click();
const copied = [yaml];
for (const label of ["Server command", "Client command"]) {
const command = await page.getByLabel(label, { exact: true }).innerText();
await page.getByRole("button", { name: `Copy ${label.toLowerCase()}`, exact: true }).click();
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(command);
copied.push(command);
}
expect(new Set(copied).size).toBe(3);
});
test("deployment changes reset edits and support MLX and streaming", async ({ page }) => {
await page.goto(route);
await settings(page, "Serving");
await page.getByLabel("Port", { exact: true }).fill("9000");
await backend(page, "MLX");
await expect(page.getByLabel("Server command", { exact: true })).toContainText("mlx_server");
await settings(page, "Serving");
await expect(page.getByLabel("Port", { exact: true })).toHaveValue("8000");
await page.goto("/FastVideo/cookbook/ltx/ltx2-distilled");
await expect(page.getByLabel("Client command", { exact: true })).toContainText("/health");
await page.locator(".recipe-request-code > summary").click();
await expect(page.getByText(/WebSocket URL:/)).toBeVisible();
await page.goto("/FastVideo/cookbook/wan/wan21-i2v");
await expect(page.getByLabel("Client command", { exact: true })).toContainText("input_reference");
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Wan2.1 I2V 14B");
});
test("failed loads offer retry without keeping stale commands", async ({ page }) => {
await page.route("**/recipes/fasth3-8step/cuda-rest.json", (route) =>
route.fulfill({ status: 503, body: "unavailable" }),
);
await page.goto(route);
await expect(page.getByRole("alert")).toContainText("HTTP 503");
await expect(page.getByLabel("Generated YAML", { exact: true })).toHaveCount(0);
await page.unroute("**/recipes/fasth3-8step/cuda-rest.json");
await page.getByRole("button", { name: "Retry loading recipes" }).click();
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toBeVisible();
});
test("an obsolete deployment response cannot replace the selected deployment", async ({ page }) => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
await page.route("**/recipes/fasth3-8step/mlx-rest.json", async (route) => {
await gate;
await route.continue().catch(() => {}); // The component may already have aborted the request.
});
await page.goto(route);
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toBeVisible();
await backend(page, "MLX");
await backend(page, "CUDA");
release();
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText("FastVideo-FastH3-8-Step-V2");
await expect(
page.getByRole("group", { name: "Hardware backend" }).getByRole("button", { name: "CUDA", exact: true }),
).toHaveAttribute("aria-pressed", "true");
});
test("mobile layout stays within the viewport and controls work by keyboard", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(route);
await settings(page, "Serving");
const port = page.getByLabel("Port", { exact: true });
await port.focus();
await port.press("ControlOrMeta+A");
await port.press("9");
await port.press("0");
await port.press("0");
await port.press("1");
await expect(page.getByLabel("Client command", { exact: true })).toContainText(":9001/");
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
});
test("inherited values, explicit false, zero and null retain their meanings", async ({ page }) => {
await page.goto("/FastVideo/cookbook?deployment=fastwan21%2Fcuda-rest");
await output(page, "config.yaml");
const yaml = page.getByLabel("Generated YAML", { exact: true });
await expect(yaml).toBeVisible();
await expect(yaml).not.toContainText('"text_encoder":');
await settings(page, "Model");
await chip(page, "Offload text encoder to CPU", "Off");
await expect(yaml).toContainText('"text_encoder": false');
await page
.getByRole("button", {
name: "Use baseline for Offload text encoder to CPU",
exact: true,
})
.click();
await expect(yaml).not.toContainText('"text_encoder":');
await settings(page, "Request");
await page.getByLabel("Seed", { exact: true }).fill("0");
await expect(yaml).toContainText('"seed": 0');
await settings(page, "Serving");
await page.getByRole("button", { name: "Set Served model name to null", exact: true }).click();
await expect(yaml).toContainText('"served_model_name": null');
});
test("catalog discovery opens dedicated model recipes and filters by family and task", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
const cards = page.locator(".recipe-card");
await expect(cards).toHaveCount(34);
await expect(cards.first()).toContainText("FastH3 8-Step V2");
await page.getByLabel("Model family", { exact: true }).selectOption("Wan");
await page.getByLabel("Search recipes").fill("Wan2.1 I2V");
await expect(cards).toHaveCount(1);
await cards.click();
await expect(page).toHaveURL(/cookbook\/wan\/wan21-i2v$/);
await expect(page.getByLabel("Base model")).toHaveCount(0);
await page.getByRole("link", { name: "← All recipes", exact: true }).click();
await page.getByLabel("Search recipes").fill("FastWan2.1");
await page.locator(".recipe-card").click();
await expect(page).toHaveURL(/cookbook\/wan\/fastwan21$/);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("FastWan2.1 1.3B");
});
test("hardware preferences stay distinct from schema-backed GPU count and topology", async ({ page }) => {
await page.goto(route);
await page.getByRole("radio", { name: "RTX PRO 6000" }).check();
await expect(page).toHaveURL(/hardware=rtx/);
await settings(page, "Hardware");
await page.getByLabel("Tensor parallelism", { exact: true }).fill("2");
await page.getByRole("button", { name: "Use automatic parallelism" }).click();
await expect(page.getByLabel("Tensor parallelism", { exact: true })).toHaveValue("-1");
await page.getByLabel("GPU count", { exact: true }).fill("1");
await output(page, "config.yaml");
const yaml = page.getByLabel("Generated YAML", { exact: true });
await expect(yaml).toContainText('"num_gpus": 1');
await expect(yaml).not.toContainText('"hardware"');
await expect(page.getByText("Unverified recipe", { exact: true })).toBeVisible();
await page.getByLabel("GPU count", { exact: true }).fill("0");
await expect(yaml).toHaveCount(0);
await expect(page.getByLabel("GPU count", { exact: true })).toHaveAttribute("aria-invalid", "true");
});
test("output tabs support keyboard navigation and precision chips use the backend schema", async ({ page }) => {
await page.goto(route);
const tabs = page.getByRole("tablist", { name: "Output format" });
await tabs.getByRole("tab", { name: "Commands", exact: true }).focus();
await page.keyboard.press("ArrowRight");
await expect(tabs.getByRole("tab", { name: "config.yaml", exact: true })).toHaveAttribute("aria-selected", "true");
await backend(page, "MLX");
await chip(page, "VAE compute precision", "bf16");
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText('"vae_dtype": "bf16"');
await expect(
page.getByRole("group", { name: "Hardware backend" }).getByRole("button", { name: "AMD", exact: true }),
).toHaveCount(0);
await page.goto("/FastVideo/cookbook");
await page.getByLabel("Search recipes").fill("does not exist");
await expect(page.getByRole("heading", { name: "No matching recipes" })).toBeVisible();
await page.getByRole("button", { name: "Clear filters" }).click();
await expect(page.locator(".recipe-card")).toHaveCount(34);
});
test("cookbook cards and controls remain readable in dark mode and reflow at 320px", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });
await page.goto("/FastVideo/cookbook");
await expect(page.locator(".recipe-card").first()).toBeVisible();
await page.setViewportSize({ width: 320, height: 900 });
await expect(page.locator(".recipe-card").first()).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await page.goto(route);
await settings(page, "Serving");
await expect(page.getByLabel("Port", { exact: true })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await expect(page.getByRole("button", { name: "Download YAML", exact: false })).toBeEnabled();
});
test("image and audio models have logos and dedicated source recipe pages", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
await page.getByRole("button", { name: "Image", exact: true }).click();
await expect(page.locator(".recipe-card")).toHaveCount(4);
const card = page.locator(".recipe-card").filter({ hasText: "FLUX.2 Klein" });
await expect(card.locator("img")).toBeVisible();
await card.click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("FLUX.2 Klein 4B");
await expect(page.getByLabel("Inference command")).toContainText("flux");
await expect(
page.getByRole("group", { name: "Run mode" }).getByRole("button", { name: "Python SDK", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("button", { name: "Serving", exact: true })).toHaveCount(0);
await page.getByLabel("Seed", { exact: true }).fill("0");
await expect(page.getByRole("button", { name: "Copy", exact: true })).toBeEnabled();
await page.goto("/FastVideo/cookbook");
await page.getByRole("button", { name: "Audio", exact: true }).click();
await expect(page.locator(".recipe-card")).toHaveCount(3);
await page.locator(".recipe-card").first().click();
await page.getByLabel("Audio duration", { exact: true }).fill("1.5");
await expect(page.getByRole("button", { name: "Copy", exact: true })).toBeEnabled();
});
test("provider labels match FastVideo logos and CompactH3 is replaced by FastH3 Trim", async ({ page }) => {
await page.goto("/FastVideo/cookbook");
const ownedCards = page.locator(".recipe-card").filter({ has: page.locator('img[src$="/fastvideo.webp"]') });
for (const card of await ownedCards.all()) await expect(card.locator(".recipe-card-kicker")).toHaveText("FastVideo");
await page.getByLabel("Search recipes").fill("FastH3 Trim");
await expect(page.locator(".recipe-card")).toHaveCount(1);
await page.locator(".recipe-card").click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("FastH3 Trim");
await expect(page.getByRole("link", { name: "View on Hugging Face ↗", exact: true })).toHaveAttribute(
"href",
/FastVideo-FastH3-Trim-8-Step-NVFP4/,
);
await expect(page.getByRole("button", { name: "Copy YAML", exact: true })).toBeEnabled();
});
test("FastH3 Trim edits drive inference, serving, and complete copyable launch scripts", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: async (text: string) => {
(window as Window & { copiedCommand?: string }).copiedCommand = text;
},
},
});
});
await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-trim");
await expect(page.getByRole("radio", { name: "DGX Spark GB10", exact: true })).toBeChecked();
await expect(page.getByRole("radio", { name: "H100 Data center", exact: true })).toHaveCount(0);
await page.getByLabel("Prompt", { exact: true }).fill("A potter finishes a blue bowl.");
await page.getByLabel("Seed", { exact: true }).fill("0");
await page.getByRole("button", { name: "Copy", exact: true }).click();
const script = await page.evaluate(() => (window as Window & { copiedCommand?: string }).copiedCommand);
expect(script).toContain("cat > config.yaml <<'FASTVIDEO_CONFIG'");
expect(script).toContain("FastVideo-FastH3-Trim-8-Step-NVFP4");
expect(script).toContain('"seed": 0');
expect(script).toContain("FASTVIDEO_VSA_TRITON=1");
expect(script).toContain("fastvideo serve --config config.yaml");
await page.locator(".recipe-request-code summary").click();
await expect(page.getByLabel("Client command", { exact: true })).toContainText("A potter finishes a blue bowl.");
await chip(page, "Run mode", "CLI");
await expect(page.getByLabel("Inference command", { exact: true })).toContainText("fastvideo generate");
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText("request:");
await expect(page.getByLabel("Generated YAML", { exact: true })).not.toContainText("server:");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText("num_inference_steps: 9");
await chip(page, "Run mode", "Python SDK");
await output(page, "Commands");
await expect(page.getByLabel("SDK command", { exact: true })).toContainText("VideoGenerator.from_config");
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
});
test("source-backed controls update the copied command without changing models", async ({ page }) => {
await page.goto("/FastVideo/cookbook/minimax-h3/models/minimax-h3-t2v");
await chip(page, "GPU count", "2");
await expect(page.getByLabel("Inference command", { exact: true })).toContainText("--num-gpus '2'");
});
test("Wan I2V requires an image for CLI and SDK examples without changing server defaults", async ({ page }) => {
await page.goto("/FastVideo/cookbook/wan/wan21-i2v");
await chip(page, "Run mode", "CLI");
await expect(page.getByRole("alert")).toContainText("Enter a source image path");
await expect(page.getByRole("button", { name: "Copy", exact: true })).toBeDisabled();
await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeDisabled();
await page.getByLabel("Source image path", { exact: true }).fill("/data/first-frame.png");
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText("image_path: /data/first-frame.png");
await expect(page.getByRole("button", { name: "Copy", exact: true })).toBeEnabled();
await chip(page, "Run mode", "Python SDK");
await expect(page.getByLabel("Generated YAML", { exact: true })).toContainText("image_path: /data/first-frame.png");
await page.getByLabel("Source image path", { exact: true }).fill("");
await expect(page.getByRole("button", { name: "Copy YAML", exact: true })).toBeDisabled();
await expect(page.getByLabel("Generated YAML", { exact: true })).toHaveCount(0);
await chip(page, "Run mode", "Serving");
await expect(page.getByRole("button", { name: "Copy", exact: true })).toBeEnabled();
await expect(page.getByLabel("Generated YAML", { exact: true })).not.toContainText("image_path");
});
test("source editors retain required model flags and preparation commands", async ({ page }) => {
await page.goto("/FastVideo/cookbook/wan/models/fastmetal-1-3b-mlx");
await expect(page.getByLabel("Height", { exact: true })).toHaveValue("480");
await expect(page.getByLabel("Inference command", { exact: true })).toContainText(
"hf download FastVideo/FastMetal-1.3B-QAD",
);
await expect(page.getByLabel("Inference command", { exact: true })).toContainText(
"--model-root ./FastMetal-1.3B-QAD",
);
await page.getByLabel("Seed", { exact: true }).fill("0");
await expect(page.getByLabel("Inference command", { exact: true })).toContainText("--seed '0'");
await page.getByRole("tab", { name: "Python example", exact: true }).click();
await expect(page.getByLabel("Source example", { exact: true })).toContainText("default=480");
});
test("source-backed serving pages expose their actual server settings", async ({ page }) => {
await page.goto("/FastVideo/cookbook/wan/models/wan22-t2v");
await chip(page, "Run mode", "Serving");
await page.locator(".matrix-advanced > summary").click();
await page.getByLabel("Port", { exact: true }).fill("9005");
await page.getByRole("button", { name: "cURL", exact: true }).click();
await expect(page.getByLabel("Client command", { exact: true })).toContainText(":9005/v1/videos/sync");
await page.getByRole("tab", { name: "config.yaml", exact: true }).click();
await expect(page.getByLabel("Source example", { exact: true })).toContainText("port: 9005");
await expect(page.getByLabel("Source example", { exact: true })).toContainText("Wan2.2-T2V-A14B");
await expect(page.getByRole("button", { name: "Copy", exact: true })).toBeEnabled();
});
test("source model hardware preferences survive hydration and reload", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/FastVideo/cookbook/flux/models/flux2-klein-t2i?hardware=custom%3ARTX%204090");
await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked();
await page.reload();
await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked();
expect(errors).toEqual([]);
});
test("custom devices persist and component previews cannot silently generate incompatible YAML", async ({ page }) => {
await page.goto(route);
await page.getByRole("button", { name: "+ Find or add a GPU", exact: true }).click();
await page.getByLabel("Device name", { exact: true }).fill("RTX 4090");
await page.getByRole("button", { name: "Add device", exact: true }).click();
await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked();
await page.reload();
await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked();
await page.locator(".component-plan > summary").click();
await chip(page, "Text encoder", "NVFP4 encoder");
await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeDisabled();
await expect(page.getByRole("alert")).toContainText("compatible serving recipe");
await page.getByRole("button", { name: "Restore recipe components", exact: true }).click();
await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled();
});
+235
View File
@@ -0,0 +1,235 @@
import { expect, test } from "@playwright/test";
test("GitHub stars refresh automatically and survive a failed refresh", async ({ page }) => {
await page.clock.install();
let requests = 0;
await page.route("https://api.github.com/repos/hao-ai-lab/FastVideo", async (route) => {
requests++;
if (requests === 2) return route.fulfill({ status: 503, body: "Unavailable" });
await route.fulfill({ json: { stargazers_count: requests === 1 ? 4612 : 5234 } });
});
await page.goto("/FastVideo");
const stars = page.locator(".home-github-stars");
await expect(stars).toHaveText("4.6k");
await expect(stars).toHaveAccessibleName("FastVideo on GitHub, 4,612 stars");
await page.clock.fastForward(5 * 60 * 1000);
await expect.poll(() => requests).toBe(2);
await expect(stars).toHaveText("4.6k");
await page.clock.fastForward(5 * 60 * 1000);
await expect(stars).toHaveText("5.2k");
});
test("GitHub rate limits leave a usable Star link without a fabricated count", async ({ page }) => {
await page.route("https://api.github.com/repos/hao-ai-lab/FastVideo", (route) =>
route.fulfill({ status: 403, json: { message: "API rate limit exceeded" } }),
);
await page.goto("/FastVideo");
const stars = page.getByRole("link", { name: "Star FastVideo on GitHub", exact: true });
await expect(stars).toHaveText("Star");
await expect(stars).toHaveAttribute("href", "https://github.com/hao-ai-lab/FastVideo");
});
test("documentation navigation opens transferred guides", async ({ page }) => {
await page.goto("/FastVideo");
await expect(page.getByRole("heading", { name: "Welcome to FastVideo", exact: true })).toBeVisible();
await page.locator("#blume-nav-drawer").getByRole("link", { name: "Installation", exact: true }).click();
await expect(page).toHaveURL(/getting_started\/installation/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});
test("the welcome page introduces the project and opens the cookbook", async ({ page }) => {
await page.goto("/FastVideo");
const logo = page.getByRole("heading", { level: 1, name: "Welcome to FastVideo" }).getByRole("img");
await expect(logo).toBeVisible();
expect(await logo.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true);
const column = await page.locator(".home-page").boundingBox();
const image = await logo.boundingBox();
expect(Math.abs(image!.x + image!.width / 2 - (column!.x + column!.width / 2))).toBeLessThan(2);
await expect(page.getByRole("link", { name: "Hao AI Lab", exact: true })).toHaveAttribute(
"href",
"https://haoailab.com/",
);
const lab = page.locator(".home-lab-link img");
expect(await lab.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true);
const trending = page.locator(".home-trendshift a");
await expect(trending).toHaveAttribute(
"href",
"https://trendshift.io/repositories/14509?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-14509",
);
await expect(trending.locator("img")).toHaveAttribute("src", "https://trendshift.io/api/badge/repositories/14509");
await expect(page.locator(".home-news time")).toHaveCount(2);
await expect(page.locator(".home-news")).toContainText("Kandinsky 6");
await expect(page.getByRole("heading", { name: "Projects and research", exact: false })).toBeVisible();
await expect(page.getByRole("link", { name: "DanceGRPO", exact: true })).toBeVisible();
await expect(page.getByText("Use the navigation menu on the left to explore different sections:")).toHaveCount(0);
await expect(page.getByText("About FastVideo and performance overview", { exact: true })).toHaveCount(0);
await expect(page.locator(".home-page > p").first()).toContainText(
"FastVideo is an inference and post-training framework",
);
await page.getByRole("link", { name: /^Cookbook Choose a model recipe/ }).click();
await expect(page).toHaveURL(/FastVideo\/cookbook/);
await expect(page.getByRole("heading", { name: "Cookbook", exact: true })).toBeVisible();
});
test("Fast Models connects the homepage, family collections, and checkpoints", async ({ page }) => {
await page.goto("/FastVideo");
await page.locator(".home-project-links").getByRole("link", { name: "Fast Models", exact: true }).click();
await expect(page).toHaveURL(/FastVideo\/fast-models$/);
await expect(page.getByRole("heading", { level: 1, name: "Fast Models", exact: true })).toBeVisible();
for (const family of ["FastH3", "FastWan", "FastMetal"]) {
await expect(page.getByRole("heading", { level: 2, name: family, exact: false })).toBeVisible();
}
await expect(page.getByRole("link", { name: "FastMetal collection", exact: true })).toHaveAttribute(
"href",
"https://huggingface.co/collections/FastVideo/fastmetal",
);
await expect(page.locator('a[href="https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B"]').first()).toBeVisible();
const checkpoints = await page
.locator('a[href^="https://huggingface.co/FastVideo/"]')
.evaluateAll((links) => [...new Set(links.map((link) => link.getAttribute("href")))]);
expect(checkpoints.length).toBeGreaterThanOrEqual(18);
await expect(
page.locator("#blume-nav-drawer").getByRole("link", { name: "Fast Models", exact: true }),
).toHaveAttribute("aria-current", "page");
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
});
test("V1 API renders Python signatures within the Blume shell", async ({ page }) => {
await page.goto("/FastVideo/getting_started/v1_api");
await expect(page.getByRole("heading", { name: "V1 API", exact: true })).toBeVisible();
await expect(page.locator('[id="fastvideo.entrypoints.video_generator.VideoGenerator"]')).toBeVisible();
await expect(page.locator(".python-reference")).toContainText("from_pretrained");
for (const description of [
"The inference arguments",
"The executor class to use for inference",
"Whether to log statistics",
"Optional multiprocessing.Queue to forward worker logs to",
]) {
await expect(page.locator(".python-reference")).toContainText(description);
}
await expect(page.getByRole("button", { name: "Search", exact: true })).toBeVisible();
});
test("the API overview resolves Python references and guide links", async ({ page }) => {
await page.goto("/FastVideo/api");
const reference = page.locator(".python-reference");
await expect(reference.getByRole("link", { name: "API contracts and sampling", exact: true })).toHaveAttribute(
"href",
"/FastVideo/api/fastvideo/api#fastvideo.api",
);
await reference.getByRole("link", { name: "fastvideo.VideoGenerator", exact: true }).click();
await expect(page.locator('[id="fastvideo.VideoGenerator"]')).toBeVisible();
await page.goto("/FastVideo/api");
await page.locator(".python-reference").getByRole("link", { name: "V1 API", exact: true }).click();
await expect(page).toHaveURL(/\/FastVideo\/getting_started\/v1_api$/);
await expect(page.getByRole("heading", { name: "V1 API", exact: true })).toBeVisible();
});
test("generated module links stay inside the transferred Python reference", async ({ page }) => {
await page.goto("/FastVideo/api/fastvideo");
await page.getByRole("link", { name: "configs", exact: true }).click();
await expect(page).toHaveURL(/FastVideo\/api\/fastvideo\/configs/);
});
test("search finds a transferred guide and opens it by keyboard", async ({ page }) => {
await page.goto("/FastVideo");
await page.getByRole("button", { name: "Search", exact: true }).click();
const query = page.getByRole("combobox", { name: "Search docs", exact: true });
await query.fill("offloading");
await expect(page.getByRole("option", { name: /^Offloading/ }).first()).toBeVisible();
await query.press("Enter");
await expect(page).toHaveURL(/FastVideo\/inference\/offloading/);
});
test("the welcome cards reflow on mobile without horizontal scrolling", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/FastVideo");
await expect(page.getByRole("heading", { name: "Welcome to FastVideo", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: /Quick start Generate/ })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
});
test("the model catalog filters by family and checkpoint name", async ({ page }) => {
await page.goto("/FastVideo/fast-models");
const catalog = page.locator(".model-catalog");
await expect(catalog.getByRole("status")).toHaveText("18 checkpoints");
await page.getByRole("button", { name: "FastMetal", exact: true }).click();
await expect(catalog.getByRole("status")).toHaveText("3 checkpoints");
await page.getByRole("searchbox", { name: "Search checkpoints", exact: true }).fill("14B");
await expect(catalog.getByRole("status")).toHaveText("1 checkpoint");
await page.getByRole("searchbox").fill("no-such-model");
await expect(catalog.getByRole("status")).toHaveText("0 checkpoints");
await page.getByRole("button", { name: "Clear filters", exact: true }).click();
await expect(catalog.getByRole("status")).toHaveText("18 checkpoints");
});
test("hardware guides are grouped below general inference tasks", async ({ page }) => {
await page.goto("/FastVideo");
const nav = page.locator("#blume-nav-drawer");
await expect(nav.getByRole("link", { name: "Two DGX Sparks", exact: true })).not.toBeVisible();
await nav.getByText("Hardware", { exact: true }).click();
await nav.getByRole("link", { name: "Two DGX Sparks", exact: true }).click();
await expect(page).toHaveURL(/getting_started\/installation\/spark_pair/);
});
test("family runbooks use React model cards without the old cookbook runtime", async ({ page }) => {
await page.goto("/FastVideo/cookbook/wan/");
await expect(page.locator(".recipe-card").first()).toBeVisible();
await expect(page.getByRole("button", { name: "Search", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "← All models", exact: true })).toHaveAttribute(
"href",
/\/FastVideo\/cookbook$/,
);
await expect(page.locator('script[src*="/assets/cookbook.js"],link[href*="cookbook-runbooks.css"]')).toHaveCount(0);
});
test("generated example titles render once and retain their original fragment", async ({ page }) => {
await page.goto("/FastVideo/inference/examples/basic");
await expect(page.getByRole("heading", { name: "Basic Video Generation Tutorial", exact: true })).toHaveCount(1);
await expect(page.locator('[id="basic-video-generation-tutorial"]')).toHaveCount(1);
await expect(page.locator("article")).toContainText("examples/inference/basic");
});
test("hardware overview distinguishes runtime support and AMD documentation limits", async ({ page }) => {
await page.goto("/FastVideo/getting_started/hardware");
const table = page.getByRole("table");
for (const platform of ["RTX", "data center", "DGX Spark", "Apple Silicon", "AMD"]) {
await expect(table).toContainText(platform);
}
await page.getByRole("link", { name: "ROCm support and current limits", exact: true }).click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("AMD and ROCm");
await expect(page.locator("article")).toContainText("not currently documented");
});
test("homepage project buttons have visible borders in dark mode", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });
await page.goto("/FastVideo");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
const colors = await page.locator(".home-project-links a").evaluateAll((links) =>
links.map((link) => {
const style = getComputedStyle(link);
return {
border: style.borderTopColor,
width: style.borderTopWidth,
background: getComputedStyle(document.body).backgroundColor,
};
}),
);
expect(colors).toHaveLength(3);
for (const color of colors) {
expect(color.width).toBe("1px");
const luminance = (value: string) => {
const channels = value
.match(/[\d.]+/g)!
.slice(0, 3)
.map(Number)
.map((channel) => (value.startsWith("color(srgb") ? channel : channel / 255))
.map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4));
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
};
const border = luminance(color.border);
const background = luminance(color.background);
expect((Math.max(border, background) + 0.05) / (Math.min(border, background) + 0.05)).toBeGreaterThanOrEqual(3);
}
});
+121
View File
@@ -0,0 +1,121 @@
"""Regression checks for source-only API rendering, without importing model code."""
import ast
import importlib.util
from pathlib import Path
import tempfile
import unittest
import griffe
SPEC = importlib.util.spec_from_file_location("python_reference", Path(__file__).parents[1] / "scripts/prepare-python-docs.py")
assert SPEC is not None and SPEC.loader is not None
renderer = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(renderer)
class PythonReferenceTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
root = Path(self.temporary.name)
(root / "sample.py").write_text('''raise RuntimeError("This module must never be imported")
def generate(prompt: str, /, count: int = 0, *, audio: bool = False, **kwargs) -> str:
"""Generate an output.
Args:
prompt: Text describing the video.
count: Number of outputs.
audio: Include audio.
"""
return {"outputs": []}["outputs"](prompt)
class Generator:
"""A configurable generator."""
def __init__(self, count: int = 0):
"""Initialize the generator.
Args:
count: Number of videos to generate.
"""
self.count = count
def run(self):
"""Generate the videos."""
class Empty:
"""A class without an explicit constructor."""
''')
(root / "public.py").write_text("from sample import Generator\n")
loader = griffe.GriffeLoader(search_paths=[root], allow_inspection=False, docstring_parser="google")
self.module = loader.load("sample")
self.public = loader.load("public")
loader.resolve_aliases(implicit=True, external=False)
def test_source_is_read_without_executing_module(self):
self.assertIn("generate", self.module.members)
rendered = renderer.symbol(self.module["generate"], "sample.generate")
self.assertIn('id="sample.generate"', rendered)
self.assertIn("Text describing the video", rendered)
self.assertIn("<summary>Source</summary>", rendered)
def test_signature_preserves_parameter_kinds_zero_and_false(self):
self.assertEqual(renderer.signature(self.module["generate"]),
"generate(prompt: str, /, count: int = 0, *, audio: bool = False, **kwargs) -> str")
def test_source_brackets_cannot_be_misread_as_markdown_links(self):
rendered = renderer.symbol(self.module["generate"], "sample.generate")
self.assertNotIn('[]', rendered)
self.assertIn("&#91;", rendered)
def test_example_controls_edit_literals_without_imports_and_use_javascript_offsets(self):
source = '''raise RuntimeError("Never execute this example 🥣")
PROMPT = "A bowl"
generator = VideoGenerator.from_pretrained("Example/Model", num_gpus=1)
parser.add_argument("--num-frames", type=int, default=124)
parser.add_argument("--fps", type=float, default=24)
generator.generate_video(height=480, prompt=PROMPT)
'''
controls = renderer.source_controls(source)
self.assertEqual({item["name"] for item in controls}, {"prompt", "num_gpus", "num_frames", "height", "fps"})
self.assertFalse(next(item for item in controls if item["name"] == "fps")["integer"])
frames = next(item for item in controls if item["name"] == "num_frames")
self.assertEqual(frames["flag"], "--num-frames")
for item in controls:
for start, end in item["spans"]:
literal = source.encode("utf-16-le")[start * 2:end * 2].decode("utf-16-le")
self.assertEqual(ast.literal_eval(literal), item["value"])
def test_class_includes_constructor_documentation_without_a_separate_method(self):
for obj, path in [(self.module["Generator"], "sample.Generator"),
(self.public["Generator"], "public.Generator")]:
with self.subTest(path=path):
rendered = renderer.symbol(obj, path, source=False)
self.assertIn("A configurable generator.", rendered)
self.assertEqual(rendered.count("Number of videos to generate."), 1)
self.assertIn('<dt><code>count: int</code></dt>', rendered)
self.assertNotIn('__init__', rendered)
self.assertIn(f'id="{path}.run"', rendered)
self.assertIn("A class without an explicit constructor.",
renderer.symbol(self.module["Empty"], "sample.Empty", source=False))
def test_overview_resolves_module_symbol_and_public_alias_references(self):
rendered = renderer.api_overview("[Package][sample]\n\n[`sample.Generator`][]", self.module)
self.assertIn('href="/api/sample#sample"', rendered)
self.assertIn('href="/api/sample#sample.Generator"', rendered)
rendered = renderer.api_overview("[`public.Generator`][]", self.public)
self.assertIn('href="/api/public#public.Generator"', rendered)
def test_overview_converts_guide_paths_and_preserves_external_links(self):
rendered = renderer.api_overview(
"[Guide](../getting_started/v1_api.md?mode=full#configuration)\n\n"
"[Overview](../training/index.md)\n\n[External](https://example.com/guide.md)", self.module)
self.assertIn('href="/getting_started/v1_api?mode=full#configuration"', rendered)
self.assertIn('href="/training"', rendered)
self.assertIn('href="https://example.com/guide.md"', rendered)
if __name__ == "__main__":
unittest.main()
+1280
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"resolveJsonModule": true
},
"include": [
"blume.config.ts",
"islands/**/*.tsx",
"components/**/*.tsx",
"lib/**/*.ts",
"tests/**/*.ts",
"playwright.config.ts"
]
}
+17 -101
View File
@@ -1,113 +1,29 @@
# FastVideo Documentation
# FastVideo documentation
This directory contains the FastVideo documentation built with MkDocs.
Canonical guides live in this directory. The Blume application in
[docs-site](../docs-site/README.md) builds and publishes them at
[haoailab.com/FastVideo](https://haoailab.com/FastVideo/).
## Build the docs locally
See [Documenting FastVideo](contributing/documentation.md) for local setup,
source ownership, page structure, validation, and deployment. The existing
GitHub Pages workflow publishes Blume from `main` with the same domain and base
path. MkDocs is no longer part of the build.
```bash
# Install docs dependencies
uv pip install -r requirements-mkdocs.txt
npm ci --prefix docs
## Cookbook sources
# Build the browser API/validator bundle and generate cookbook catalogs
npm run build:catalog --prefix docs
# Serve docs with live reload (recommended for development)
mkdocs serve
# Or build static site
mkdocs build
```
Run these commands from the repository root with Node 22.13+ (22.x) or Node 24+. Catalog generation uses authored
YAML and Node dependencies; it does not import FastVideo or install model runtime
dependencies. `mkdocs build` and `mkdocs serve` use the generated files without
regenerating them. A small MkDocs hook checks required assets before and after
the build and explains how to prepare them if missing. Rerun `build:catalog`
after changing cookbook sources. The npm `prebuild:catalog` and `pretest:cookbook`
hooks automatically run `build:assets`, including in CI. The combined
`docs/assets/cookbook-config/cookbook-config.js`, its
`cookbook-config.LICENSE.txt`, index and catalogs are Git-ignored and published
together; do not hand-edit or commit them. Handwritten modules live in `docs/js/`
and tests in `docs/tests/`.
## View the docs
### Development server (with live reload)
```bash
mkdocs serve
```
Then open your browser to: http://127.0.0.1:8000
### Static build
```bash
mkdocs build
python -m http.server -d site/
```
Then open your browser to: http://localhost:8000
## Automatic Deployment
Documentation is automatically built and deployed to GitHub Pages when relevant
changes are pushed to the `main` branch via `.github/workflows/infra-docs.yml`.
Pull requests run the build without deploying. The normal docs job installs
`requirements-mkdocs.txt` and the Node dependencies with `npm ci --prefix docs`,
runs scoped lint/format checks, cookbook tests and asset-check tests, generates catalogs with `npm run build:catalog --prefix docs`,
and runs `mkdocs build`. Invalid authored
metadata stops the build. The generated JSON remains Git-ignored but is copied
to `site/assets/cookbook-config/` for publication. The source-only
`docs/cookbook/options.yaml` and recipe manifests are excluded from the site.
No separate metadata job or runtime server is required.
## Update documentation dependencies
Edit `requirements-mkdocs.in`, then regenerate the pinned Linux/Python 3.12 lock file:
```bash
uv pip compile requirements-mkdocs.in \
-o requirements-mkdocs.txt \
--python-platform x86_64-manylinux_2_28 \
--python-version 3.12
```
## Local cookbook catalogs
Generate catalogs from the repository root:
Recipe metadata, shared JavaScript, and tests remain under `docs/`. From the
repository root:
```bash
npm ci --prefix docs
npm run build:catalog --prefix docs
```
This command builds the combined API/validator bundle first. Rerun it whenever
shared options, recipe manifests or baseline YAML change. Then `mkdocs build`
or `mkdocs serve` includes the generated assets. Generation loads no model
weights and performs no inference.
Inspect a deployment without a browser UI:
```bash
node docs/js/build-cookbook-config.mjs --preview fastwan21/cuda-rest
```
See [the contributor guide](contributing/cookbook_configuration.md) for recipe
authoring and local test commands, and [the API contract](cookbook/design.md)
for using the same catalogs from a cookbook UI.
## Check cookbook changes
```bash
npm run test:cookbook --prefix docs
npm run check:cookbook --prefix docs
npm run test:cookbook --prefix docs
python -m unittest discover -s docs/tests -p 'test_*.py'
```
`npm run format:cookbook --prefix docs` formats handwritten cookbook modules,
build scripts, tooling configuration and Node tests under `docs/`. The check
commands do not change files. The Node tests use synthetic fixtures for reusable
behavior and discover every production recipe for integration checks. They do
not launch serving processes.
The combined browser bundle, notices, index and catalogs are generated together
under `assets/cookbook-config/`. Do not hand-edit or commit those outputs.
The site preparation builds and copies them into Blume's public assets.
See the [recipe authoring guide](contributing/cookbook_configuration.md) and
[configuration API contract](cookbook/design.md) before changing the schema.
+26 -72
View File
@@ -1,5 +1,5 @@
{
"version": 16,
"version": 17,
"recipes": [
{
"id": "fastwan21-t2v",
@@ -603,77 +603,6 @@
"Height, width, frames, and steps in the YAML are examples. Edit them or pass CLI flags. See docs/getting_started/installation/spark_pair.md."
]
},
{
"id": "compacth3-rtx5090",
"group": "compacth3-rtx5090",
"group_label": "CompactH3 on RTX 5090",
"group_task": "4-step 42-block text to video + audio",
"family": "minimax_h3",
"stage": "inference",
"task": "Few-step text to video (with audio)",
"label": "CompactH3 NVFP4 on RTX 5090",
"summary": "Run the 42-block CompactH3 NVFP4 DiT with the NVFP4 Qwen3-VL encoder, Comfy int8-convrot VAE, and SageAttention3 FP4 on one 32 GB RTX 5090. Sequential load parks the encoder in pinned host RAM.",
"model": "./CompactH3",
"source": "examples/inference/basic/basic_compacth3_rtx5090.yaml",
"serving": {
"source": "examples/serving/openai_compacth3_rtx5090.yaml",
"install": "UV_TORCH_BACKEND=cu128 uv pip install -e \".[fasth3]\"",
"prepare": "hf download FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree --local-dir ./CompactH3 --include model_index.json --include \"tokenizer/**\" --include \"processor/**\" --include \"scheduler/**\" --include \"audio_scheduler/**\" --include \"audio_vae/**\" --include \"vae/**\"\nhf download aryan5v/FastH3-20B-42block-dmd2-ckpt1400-bf16 --local-dir ./CompactH3/transformer\nhf download aryan5v/FastH3-20B-42block-dmd2-ckpt1400-nvfp4 --local-dir ./CompactH3/transformer --include nvfp4_weights.safetensors\nhf download KyleNeverGivesUp/FastH3-text-encoder-nvfp4 --local-dir ./CompactH3/text_encoder\nhf download Comfy-Org/MiniMax-H3 --local-dir ./Comfy-MiniMax-H3 --include vae/minimax_h3_video_vae_int8_convrot.safetensors\ncp ./Comfy-MiniMax-H3/vae/minimax_h3_video_vae_int8_convrot.safetensors ./CompactH3/vae/",
"env": "FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_INFER FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FLASHINFER_CUDA_ARCH_LIST=12.0a"
},
"command": "FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_INFER FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FLASHINFER_CUDA_ARCH_LIST=12.0a FASTVIDEO_STAGE_LOGGING=1 fastvideo generate --config examples/inference/basic/basic_compacth3_rtx5090.yaml",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"evidence": "source-configured"
},
"evidence": "Source-backed",
"expected_artifact": "MP4 under outputs/compacth3_rtx5090/",
"modes": ["T2VA", "CompactH3 NVFP4", "RTX 5090"],
"limitations": [
"Assemble ./CompactH3 before running. The DiT NVFP4 export and encoder snapshot are gated; run huggingface-cli login and accept each repo license.",
"32 GB cannot keep the NVFP4 encoder and DiT on the GPU together. Keep h3_sequential_load on and lazy_module_load off.",
"Blackwell sm_120 needs ATTN_QAT_INFER, FLASHINFER_CUDA_ARCH_LIST=12.0a, and a CUDA 12.8 PyTorch wheel.",
"Legal num_frames values are 17n+5, capped at 362 (15.08 s). Native 16:9 sizes include 832x480 and 1344x768; 1344x768 on 32 GB is unmeasured."
]
},
{
"id": "compacth3-rtx-pro6000",
"group": "compacth3-rtx-pro6000",
"group_label": "CompactH3 on RTX PRO 6000",
"group_task": "4-step 42-block text to video + audio",
"family": "minimax_h3",
"stage": "inference",
"task": "Few-step text to video (with audio)",
"label": "CompactH3 NVFP4 on RTX PRO 6000 Blackwell",
"summary": "Run CompactH3 NVFP4 with the encoder, DiT, and int8-convrot VAE resident on one 96 GB RTX PRO 6000 Blackwell. The checked-in example is 1344x768 and 124 frames (5.17 s) with VAE torch.compile. Use 832x480 for clip-queue playground traffic.",
"model": "./CompactH3",
"source": "examples/inference/basic/basic_compacth3_rtx_pro6000.yaml",
"serving": {
"source": "examples/serving/openai_compacth3_rtx_pro6000.yaml",
"install": "UV_TORCH_BACKEND=cu128 uv pip install -e \".[fasth3]\"",
"prepare": "hf download FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree --local-dir ./CompactH3 --include model_index.json --include \"tokenizer/**\" --include \"processor/**\" --include \"scheduler/**\" --include \"audio_scheduler/**\" --include \"audio_vae/**\" --include \"vae/**\"\nhf download aryan5v/FastH3-20B-42block-dmd2-ckpt1400-bf16 --local-dir ./CompactH3/transformer\nhf download aryan5v/FastH3-20B-42block-dmd2-ckpt1400-nvfp4 --local-dir ./CompactH3/transformer --include nvfp4_weights.safetensors\nhf download KyleNeverGivesUp/FastH3-text-encoder-nvfp4 --local-dir ./CompactH3/text_encoder\nhf download Comfy-Org/MiniMax-H3 --local-dir ./Comfy-MiniMax-H3 --include vae/minimax_h3_video_vae_int8_convrot.safetensors\ncp ./Comfy-MiniMax-H3/vae/minimax_h3_video_vae_int8_convrot.safetensors ./CompactH3/vae/",
"env": "FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_INFER FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FLASHINFER_CUDA_ARCH_LIST=12.0a"
},
"command": "FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_INFER FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FLASHINFER_CUDA_ARCH_LIST=12.0a FASTVIDEO_STAGE_LOGGING=1 fastvideo generate --config examples/inference/basic/basic_compacth3_rtx_pro6000.yaml",
"gpu_types": ["NVIDIA"],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"evidence": "source-configured"
},
"evidence": "Source-backed",
"expected_artifact": "MP4 under outputs/compacth3_rtx_pro6000/",
"modes": ["T2VA", "CompactH3 NVFP4", "RTX PRO 6000"],
"limitations": [
"Assemble ./CompactH3 before running. The DiT NVFP4 export and encoder snapshot are gated; run huggingface-cli login and accept each repo license.",
"96 GB keeps the encoder, DiT, and VAE on GPU. Do not enable h3_sequential_load or lazy_module_load on this box.",
"Enable compile.vae_enabled. Leave inference_torch_compile off: FlashInfer and Sage3 custom ops cannot be compiled.",
"Blackwell sm_120 needs ATTN_QAT_INFER, FLASHINFER_CUDA_ARCH_LIST=12.0a, and a CUDA 12.8 PyTorch wheel.",
"Legal num_frames values are 17n+5, capped at 362 (15.08 s). Native 16:9 sizes include 832x480 and 1344x768. Dense CompactH3 has zero VSA gates; the pipeline raises if VIDEO_SPARSE_ATTN_H3 is loaded with all-zero to_gate_compress weights."
]
},
{
"id": "fasth3-8step-v2-cuda",
"group": "fasth3-8step-v2",
@@ -903,6 +832,31 @@
"evidence": "Source-backed",
"expected_artifact": "MP4 videos under video_samples_matrixgame3/",
"related": ["matrix-game-2"]
},
{
"id": "fasth3-trim-spark",
"family": "minimax_h3",
"stage": "inference",
"task": "Text to video with audio",
"label": "FastH3 Trim",
"summary": "Eight-step video and synchronized audio with the released NVFP4 transformer, text encoder, and lightweight VAE.",
"model": "FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4",
"source": "examples/inference/basic/basic_fasth3_spark_pruned_nvfp4.yaml",
"command": "FASTVIDEO_MINIMAX_H3_FUSIONS=all FASTVIDEO_NVFP4_MM_BACKEND=cutlass FASTVIDEO_H3_VAE_TILE_BATCH=1 FASTVIDEO_VSA_TRITON=1 FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 FASTVIDEO_STAGE_LOGGING=1 fastvideo generate --config examples/inference/basic/basic_fasth3_spark_pruned_nvfp4.yaml",
"gpu_types": [
"NVIDIA"
],
"hardware": {
"platform": "cuda",
"gpu_count": 1,
"device": "spark",
"evidence": "source-configured"
},
"evidence": "Source-backed",
"serving": {
"source": "examples/serving/openai_fasth3_trim_spark.yaml",
"install": "Install FastVideo following the DGX Spark installation guide."
}
}
]
}
-746
View File
@@ -1,746 +0,0 @@
(() => {
const PATTERN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const PATTERN_LENGTH = 900;
const SCRAMBLE_MS = 140;
const loadRecipes = (url) =>
fetch(url).then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
const generatePattern = (length) => {
const chars = new Array(length);
for (let index = 0; index < length; index += 1) {
chars[index] = PATTERN_CHARS.charAt((Math.random() * PATTERN_CHARS.length) | 0);
}
return chars.join("");
};
const motionQuery = () =>
window.matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)");
const initEvervault = (root) => {
root.querySelectorAll("[data-evervault]").forEach((visual) => {
if (visual.dataset.evervaultReady) return;
visual.dataset.evervaultReady = "true";
const noise = visual.querySelector("[data-cookbook-pattern]");
if (!noise) return;
const query = motionQuery();
if (!query.matches) return;
let rect = null;
let pointerX = 0;
let pointerY = 0;
let frame = 0;
let lastScramble = 0;
let patternReady = false;
const paint = (scramble) => {
visual.style.setProperty("--mouse-x", `${pointerX}px`);
visual.style.setProperty("--mouse-y", `${pointerY}px`);
if (scramble) noise.textContent = generatePattern(PATTERN_LENGTH);
frame = 0;
};
const onEnter = () => {
rect = visual.getBoundingClientRect();
if (!patternReady) {
noise.textContent = generatePattern(PATTERN_LENGTH);
patternReady = true;
}
};
const onMove = (event) => {
if (!rect) rect = visual.getBoundingClientRect();
pointerX = event.clientX - rect.left;
pointerY = event.clientY - rect.top;
if (frame) return;
frame = window.requestAnimationFrame(() => {
const now = performance.now();
const scramble = now - lastScramble >= SCRAMBLE_MS;
if (scramble) lastScramble = now;
paint(scramble);
});
};
const onLeave = () => {
if (frame) window.cancelAnimationFrame(frame);
frame = 0;
rect = null;
pointerX = 0;
pointerY = 0;
visual.style.setProperty("--mouse-x", "50%");
visual.style.setProperty("--mouse-y", "50%");
};
visual.addEventListener("mouseenter", onEnter);
visual.addEventListener("mousemove", onMove);
visual.addEventListener("mouseleave", onLeave);
});
};
let familyPopstate = null;
const bindFamilyPopstate = () => {
if (bindFamilyPopstate.bound) return;
bindFamilyPopstate.bound = true;
window.addEventListener("popstate", () => {
if (typeof familyPopstate === "function") familyPopstate();
});
};
const groupIdFor = (recipe) => recipe.group || recipe.id;
const gpuCountLabel = (hardware, runtimeId) => {
const count = hardware?.gpu_count;
if (count == null) return "";
if (runtimeId === "spark") return `${count} Spark${count === 1 ? "" : "s"}`;
return `${count} GPU${count === 1 ? "" : "s"}`;
};
const knobsFor = (recipe) => recipe.knobs || [];
const knobOptions = (knob) =>
knob.options.map((option) =>
option !== null && typeof option === "object" ? option : { value: option, label: String(option) },
);
const knobDefaultLabel = (knob) => {
const match = knobOptions(knob).find((option) => option.value === knob.default);
return match ? match.label : String(knob.default);
};
// Knob flags are always shown explicitly in the displayed command, even at
// their default value, so the command stays copy-pasteable and precise
// about what it runs -- not just "trust the script's own default".
const appendKnobFlags = (commandText, knobs, knobValues) => {
const flags = knobs
.filter((knob) => knobValues[knob.key] !== undefined)
.map((knob) => `${knob.flag} ${knobValues[knob.key]}`);
if (!flags.length) return commandText;
const lines = commandText.split("\n");
lines[lines.length - 1] = `${lines[lines.length - 1]} ${flags.join(" ")}`;
return lines.join("\n");
};
const runtimeFor = (recipe) => {
const platform = recipe.hardware?.platform || "cuda";
if (platform === "mlx") {
return {
id: "mlx",
label: "Apple Silicon · MLX",
hint:
recipe.hardware?.minimum_memory ||
[recipe.hardware?.accelerator, recipe.hardware?.system_memory].filter(Boolean).join(" · ") ||
"Memory not recorded",
};
}
if (platform === "mps") {
return {
id: "mps",
label: "Apple Silicon · PyTorch MPS",
hint: recipe.hardware?.minimum_memory || recipe.hardware?.system_memory || "Memory not recorded",
};
}
const hardware = recipe.hardware || {};
if (hardware.device === "spark") {
return {
id: "spark",
label: "NVIDIA DGX Spark",
hint: "GB10 · 128 GB unified memory",
};
}
return {
id: "cuda",
label: "NVIDIA CUDA",
hint: hardware.accelerator
? `${hardware.accelerator} · ${gpuCountLabel(hardware, "cuda")}`
: `${gpuCountLabel(hardware, "cuda")} configured · GPU model not recorded`,
};
};
const runtimeSummary = (recipe) => {
const runtime = runtimeFor(recipe);
const hardware = recipe.hardware || {};
if (runtime.id === "mlx") {
return [hardware.accelerator || "Apple Silicon", hardware.system_memory || hardware.minimum_memory, "MLX"]
.filter(Boolean)
.join(" · ");
}
if (runtime.id === "mps") {
return [hardware.accelerator || "Apple Silicon", hardware.system_memory || hardware.minimum_memory, "PyTorch MPS"]
.filter(Boolean)
.join(" · ");
}
if (runtime.id === "spark") {
return [hardware.accelerator || "NVIDIA GB10", gpuCountLabel(hardware, "spark")].filter(Boolean).join(" · ");
}
if (hardware.accelerator) return [hardware.accelerator, gpuCountLabel(hardware, runtime.id)].join(" · ");
return `NVIDIA CUDA · ${gpuCountLabel(hardware, "cuda")} configured · GPU model and VRAM not recorded`;
};
const renderHardwareEvidence = (container, badge, recipe) => {
const hardware = recipe.hardware || {};
const runtime = runtimeFor(recipe);
const isValidated = hardware.evidence === "validated";
container.classList.toggle("cookbook-hardware-state--verified", isValidated);
container.classList.toggle("cookbook-hardware-state--source", !isValidated);
badge.classList.toggle("cookbook-badge--verified", isValidated);
badge.classList.toggle("cookbook-badge--configured", !isValidated);
badge.textContent = isValidated ? "Recorded run" : "Source config";
const heading = document.createElement("strong");
heading.textContent = isValidated ? "Recorded hardware" : "Source configuration";
const details = document.createElement("span");
if (isValidated) {
const recorded = [
hardware.accelerator,
runtime.id === "mlx" || runtime.id === "mps" ? hardware.system_memory : gpuCountLabel(hardware, runtime.id),
].filter(Boolean);
const statements = [`${recorded.join(" · ")}.`];
if (hardware.minimum_memory) statements.push(`Documented minimum: ${hardware.minimum_memory}.`);
if (hardware.peak_memory) statements.push(`Measured: ${hardware.peak_memory}.`);
if (!hardware.minimum_memory) statements.push("This recorded device is not a minimum requirement.");
details.textContent = ` ${statements.join(" ")}`;
} else if (runtime.id === "spark") {
details.textContent = ` NVIDIA DGX Spark · ${gpuCountLabel(hardware, "spark")}. GB10 has no FA4 / sm_100a VSA kernel; keep Triton VSA and FA4 off.`;
} else if (runtime.id === "cuda") {
details.textContent = ` NVIDIA CUDA · ${gpuCountLabel(hardware, "cuda")}. The source does not record the GPU model or VRAM.`;
} else {
details.textContent = ` ${runtime.label}. The source does not record a device or memory requirement.`;
}
container.replaceChildren(heading, details);
if (hardware.evidence_url) {
const evidenceLink = document.createElement("a");
evidenceLink.href = hardware.evidence_url;
evidenceLink.textContent = "View run evidence";
evidenceLink.setAttribute("aria-label", `View recorded hardware evidence for ${recipe.label}`);
container.append(" ", evidenceLink);
}
};
const compactLifecycle = (root) => {
const lifecycle = root.querySelector(".cookbook-lifecycle");
if (!lifecycle || lifecycle.dataset.compact) return;
lifecycle.dataset.compact = "true";
const stages = [...lifecycle.querySelectorAll(".cookbook-lifecycle__stage")];
const active = stages.find((stage) => stage.classList.contains("cookbook-lifecycle__stage--active"));
const planned = stages
.filter((stage) => stage !== active)
.map((stage) => stage.childNodes[0]?.textContent?.trim())
.filter(Boolean);
const summary = document.createElement("span");
summary.className = "cookbook-lifecycle__summary";
summary.textContent = `Next: ${planned.join(", ")}`;
lifecycle.replaceChildren(...(active ? [active] : []), summary);
};
const initFamilyBuilder = async (root) => {
const family = root.dataset.family;
if (!family) return;
compactLifecycle(root);
const modelOptions = root.querySelector("[data-cookbook-model-options]");
const hardwareOptions = root.querySelector("[data-cookbook-hardware-options]");
const description = root.querySelector("[data-cookbook-description]");
const label = root.querySelector("[data-cookbook-label]");
const model = root.querySelector("[data-cookbook-model]");
const task = root.querySelector("[data-cookbook-task]");
const hardwareValue = root.querySelector("[data-cookbook-gpus]");
const artifact = root.querySelector("[data-cookbook-artifact]");
const evidenceCell = root.querySelector("[data-cookbook-evidence]");
const source = root.querySelector("[data-cookbook-source]");
const modelLink = root.querySelector("[data-cookbook-model-link]");
const command = root.querySelector("[data-cookbook-command]");
const status = root.querySelector("[data-cookbook-status]");
const hardwareState = root.querySelector("[data-cookbook-hardware-state]");
const hardwareBadge = root.querySelector("[data-cookbook-hardware-badge]");
const count = root.querySelector("[data-cookbook-count]");
const result = root.querySelector(".cookbook-result");
const commandBlock = root.querySelector(".cookbook-command");
const servingPanel = root.querySelector("[data-cookbook-serving]");
const usage = root.querySelector("[data-cookbook-usage]");
const servingAvailability = root.querySelector("[data-cookbook-serving-availability]");
const knobsContainer = root.querySelector("[data-cookbook-knobs]");
const deviceRow = root.querySelector("[data-cookbook-device-row]");
const deviceOptions = root.querySelector("[data-cookbook-device-options]");
const deviceCaption = root.querySelector("[data-cookbook-device-caption]");
modelOptions.setAttribute("aria-label", "Recipe");
hardwareOptions.setAttribute("aria-label", "Runtime");
let recipes;
try {
({ recipes } = await loadRecipes(root.dataset.recipes));
} catch (error) {
if (status) status.textContent = "Recipes could not be loaded. Use the maintained examples link below.";
console.error("Failed to load FastVideo cookbook recipes", error);
return;
}
if (!root.isConnected) return;
const familyRecipes = recipes.filter((recipe) => recipe.family === family);
if (!familyRecipes.length) return;
let servingProfiles = {};
let servingLoadFailed = false;
if (servingPanel && familyRecipes.some((recipe) => recipe.serving)) {
try {
const dataUrl = new URL(root.dataset.recipes, document.baseURI);
servingProfiles = await loadRecipes(new URL("cookbook-serving.json", dataUrl));
} catch (error) {
servingLoadFailed = true;
console.error("Failed to load FastVideo serving profiles", error);
}
}
const byId = new Map(familyRecipes.map((recipe) => [recipe.id, recipe]));
const groups = new Map();
familyRecipes.forEach((recipe) => {
const groupId = groupIdFor(recipe);
if (!groups.has(groupId)) groups.set(groupId, []);
groups.get(groupId).push(recipe);
});
if (count) count.textContent = `${familyRecipes.length} maintained recipes`;
modelOptions.replaceChildren();
groups.forEach((groupRecipes, groupId) => {
const representative = groupRecipes[0];
const option = document.createElement("button");
option.type = "button";
option.dataset.recipeGroup = groupId;
option.setAttribute("aria-pressed", "false");
const optionLabel = document.createElement("strong");
optionLabel.textContent = representative.group_label || representative.label;
const optionTask = document.createElement("span");
optionTask.textContent = representative.group_task || representative.task;
option.append(optionLabel, optionTask);
modelOptions.append(option);
});
const query = new URLSearchParams(window.location.search);
const requestedRecipe = query.get("recipe");
const defaultRecipeId = byId.has(root.dataset.defaultRecipe) ? root.dataset.defaultRecipe : familyRecipes[0].id;
let selectedRecipeId = requestedRecipe && byId.has(requestedRecipe) ? requestedRecipe : defaultRecipeId;
let selectedGroupId = groupIdFor(byId.get(selectedRecipeId));
let renderedRuntimeGroup = null;
// Keep previously shared local/openai links working after renaming workflows.
const workflow = (value) => ["local", "python"].includes(value) ? "python" : "server";
let usagePreference = workflow(query.get("use"));
let selectedClient = ["python", "javascript", "curl"].includes(query.get("client")) ? query.get("client") : "curl";
const clientDetails = servingPanel?.querySelector(".cookbook-serving__code");
if (clientDetails && query.has("client")) clientDetails.open = true;
const knobDefs = new Map();
familyRecipes.forEach((recipe) => knobsFor(recipe).forEach((knob) => {
if (!knobDefs.has(knob.key)) knobDefs.set(knob.key, knob);
}));
const knobValues = {};
knobDefs.forEach((knob, key) => {
const fromQuery = query.get(key);
const validValues = knobOptions(knob).map((option) => String(option.value));
const useQueryValue = fromQuery !== null && validValues.includes(fromQuery);
const raw = useQueryValue ? fromQuery : knob.default;
knobValues[key] = typeof knob.default === "number" ? Number(raw) : raw;
});
const renderKnobs = (recipe, hidden) => {
if (!knobsContainer) return;
const knobs = knobsFor(recipe);
const renderedKeys = [...knobsContainer.querySelectorAll("[data-knob-row]")].map((row) => row.dataset.knobRow);
if (renderedKeys.join(",") !== knobs.map((knob) => knob.key).join(",")) {
knobsContainer.replaceChildren();
knobs.forEach((knob) => {
const row = document.createElement("div");
row.className = "cookbook-selection-row";
row.dataset.knobRow = knob.key;
const labelWrap = document.createElement("div");
labelWrap.className = "cookbook-selection-row__label";
const strongLabel = document.createElement("strong");
strongLabel.textContent = knob.label;
const hintLabel = document.createElement("span");
hintLabel.textContent = knob.hint || "";
labelWrap.append(strongLabel, hintLabel);
const grid = document.createElement("div");
grid.className = "cookbook-option-grid cookbook-option-grid--hardware";
grid.setAttribute("role", "group");
grid.setAttribute("aria-label", knob.label);
knobOptions(knob).forEach((option) => {
const optionButton = document.createElement("button");
optionButton.type = "button";
optionButton.dataset.knobKey = knob.key;
optionButton.dataset.knobValue = String(option.value);
optionButton.setAttribute("aria-pressed", "false");
const optionLabel = document.createElement("strong");
optionLabel.textContent = option.label;
optionButton.append(optionLabel);
grid.append(optionButton);
});
row.append(labelWrap, grid);
knobsContainer.append(row);
});
}
knobsContainer.hidden = hidden || knobs.length === 0;
knobsContainer.querySelectorAll("button[data-knob-key]").forEach((optionButton) => {
const selected = String(knobValues[optionButton.dataset.knobKey]) === optionButton.dataset.knobValue;
optionButton.classList.toggle("cookbook-option--selected", selected);
optionButton.setAttribute("aria-pressed", String(selected));
});
};
const recipesForRuntime = (runtimeId, groupRecipes) =>
groupRecipes.filter((item) => runtimeFor(item).id === runtimeId);
const uniqueRuntimeIds = (groupRecipes) => {
const ids = [];
groupRecipes.forEach((item) => {
const id = runtimeFor(item).id;
if (!ids.includes(id)) ids.push(id);
});
return ids;
};
const pickRecipeForRuntime = (runtimeId, preferredCount, groupRecipes) => {
const siblings = recipesForRuntime(runtimeId, groupRecipes);
if (!siblings.length) return null;
if (preferredCount != null) {
const match = siblings.find((item) => item.hardware?.gpu_count === preferredCount);
if (match) return match;
}
return siblings.find((item) => item.hardware?.gpu_count === 1) || siblings[0];
};
const renderRuntimeOptions = () => {
const groupRecipes = groups.get(selectedGroupId) || [];
const runtimeIds = uniqueRuntimeIds(groupRecipes);
const renderedIds = [...hardwareOptions.querySelectorAll("[data-runtime-id]")].map((option) => option.dataset.runtimeId);
if (renderedIds.join(",") === runtimeIds.join(",")) return;
hardwareOptions.replaceChildren();
runtimeIds.forEach((runtimeId) => {
const representative = pickRecipeForRuntime(runtimeId, 1, groupRecipes);
const runtime = runtimeFor(representative);
const option = document.createElement("button");
option.type = "button";
option.dataset.runtimeId = runtime.id;
option.setAttribute("aria-pressed", "false");
const optionLabel = document.createElement("strong");
optionLabel.textContent = runtime.label;
const optionHint = document.createElement("span");
optionHint.textContent = runtime.hint;
option.append(optionLabel, optionHint);
hardwareOptions.append(option);
});
};
const renderDeviceOptions = (recipe) => {
if (!deviceRow || !deviceOptions) return;
const groupRecipes = groups.get(selectedGroupId) || [];
const runtime = runtimeFor(recipe);
const siblings = recipesForRuntime(runtime.id, groupRecipes)
.slice()
.sort((left, right) => (left.hardware?.gpu_count || 0) - (right.hardware?.gpu_count || 0));
const show = siblings.length > 1;
deviceRow.hidden = !show;
if (deviceCaption) {
deviceCaption.textContent = runtime.id === "spark" ? "1 Spark or a QSFP pair" : "GPU count for this runtime";
}
if (!show) {
deviceOptions.replaceChildren();
return;
}
const renderedIds = [...deviceOptions.querySelectorAll("[data-recipe-id]")].map((option) => option.dataset.recipeId);
if (renderedIds.join(",") !== siblings.map((item) => item.id).join(",")) {
deviceOptions.replaceChildren();
siblings.forEach((candidate) => {
const option = document.createElement("button");
option.type = "button";
option.dataset.recipeId = candidate.id;
option.setAttribute("aria-pressed", "false");
const optionLabel = document.createElement("strong");
optionLabel.textContent = gpuCountLabel(candidate.hardware, runtime.id);
const optionHint = document.createElement("span");
optionHint.textContent = runtime.id === "spark" && candidate.hardware?.gpu_count === 2
? "Ray sequence parallel over QSFP"
: runtime.id === "spark"
? "One GB10, local process"
: `${gpuCountLabel(candidate.hardware, runtime.id)} configured`;
option.append(optionLabel, optionHint);
deviceOptions.append(option);
});
}
deviceOptions.querySelectorAll("button").forEach((option) => {
const selected = option.dataset.recipeId === recipe.id;
option.classList.toggle("cookbook-option--selected", selected);
option.setAttribute("aria-pressed", String(selected));
});
};
let notes = root.querySelector("[data-cookbook-notes]");
if (!notes) {
notes = document.createElement("aside");
notes.className = "cookbook-recipe-notes";
notes.dataset.cookbookNotes = "";
notes.hidden = true;
result.insertBefore(notes, commandBlock);
}
const render = ({ groupChanged = false, historyMode = "replace" } = {}) => {
if (!root.isConnected) return;
if (!byId.has(selectedRecipeId)) selectedRecipeId = defaultRecipeId;
let recipe = byId.get(selectedRecipeId);
if (groupChanged || groupIdFor(recipe) !== selectedGroupId) {
const currentRuntime = runtimeFor(recipe).id;
const currentCount = recipe.hardware?.gpu_count;
const groupRecipes = groups.get(selectedGroupId) || [];
recipe = pickRecipeForRuntime(currentRuntime, currentCount, groupRecipes) || groupRecipes[0];
selectedRecipeId = recipe.id;
}
selectedGroupId = groupIdFor(recipe);
if (renderedRuntimeGroup !== selectedGroupId) {
renderRuntimeOptions();
renderedRuntimeGroup = selectedGroupId;
}
renderDeviceOptions(recipe);
const runtime = runtimeFor(recipe);
const profile = servingPanel && servingProfiles[recipe.id];
const useServer = Boolean(profile && usagePreference === "server");
// Only some servers expose the browser playground or return audio; both come from the serving profile.
const hasPlayground = Boolean(profile && profile.playground_url);
const hasAudio = Boolean(profile && profile.audio);
const playgroundAny = familyRecipes.some((item) => servingProfiles[item.id] && servingProfiles[item.id].playground_url);
// The measured local profile and the server config have separate evidence.
const activeRecipe = useServer ? { ...recipe, hardware: profile.hardware, evidence: "Source-backed" } : recipe;
const knobs = knobsFor(recipe);
renderKnobs(recipe, useServer);
if (usage) {
usage.querySelectorAll("[data-cookbook-mode]").forEach((option) => {
const selected = option.dataset.cookbookMode === (useServer ? "server" : "python");
option.disabled = option.dataset.cookbookMode === "server" && !profile;
const hint = option.querySelector("[data-cookbook-server-hint]");
if (hint) hint.textContent = (profile ? hasPlayground : playgroundAny) ? "Playground, cURL, or an API client" : "cURL or an API client";
option.classList.toggle("cookbook-option--selected", selected);
option.setAttribute("aria-pressed", String(selected));
});
const servedNames = [...new Set(familyRecipes.filter((item) => servingProfiles[item.id]).map((item) => item.group_label || item.label))];
servingAvailability.textContent = profile
? `${hasPlayground ? "The playground" : "cURL"} and the OpenAI Python client share one server process. Both workflows can run on your own machine.`
: servingLoadFailed
? "Server examples could not be loaded. Use Python directly."
: servedNames.length
? `This recipe uses Python directly. ${new Intl.ListFormat("en").format(servedNames)} can also run a local server for ${playgroundAny ? "the playground and " : "cURL and "}the OpenAI Python client.`
: "This recipe uses Python directly.";
servingPanel.hidden = !useServer;
commandBlock.hidden = useServer;
root.querySelector("[data-cookbook-python-note]").hidden = useServer;
}
if (useServer) {
const isMLX = profile.runtime === "mlx";
const isSpark = runtime.id === "spark";
const where = hasPlayground ? "in the playground or your app" : "in your app";
servingPanel.querySelector("[data-cookbook-server-lifetime]").textContent = isMLX
? `Start once, then change prompts ${where}. MLX reuses its pipeline and prompt cache, but loads and releases model components between phases to limit unified-memory use. It does not keep all weights resident.`
: isSpark
? `Start once, then change prompts ${where}. On a DGX Spark, lazy module load still reloads Qwen3-VL and the DiT between phases of each request, so later prompts are not a free hot cache.`
: `Start once, then change prompts ${where}. CUDA requests reuse the loaded model. The Python SDK can also reuse a generator within one process.`;
servingPanel.querySelector("[data-cookbook-install-guide]").href = isMLX
? "../../getting_started/installation/mlx/"
: isSpark
? "../../getting_started/installation/spark/"
: "../../getting_started/installation/gpu/";
servingPanel.querySelector("[data-cookbook-prepare]").hidden = !profile.prepare;
servingPanel.querySelector("[data-cookbook-server-prepare]").textContent = profile.prepare;
servingPanel.querySelector("[data-cookbook-server-install]").textContent = profile.install;
servingPanel.querySelector("[data-cookbook-server-command]").textContent = profile.command;
servingPanel.querySelector("[data-cookbook-health-command]").textContent = profile.health_command;
servingPanel.querySelectorAll("[data-cookbook-playground-only]").forEach((element) => {
element.hidden = !hasPlayground;
});
if (hasPlayground) servingPanel.querySelector("[data-cookbook-playground]").href = profile.playground_url;
const client = profile.clients[selectedClient];
const filename = client.source.split("/").pop();
servingPanel.querySelector("[data-cookbook-client-install]").textContent = client.install;
const clientCode = servingPanel.querySelector("[data-cookbook-client-code]");
clientCode.className = `language-${selectedClient === "curl" ? "bash" : selectedClient}`;
clientCode.textContent = client.code;
servingPanel.querySelector("[data-cookbook-client-filename]").textContent = filename;
servingPanel.querySelector("[data-cookbook-client-source]").href = `https://github.com/hao-ai-lab/FastVideo/blob/main/${client.source}`;
const runner = { python: "python", javascript: "node", curl: "bash" }[selectedClient];
servingPanel.querySelector("[data-cookbook-client-run]").textContent = `Save as ${filename} and run ${runner} ${filename}. The MP4 is saved with the job ID as its filename.`;
servingPanel.querySelectorAll("[data-cookbook-client]").forEach((option) => {
option.setAttribute("aria-pressed", String(option.dataset.cookbookClient === selectedClient));
});
}
modelOptions.querySelectorAll("button").forEach((option) => {
const selected = option.dataset.recipeGroup === selectedGroupId;
option.classList.toggle("cookbook-option--selected", selected);
option.setAttribute("aria-pressed", String(selected));
});
hardwareOptions.querySelectorAll("button").forEach((option) => {
const selected = option.dataset.runtimeId === runtime.id;
option.classList.toggle("cookbook-option--selected", selected);
option.setAttribute("aria-pressed", String(selected));
});
description.textContent = useServer
? `${recipe.group_label || recipe.label} generates video${hasAudio ? " with audio" : ""}. Start the local server, then use ${hasPlayground ? "the playground or " : "cURL or "}the OpenAI Python client. This profile uses the checked-in ${runtime.label} configuration.`
: recipe.summary;
label.textContent = useServer ? `${recipe.group_label || recipe.label} · Server` : recipe.label;
model.textContent = recipe.model;
task.textContent = (useServer && recipe.serving && recipe.serving.task) || recipe.task;
hardwareValue.textContent = runtimeSummary(activeRecipe);
if (artifact) {
artifact.textContent = useServer
? (hasAudio ? "MP4 with audio" : "MP4 video")
: recipe.expected_artifact || "Not yet documented for this recipe.";
}
if (evidenceCell) {
evidenceCell.textContent = activeRecipe.evidence || "Source-backed";
evidenceCell.classList.toggle("cookbook-badge--verified", activeRecipe.evidence === "Verified");
evidenceCell.classList.toggle("cookbook-badge--source-backed", activeRecipe.evidence !== "Verified");
}
source.href = `https://github.com/hao-ai-lab/FastVideo/blob/main/${useServer ? profile.source : recipe.source}`;
source.textContent = useServer ? "View server configuration" : "Open example source";
modelLink.href = `https://huggingface.co/${recipe.model}`;
command.textContent = useServer ? recipe.command : appendKnobFlags(recipe.command, knobs, knobValues);
renderHardwareEvidence(hardwareState, hardwareBadge, activeRecipe);
const knobCaveats = useServer ? [] : knobs
.filter((knob) => String(knobValues[knob.key]) !== String(knob.default))
.map((knob) => `${knob.label} is set away from its recorded default (${knobDefaultLabel(knob)}). ` +
"The script accepts this value, but it has not been benchmarked here.");
const limitations = [...(useServer ? [
profile.runtime === "mlx"
? "This MLX server config has no recorded hardware run. Measurements from the Python recipe are not server memory requirements. Only text-to-video/audio is wired; reference inputs and fast modes are not exposed here."
: runtime.id === "spark"
? "This Spark server config has no recorded serving benchmark. Lazy module load reloads Qwen3-VL and the DiT between phases of each request. Compilation of the DiT is disabled."
: `This server config has no recorded serving benchmark.${profile.compile_enabled === false ? " Compilation is disabled, unlike the measured Python performance profile." : ""}`,
`${profile.sampling.width} × ${profile.sampling.height} · ${profile.sampling.num_frames} frames · ${profile.sampling.fps} fps. The server supplies these defaults; the client sends the model and prompt.`,
"Generation is serialized. Job metadata is held in memory and is lost when the server restarts.",
] : recipe.limitations || []),
...(useServer && recipe.serving && Array.isArray(recipe.serving.limitations) ? recipe.serving.limitations : []),
...knobCaveats];
notes.replaceChildren();
notes.hidden = limitations.length === 0;
if (limitations.length) {
const notesHeading = document.createElement("strong");
notesHeading.textContent = "Know before you run";
const notesList = document.createElement("ul");
limitations.forEach((item) => {
const listItem = document.createElement("li");
listItem.textContent = item;
notesList.append(listItem);
});
notes.append(notesHeading, notesList);
}
const nextQuery = new URLSearchParams(window.location.search);
nextQuery.set("recipe", recipe.id);
nextQuery.set("runtime", runtime.id);
const deviceSiblings = recipesForRuntime(runtime.id, groups.get(selectedGroupId) || []);
if (deviceSiblings.length > 1 && recipe.hardware?.gpu_count != null) {
nextQuery.set("gpus", String(recipe.hardware.gpu_count));
} else {
nextQuery.delete("gpus");
}
knobDefs.forEach((knob, key) => {
if (knobs.some((activeKnob) => activeKnob.key === key)) nextQuery.set(key, String(knobValues[key]));
else nextQuery.delete(key);
});
if (usage) {
nextQuery.set("use", useServer ? "server" : "python");
if (useServer && clientDetails?.open) nextQuery.set("client", selectedClient);
else nextQuery.delete("client");
}
const nextUrl = `${window.location.pathname}?${nextQuery.toString()}${window.location.hash}`;
if (historyMode === "push") window.history.pushState({}, "", nextUrl);
else if (historyMode === "replace") window.history.replaceState({}, "", nextUrl);
const modeSummary = useServer ? " with a persistent server" : "";
status.textContent = `${recipe.label} selected for ${runtime.label}${modeSummary}.`;
};
modelOptions.addEventListener("click", (event) => {
const option = event.target.closest("button[data-recipe-group]");
if (!option) return;
selectedGroupId = option.dataset.recipeGroup;
render({ groupChanged: true, historyMode: "push" });
});
hardwareOptions.addEventListener("click", (event) => {
const option = event.target.closest("button[data-runtime-id]");
if (!option) return;
const groupRecipes = groups.get(selectedGroupId) || [];
const currentCount = byId.get(selectedRecipeId)?.hardware?.gpu_count;
const nextRecipe = pickRecipeForRuntime(option.dataset.runtimeId, currentCount, groupRecipes);
if (!nextRecipe) return;
selectedRecipeId = nextRecipe.id;
selectedGroupId = groupIdFor(nextRecipe);
render({ historyMode: "push" });
});
deviceOptions?.addEventListener("click", (event) => {
const option = event.target.closest("button[data-recipe-id]");
if (!option) return;
selectedRecipeId = option.dataset.recipeId;
selectedGroupId = groupIdFor(byId.get(selectedRecipeId));
render({ historyMode: "push" });
});
usage?.addEventListener("click", (event) => {
const option = event.target.closest("button[data-cookbook-mode]");
if (!option || option.disabled) return;
usagePreference = option.dataset.cookbookMode;
render({ historyMode: "push" });
});
servingPanel?.addEventListener("click", (event) => {
const option = event.target.closest("button[data-cookbook-client]");
if (!option) return;
selectedClient = option.dataset.cookbookClient;
render({ historyMode: "push" });
});
knobsContainer?.addEventListener("click", (event) => {
const option = event.target.closest("button[data-knob-key]");
if (!option) return;
const knob = knobDefs.get(option.dataset.knobKey);
knobValues[option.dataset.knobKey] = typeof knob.default === "number"
? Number(option.dataset.knobValue) : option.dataset.knobValue;
render({ historyMode: "push" });
});
render();
familyPopstate = () => {
if (!root.isConnected) return;
const nextQuery = new URLSearchParams(window.location.search);
const nextRecipe = nextQuery.get("recipe");
selectedRecipeId = nextRecipe && byId.has(nextRecipe) ? nextRecipe : defaultRecipeId;
selectedGroupId = groupIdFor(byId.get(selectedRecipeId));
usagePreference = workflow(nextQuery.get("use"));
selectedClient = ["python", "javascript", "curl"].includes(nextQuery.get("client")) ? nextQuery.get("client") : "curl";
if (clientDetails) clientDetails.open = nextQuery.has("client");
knobDefs.forEach((knob, key) => {
const fromQuery = nextQuery.get(key);
const validValues = knobOptions(knob).map((option) => String(option.value));
if (fromQuery !== null && validValues.includes(fromQuery)) {
knobValues[key] = typeof knob.default === "number" ? Number(fromQuery) : fromQuery;
}
});
render({ historyMode: "none" });
};
bindFamilyPopstate();
};
const init = () => {
initEvervault(document);
document.querySelectorAll("[data-cookbook][data-family]").forEach((root) => {
if (root.dataset.initialized) return;
root.dataset.initialized = "true";
initFamilyBuilder(root);
});
};
if (window.document$) window.document$.subscribe(init);
else document.addEventListener("DOMContentLoaded", init);
})();
-205
View File
@@ -1,205 +0,0 @@
// Add a one-click copy button for the rendered MkDocs article.
(function () {
const BUTTON_ID = "copy-page-button";
function text(value) {
return (value || "").replace(/\s+/g, " ").trim();
}
function codeLanguage(code) {
const classes = Array.from(code.classList || []);
const language = classes.find((name) => name.startsWith("language-"));
return language ? language.replace("language-", "") : "";
}
function serializeInline(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || "";
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return "";
}
const tagName = node.tagName.toLowerCase();
if (tagName === "code" && node.parentElement && node.parentElement.tagName.toLowerCase() !== "pre") {
return "`" + (node.textContent || "").trim() + "`";
}
if (tagName === "a") {
if (node.classList.contains("headerlink")) {
return "";
}
const label = text(Array.from(node.childNodes).map(serializeInline).join(""));
const href = node.href;
return href && label ? `${label} (${href})` : label;
}
if (tagName === "img") {
const alt = node.getAttribute("alt") || "image";
const src = node.src || "";
return src ? `[${alt}](${src})` : `[${alt}]`;
}
if (tagName === "br") {
return "\n";
}
return Array.from(node.childNodes).map(serializeInline).join("");
}
function serializeTable(table) {
const rows = Array.from(table.rows);
if (rows.length === 0) return "";
const mdRows = rows.map(
(row) => "| " + Array.from(row.children).map((cell) => text(serializeInline(cell))).join(" | ") + " |"
);
const separator = "| " + Array.from(rows[0].children)
.map(() => "---")
.join(" | ") + " |";
mdRows.splice(1, 0, separator);
return mdRows.join("\n");
}
function serializeBlock(node, listDepth = 0) {
if (node.nodeType === Node.TEXT_NODE) {
return text(node.textContent);
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return "";
}
const tagName = node.tagName.toLowerCase();
if (["script", "style", "nav", "button"].includes(tagName) || node.id === BUTTON_ID) {
return "";
}
if (/^h[1-6]$/.test(tagName)) {
const level = Number(tagName.slice(1));
return `${"#".repeat(level)} ${text(serializeInline(node))}`;
}
if (tagName === "pre") {
const code = node.querySelector("code");
const content = code ? code.textContent || "" : node.textContent || "";
return `\`\`\`${code ? codeLanguage(code) : ""}\n${content.replace(/\n$/, "")}\n\`\`\``;
}
if (["p", "figcaption"].includes(tagName)) {
return text(serializeInline(node));
}
if (tagName === "blockquote") {
return serializeChildren(node, listDepth)
.split("\n")
.map((line) => (line ? `> ${line}` : ">"))
.join("\n");
}
if (tagName === "ul" || tagName === "ol") {
return Array.from(node.children)
.filter((child) => child.tagName && child.tagName.toLowerCase() === "li")
.map((item, index) => serializeListItem(item, tagName === "ol", index, listDepth))
.join("\n");
}
if (tagName === "table") {
return serializeTable(node);
}
if (["hr"].includes(tagName)) {
return "---";
}
return serializeChildren(node, listDepth);
}
function serializeListItem(item, ordered, index, listDepth) {
const marker = ordered ? `${index + 1}. ` : "- ";
const indent = " ".repeat(listDepth);
const childBlocks = [];
const inlineParts = [];
Array.from(item.childNodes).forEach((child) => {
if (child.nodeType === Node.ELEMENT_NODE && ["ul", "ol"].includes(child.tagName.toLowerCase())) {
childBlocks.push(serializeBlock(child, listDepth + 1));
} else {
const content = serializeInline(child);
if (content) inlineParts.push(content);
}
});
const firstLine = `${indent}${marker}${text(inlineParts.join(" "))}`.trimEnd();
return [firstLine, ...childBlocks.filter(Boolean)].join("\n");
}
function serializeChildren(node, listDepth = 0) {
return Array.from(node.childNodes)
.map((child) => serializeBlock(child, listDepth))
.map((value) => value.trim())
.filter(Boolean)
.join("\n\n");
}
function articleText(article) {
const clone = article.cloneNode(true);
clone.querySelectorAll("script, style, .headerlink, .md-clipboard, #copy-page-button").forEach((node) => node.remove());
const content = serializeChildren(clone).trim();
const title = document.querySelector("h1") || document.querySelector("title");
const pageTitle = title ? text(title.textContent) : "";
if (pageTitle && !content.startsWith("# ")) {
return `# ${pageTitle}\n\n${content}`.trim();
}
return content;
}
async function copyArticle(button, article) {
const originalLabel = button.textContent;
try {
await navigator.clipboard.writeText(articleText(article));
button.textContent = "Copied!";
button.classList.add("copy-page-button--copied");
} catch (error) {
button.textContent = "Copy failed";
button.classList.add("copy-page-button--error");
console.error("Failed to copy page", error);
}
window.setTimeout(() => {
button.textContent = originalLabel;
button.classList.remove("copy-page-button--copied", "copy-page-button--error");
}, 2000);
}
function addCopyButton() {
const article = document.querySelector("article.md-content__inner");
if (!article || document.getElementById(BUTTON_ID)) {
return;
}
const button = document.createElement("button");
button.id = BUTTON_ID;
button.type = "button";
button.className = "copy-page-button md-button md-button--primary";
button.textContent = "Copy page";
button.setAttribute("aria-label", "Copy this page as plain text");
button.addEventListener("click", () => copyArticle(button, article));
article.insertBefore(button, article.firstChild);
}
if (typeof document$ !== "undefined") {
document$.subscribe(addCopyButton);
}
document.addEventListener("DOMContentLoaded", addCopyButton);
window.addEventListener("load", addCopyButton);
})();
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
// Update URL search params when tab is clicked
document.addEventListener("DOMContentLoaded", function () {
const tabs = document.querySelectorAll(".sd-tab-label");
function updateURL(tab) {
const syncGroup = tab.getAttribute("data-sync-group");
const syncId = tab.getAttribute("data-sync-id");
if (syncGroup && syncId) {
const url = new URL(window.location);
url.searchParams.set(syncGroup, syncId);
window.history.replaceState(null, "", url);
}
}
tabs.forEach(tab => {
tab.addEventListener("click", () => updateURL(tab));
});
});
+2 -2
View File
@@ -173,7 +173,7 @@ Create a new documentation page for your backend to explain its usage, installat
1. **Create Directory**: `docs/attention/mynew_attn/`
2. **Create Index**: `docs/attention/mynew_attn/index.md`
3. **Update Navigation**: Add an entry to `mkdocs.yml` under the "Attention" tab.
3. **Update Navigation**: Add an entry under Attention research in `docs-site/navigation.json`.
## Checklist
@@ -184,4 +184,4 @@ Create a new documentation page for your backend to explain its usage, installat
* [ ] (Optional) Exported kernel in `fastvideo-kernel/python/fastvideo_kernel/__init__.py`.
* [ ] Implemented `forward` method respecting the standard signature.
* [ ] Added unit tests in `tests/`.
* [ ] Added documentation in `docs/attention/` and updated `mkdocs.yml`.
* [ ] Added documentation in `docs/attention/` and updated `docs-site/navigation.json`.
+5 -5
View File
@@ -24,9 +24,9 @@ def check_assets(directory: Path) -> None:
raise RuntimeError(f"Missing cookbook catalog: {url}. {PREPARE}")
def on_pre_build(config, **kwargs):
check_assets(Path(config["docs_dir"]) / "assets/cookbook-config")
if __name__ == "__main__":
import sys
def on_post_build(config, **kwargs):
check_assets(Path(config["site_dir"]) / "assets/cookbook-config")
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent
check_assets(root / "assets/cookbook-config")
print(f"Cookbook assets verified in {root}.")
+14
View File
@@ -0,0 +1,14 @@
# Contact and Maintainers
## Contact
- **Contact email:** wlsaidhi@gmail.com
- **Bug reports and feature requests:** open a
[GitHub issue](https://github.com/hao-ai-lab/FastVideo/issues).
- **Documentation:** see the [Developer Guide](contributing/overview.md).
## Maintainers
FastVideo is an open-source project of the
[hao-ai-lab](https://github.com/hao-ai-lab) GitHub organization. The source code
lives in [hao-ai-lab/FastVideo](https://github.com/hao-ai-lab/FastVideo).
+5 -4
View File
@@ -1,9 +1,10 @@
# Activation Trace Mode
!!! note
This page covers Extension 0 (module forward hooks), which is the implemented
tracing mechanism. Extensions 1-3 are design sketches for future work and are
**not yet implemented**.
:::note
This page covers Extension 0 (module forward hooks), which is the implemented
tracing mechanism. Extensions 1-3 are design sketches for future work and are
**not yet implemented**.
:::
## Overview
+96 -7
View File
@@ -60,7 +60,7 @@ Three Buildkite entry pipelines share the validated graph:
|---|---|---|
| `pr-fastcheck` | Automatic pull-request webhook | Six Fastcheck lanes |
| `ci` | `/merge`, `ready`, schedules, and `/test` API builds | Change-aware merge gates, scheduled SSIM, explicit Full Suite, Fastcheck reruns, or one direct lane |
| `fastvideo-performance-lane` | Weekly scheduler | Direct performance lane |
| `fastvideo-performance-lane` | GitHub weekly scheduler | Direct performance lane |
Each entry pipeline starts with the same trusted `pipeline-upload` job on the
`ci-runner` queue. The `ci` pipeline's incoming GitHub webhook is disabled;
@@ -205,6 +205,95 @@ inside that range.
See [Performance Benchmarks](performance_benchmarks.md) for the performance
lane's thresholds, rolling baseline, artifacts, and reseeding process.
`ci-scheduled-performance.yml` triggers the performance pipeline every Sunday
at 23:00 America/Los_Angeles, passing the immutable `github.sha`. Its two UTC
schedules select the matching Pacific offset so it runs once across daylight
saving time. After this workflow lands, disable the legacy Buildkite schedule
under **Settings → Schedules → FastVideo scheduled performance benchmarks →
Edit → Enabled**. That schedule submits the literal `HEAD`, which the trusted
runner rejects before upload. The GitHub `BUILDKITE_API_TOKEN` must have
`write_builds` access to the performance pipeline.
The performance lane recognizes main-branch API builds from the dedicated
performance pipeline with `TEST_SCOPE=direct` as scheduled-main runs. They
refresh eligible baselines after successful tests and still produce comparison
reports after test failures. PR performance runs use strict baseline reads and
keep reports as Buildkite artifacts without requiring upload credentials.
Scheduled-main publishing requires an operator-provisioned token with write
access to `FastVideo/performance-tracking`.
The gated FLUX.1-dev SSIM and golden tests and FLUX.2-klein-9B SSIM test are explicitly paused until their
model access is provisioned. Their pytest skips are not FLUX quality validation;
the other SSIM and golden tests and the suites' fail-fast behavior remain active.
The SD3.5 golden and SSIM tests are also explicitly paused at the maintainer's
request while their gated checkpoint access is unavailable.
MatrixGame 3 explicitly selects FA2 for its existing GB200 reference. That
reference was published in dataset commit `e1a6e64` and was not part of the
later FA4 refresh. Unchanged main with FA2 reproduces its video SHA-256
`1d1d26a59481e37a9e838a628d875e196211c2236e8f9a370e0a9f73e97300c7`;
FA4 scores 0.96636 against its unchanged 0.98 requirement. The test compares
the current generation's returned path so older files cannot mask that check.
The missing GB200 HunyuanVideo 1.5 I2V and Kandinsky 6 references were generated
from unchanged upstream commit `d5253fd71055927f12aa6c4aa679b93a00238851`
with Torch 2.12.0, CUDA 13 and FA4. Their checkpoint revisions are pinned in
the tests and recorded alongside the videos. Independent upstream repeats and
the candidate both scored 1.0 for Hunyuan and 0.98395 for Kandinsky against
the first upstream outputs, preserving the existing 0.95 and 0.98 thresholds.
Kandinsky remains required; its authorized complete snapshot is prepared in
the shared CI cache without exposing an authentication token to PR code.
LingBot Fast's missing GB200 reference uses the same upstream commit and
runtime, with checkpoint revision `98583eb0fd66f481f73383428d65d2867c878e51`.
Two unchanged-upstream runs with CPU offloading disabled produced identical
video hashes. Its fixed FSDP CPU-offloaded run scored SSIM 1.0 against that
reference, preserving the 0.70 requirement. The generation stage leaves FSDP
parameter placement to FSDP instead of moving the whole model to the GPU.
### Provisioning Required Hugging Face Models
Gated access belongs to the individual HF account, not its organization or
token. An operator must request and obtain model access for the CI identity
before configuring authentication. A valid token without model access still
returns 403. Keep required models such as Kandinsky 6 strict until their
weights and quality references are provisioned.
The permanent operator rollout is:
1. Approve every required gated repository for one maintained CI identity.
2. Configure [HF CI/CD access through OIDC](https://huggingface.co/docs/hub/trusted-publishers)
for the trusted Buildkite preparation step, restricting issuer and job
claims to the intended organization and pipelines. A fine-grained read-only
token in the operator's secret store is an alternative. Keep performance
publishing credentials separate and restricted to the tracking dataset and
trusted main runs.
3. Before leasing GPUs, check access to all required pinned manifests and
weight files together and check that their device/backend references exist.
Report every missing dependency in one preflight result; do not turn access
failures into passing quality gates.
4. Fetch immutable model revisions into the runner-owned cache through trusted
code. Remove authentication before starting PR code, and keep download locks
and cache writes under the runner's identity. PR jobs consume the prepared
snapshots rather than depending on an interactive login or another user's
writable cache.
When multiple operators prepare the shared cache, use its existing group
with `umask 0002`. Check group write access on each prepared model's cache,
snapshot, blob and `.locks` directories, not only the cache root. Verify
manifest resolution as the CI worker identity before running the lane.
A readable weight file alone is insufficient: Hub downloads also create
metadata and lock files. For example, Stable Audio's pinned revision is
`26609645c363fc81829282fcfe6356340346c1d4`; provision its complete snapshot,
including `model_index.json`, component configs and tokenizer files.
5. For a missing SSIM reference, generate and inspect output from unchanged
upstream source on the CI GPU/backend/runtime, retain its provenance, and
publish only the reviewed new reference. Verify the candidate independently
before enabling the required gate; never use candidate output to clear a
regression failure.
See [HF gated-model access](https://huggingface.co/docs/hub/models-gated) and
the [testing guide](testing.md) for access and reference maintenance details.
## `/merge` Request Flow
The Buildkite agent and Slurm have deliberately separate responsibilities:
@@ -409,8 +498,8 @@ The docs job:
1. Installs Python and Node documentation dependencies.
2. Runs lint, formatting checks and tests for documentation tooling.
3. Generates required static assets.
4. Builds the MkDocs site and checks its assets and documentation links.
5. Uploads the site artifact and publishes only from `main`.
4. Builds the Blume site, validates routes and assets, and tests navigation and cookbook interactions.
5. Uploads `docs-site/dist/` as the Pages artifact and publishes only from `main`, at the existing `https://haoailab.com/FastVideo/` URL.
### Docker Images
@@ -461,12 +550,11 @@ The reusable implementation lives in
| Workflow | Trigger | Purpose |
|---|---|---|
| `community-issue-labeler.yml` | Issue opened or edited | Adds scope/platform labels from issue keywords. |
| `community-stale.yml` | Daily | Labels/closes stale items with exemptions. |
None of these automations post comments or messages on issues or pull
requests; they only set labels and close stale items. Keep it that way: new
automation must report through checks, commit statuses, or labels instead of
PR/issue comments.
requests; they only set labels. Issues and pull requests are never marked
stale or closed automatically. Keep it that way: new automation must report
through checks, commit statuses, or labels instead of PR/issue comments.
## File Reference
@@ -477,6 +565,7 @@ PR/issue comments.
| `.github/workflows/ci-slash-commands.yml` | `/merge` and `/test` handling |
| `.github/workflows/ci-trigger-full-suite.yml` | Change-aware merge-gate trigger for `ready` PRs and new pushes |
| `.github/workflows/ci-scheduled-ssim.yml` | Weekly complete SSIM trigger on `main` |
| `.github/workflows/ci-scheduled-performance.yml` | Weekly performance trigger with an immutable source SHA |
| `.github/scripts/plan_merge_ci.py` | Trusted changed-path to integration-lane planner |
| `.github/workflows/ci-aggregate-status.yml` | Aggregate Fastcheck and explicit Full Suite direct-rerun statuses |
| `.buildkite/pipeline.yml` | Static 20-lane Slurm Buildkite graph |
+25 -19
View File
@@ -129,8 +129,9 @@ factors, outputs may not match exactly, but they should be comparable.
This is an example workflow for adding a full model pipeline (model + configs +
examples + tests). This guide is in active development; feedback is welcome.
!!! note
If you get stuck, refer to existing models/pipelines in FastVideo or ask in Slack.
:::note
If you get stuck, refer to existing models/pipelines in FastVideo or ask in Slack.
:::
### 0) Fetch official model's code and weights
@@ -149,15 +150,18 @@ Action:
handling and download it directly with
`scripts/huggingface/download_hf.py`.
!!! note
This step is best done manually because large downloads can time out.
Example:
```bash
python scripts/huggingface/download_hf.py \
--repo_id Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
--local_dir official_weights/Wan2.1-T2V-1.3B-Diffusers \
--repo_type model
```
:::note
This step is best done manually because large downloads can time out.
Example:
```bash
python scripts/huggingface/download_hf.py \
--repo_id Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
--local_dir official_weights/Wan2.1-T2V-1.3B-Diffusers \
--repo_type model
```
:::
### 1) Implement the model + config mapping
@@ -189,15 +193,17 @@ Action:
- If you are using an agent, ask it to implement the model, config mapping,
and a parity test together so you can validate numerics immediately.
!!! note
After the first component is aligned and parity‑tested, open a **DRAFT PR**
on FastVideo so the rest of the pipeline work can build on top of it.
:::note
After the first component is aligned and parity‑tested, open a **DRAFT PR**
on FastVideo so the rest of the pipeline work can build on top of it.
:::
!!! note
If a Diffusers-format HF repo already exists and loads correctly, you can
skip conversion entirely (no conversion script needed) and just download it
with `scripts/huggingface/download_hf.py`. Otherwise, you may need a
conversion script + a `converted_weights/<model>/` staging directory.
:::note
If a Diffusers-format HF repo already exists and loads correctly, you can
skip conversion entirely (no conversion script needed) and just download it
with `scripts/huggingface/download_hf.py`. Otherwise, you may need a
conversion script + a `converted_weights/<model>/` staging directory.
:::
Example (key renaming via arch config mapping, Wan2.1‑style):
@@ -318,3 +318,64 @@ For reusable UI methods and configuration guarantees, see the
[design and API contract](../cookbook/design.md). Streaming client instructions
follow the [WebSocket contract](../design/server_contracts/streaming.md); a health
response alone does not establish generation readiness.
## Model pages in the cookbook
The Blume cookbook starts at `/cookbook` with model cards filtered by Video,
Image, Audio, or World. FastH3 appears first. Each model has its own page in a
sidebar family group.
`docs-site/lib/cookbook-pages.ts` derives the inventory from
`docs/assets/cookbook-recipes.json`. Recipes sharing a checkpoint become variants
on one model page. `transfer-docs.mjs` generates source-recipe pages and sidebar
entries. Preparation publishes checked-in YAML under `assets/cookbook-sources/`
and Python examples with source locations for editable literals under
`assets/cookbook-examples/`. Python examples are parsed with AST, never imported.
Explicit values in the maintained launch command take priority over argparse
defaults. The editor rewrites those literals and preserves required launch flags
and preparation commands. Controls inherited from a helper may use an explicitly
declared cookbook knob. Settings without a source binding stay hidden.
For a model with an editable serving catalog, add its display metadata to
`recipePages`, map the checkpoint in `backendModels`, and add an MDX overlay with
`<Cookbook modelId="your-model-key" />`. Use `mode: frame` so the island owns the
logo, heading, and YAML actions. Add the route to its sidebar family.
Presentation metadata must not define editable options or compatibility claims.
Editable pages organize catalog fields into a compact option matrix. The shared
API owns validation and YAML/command generation. Each model owns one page. Hardware backend buttons select declared
deployments, so unsupported NVIDIA, AMD, or MLX choices do not appear. Precision
and encoder controls appear only when the deployment declares them.
GPU targets are URL preferences for setup guidance. GPU count and parallelism
use backend fields. Automatic parallelism sets the supported degrees to `-1`;
it does not estimate memory. All configurations carry an unverified-recipe
label until hardware-fit and runtime validation are connected.
Old `/cookbook?deployment=<id>` links open the corresponding model page.
Provider labels follow the displayed logo. FastVideo-owned releases and converted
checkpoints using its mark display `FastVideo` as provider. FastH3 Trim replaces
the obsolete CompactH3 catalog entry. Its editable NVFP4 deployment uses the
released checkpoint and the maintained one-Spark inference configuration, with
a serving adaptation under `examples/serving/`. Do not restore the old local
assembly recipes or their four-forward sampling settings.
Every model uses the same header, run modes, command toolbar, hardware matrix,
settings, and reset controls. The top choices are Python SDK, CLI, and Serving,
limited to supported routes. Hardware backends display CUDA or MLX. Serving
appears only where a native server baseline exists.
The toolbar's Copy action writes the edited YAML or Python example with a quoted
heredoc, then runs the matching command. CUDA catalog pages use the same generator
settings for SDK, CLI, and serving. Inference uses `request`; serving uses `server`
and `default_request`. These commands assume the platform's installation guide
has already been followed. cURL and environment tools appear where the recipe
provides them. Do not invent CLI flags, serving support, or container commands.
Custom hardware names can be added through the matrix and persist in the page
URL. This is a device preference, not a memory lookup or compatibility result.
The component explorer is a design preview for weights, encoders, and decoders.
It does not alter serving YAML. Choosing an alternate component on an editable
recipe suppresses generated output until the baseline components are restored.
Connect those choices only after a matching backend recipe is defined.
+81
View File
@@ -0,0 +1,81 @@
# Documenting FastVideo
FastVideo documentation is built with Blume and published at
[haoailab.com/FastVideo](https://haoailab.com/FastVideo/).
## Source ownership
- Edit guides under `docs/`. Keep runnable examples under `examples/` and `scripts/`.
- Edit the welcome page, Fast Models catalog, and cookbook model pages under
`docs-site/content/`, `docs-site/islands/`, and `docs-site/lib/`.
- Edit sidebar labels and grouping in `docs-site/navigation.json`. Group by reader
task. Hardware, integrations, and model-specific guides belong under Run inference.
- Python reference pages are extracted from source with Griffe. They require no
FastVideo installation, GPU packages, or model imports.
- Do not edit `.generated/`, `public/`, or `dist/`. Preparation recreates them.
## Write a task guide
Give each page one task and one audience. Use this order where it applies:
1. Explain what the reader will do and when to use the guide.
2. List prerequisites, supported runtimes, and hardware assumptions.
3. Provide the shortest runnable example and its expected output.
4. Explain configuration choices after the working example.
5. Put troubleshooting beside the relevant command or in a short closing section.
6. Link to the next task and the relevant reference.
Keep measured results, hardware and limitations together. Put implementation
history and rejected experiments in an architecture or research page, rather
than between setup commands. Link to installation instead of repeating it.
The main quick start owns the first generation example; specialized guides
build on it. Keep model IDs in the catalog and runnable sources consistent.
## Build and check
From the repository root, use Node 24 and Python 3.12:
```bash
uv venv docs-site/.venv --python 3.12
uv pip install --python docs-site/.venv/bin/python -r requirements-docs.txt
npm ci --prefix docs
npm ci --prefix docs-site
npm run dev --prefix docs-site
```
Open the printed URL under `/FastVideo`. After editing canonical guides, rerun
`npm run prepare:content --prefix docs-site` and restart the dev server.
For a static production build:
```bash
npm run check --prefix docs-site
npm run format:check --prefix docs-site
npm run build --prefix docs-site
npm run validate --prefix docs-site
npm run test:content --prefix docs-site
npm test --prefix docs-site
```
Use `DOCS_PYTHON=/path/to/python` to select another documentation environment.
Install Chromium with `npm exec --prefix docs-site -- playwright install chromium`
if the browser suite requests it. Python renderer tests run with
`docs-site/.venv/bin/python -m unittest discover -s docs-site/tests -p 'test_*.py'`.
## Publish
The existing `.github/workflows/infra-docs.yml` builds Blume, checks content and
browser interactions, and uploads `docs-site/dist/` to GitHub Pages. Only pushes
to `main` deploy. The custom domain, GitHub Pages environment, and `/FastVideo`
base path remain the same. Pull requests validate the build without deploying.
When changing dependencies, edit `requirements-docs.in` and regenerate its lock:
```bash
uv pip compile requirements-docs.in -o requirements-docs.txt \
--python-platform x86_64-manylinux_2_28 --python-version 3.12
```
The committed migration anchor manifest records the previous site's URLs and
fragments. It is a compatibility contract, not generated content to refresh on
each build. A deliberate URL change needs a redirect and an updated regression
check before removing an old route.
+1
View File
@@ -218,6 +218,7 @@ longer exists also fails the test, so the fixing pull request deletes its entry.
| `FASTVIDEO_LTX2_VAE_CHANNELS_LAST_3D` | bool | `1` | performance | LTX-2 VAE uses the channels_last_3d memory format. |
| `FASTVIDEO_LTX2_DISABLE_AUDIO_AUTOCAST` | bool | `1` | performance | LTX-2 audio decoding runs without CUDA autocast. Deprecated names: `LTX2_DISABLE_AUDIO_AUTOCAST`. |
| `FASTVIDEO_FLUX2_DISABLE_BF16_REDUCED_PRECISION_REDUCTION` | bool | `0` | performance | Flux denoising disables reduced-precision reductions in bf16 matmuls, which tightens accumulation for the 4-step Klein model. |
| `FASTVIDEO_ENABLE_FUSED_NORM` | bool | `0` | performance | Opt into Wan's inference-only Triton residual + LayerNorm + modulation fusion. Its reduction order changes numerical outputs; the default keeps eager reference compatibility. |
| `FASTVIDEO_DISABLE_FUSED_NORM` | bool | `0` | performance | Turn off the Triton-fused residual + LayerNorm + modulate inference path that Wan blocks opt into, and use the eager path. |
| `FASTVIDEO_FFMPEG_BIN` | str | `ffmpeg` | output | ffmpeg executable used to save video with audio. |
| `FASTVIDEO_VIDEO_CODEC` | str | `libx264` | output | ffmpeg video codec for saved videos. |
+7 -6
View File
@@ -44,8 +44,9 @@ python fastvideo/tests/performance/dashboard.py
The pytest run never uploads anything. `compare_baseline.py` uploads only when
`PERF_UPLOAD_POLICY` is set. Local uploads are explicit opt-in and require HF
credentials. PR/direct performance runs upload passing records for dashboard
visibility, while scheduled-main runs upload both pass and fail records. The
credentials. PR performance runs read baselines strictly and retain comparison
reports as Buildkite artifacts without uploading records. Scheduled-main runs
upload both pass and fail records using an operator-provisioned CI credential. The
report directory default is container-oriented; set `PERF_REPORTS_DIR` to a
writable local path when generating dashboards or when you want local
Markdown/normalized-result artifacts from the comparator.
@@ -244,8 +245,8 @@ metrics are skipped.
This is the **drift detector** — it catches sub-threshold regressions that
slowly add up. Only scheduled-main successful records are baseline eligible.
Local and pull-request runs can upload dashboard-visible records, but they do
not update future gating baselines.
Local uploads are explicit opt-in and do not update future gating baselines.
PR runs retain their normalized records and reports as Buildkite artifacts.
Comparator summaries and normalized artifacts include an explicit
`comparison_status`:
@@ -620,9 +621,9 @@ When the rolling-baseline phase runs, it emits:
"regression_thresholds": {
"latency": { "threshold_percent": 0.10, "threshold_absolute": 1.0, "gated": true }
}
}
}
```
```
Legacy v1 configs without `config_schema_version` still load, but should not
gain v2 identity or metadata fields until they are migrated to
+4 -3
View File
@@ -3,8 +3,9 @@
For GB200 sequence-parallel transport tuning and its long-training memory
policy, see [Ulysses performance](ulysses_performance.md).
!!! warning
Profiling is only intended for FastVideo developers and maintainers to understand the proportion of time spent in different parts of the codebase. **FastVideo end-users should never turn on profiling** as it will significantly slow down inference.
:::warning
Profiling is only intended for FastVideo developers and maintainers to understand the proportion of time spent in different parts of the codebase. **FastVideo end-users should never turn on profiling** as it will significantly slow down inference.
:::
## Profiling with PyTorch
@@ -47,7 +48,7 @@ Additional knobs:
- `FASTVIDEO_TORCH_PROFILER_WITH_STACK`
- `FASTVIDEO_TORCH_PROFILER_WITH_FLOPS`
Traces can be visualized using <https://ui.perfetto.dev/>.
Traces can be visualized using [https://ui.perfetto.dev/](https://ui.perfetto.dev/).
### Best Practices
+17 -3
View File
@@ -53,6 +53,11 @@ or environment mismatch is not a pass. Do not replace a reference with the
candidate output just to clear a failure. For a relocation, the unchanged
parent is the baseline; two imports of the same class are not numerical proof.
FA4 fingerprints record the effective boolean switch. Legacy block references
with `fastvideo_fa4: <unset>` mean disabled and match an explicit `0`; enabling
FA4 still fails the environment check. Other runtime pins and zero-tolerance
tensor comparisons remain strict.
The VAE and transformer CI lanes run their Wan component goldens first.
Selected integration lanes wait for the golden lane in merge/full builds.
See [CI/CD Architecture](ci_architecture.md) for direct-rerun and skip semantics.
@@ -95,6 +100,14 @@ pytest fastvideo/tests/vaes/test_wan_vae.py -q
GPU-heavy suites need the right hardware, credentials, local caches, and
sometimes custom kernels. Document those assumptions in new tests.
The legacy VSA training regression selects a device-specific summary. Its
GB200 reference was generated from unchanged upstream commit
`d5253fd71055927f12aa6c4aa679b93a00238851` using two GB200 GPUs, Torch
2.12.0+cu130, CUDA 13.0 and FA2. Two independent upstream runs produced
gradient norms 1.909 and 1.935 and losses 0.30861 and 0.30886. The reference
JSON retains provenance; the H100 reference and all comparison tolerances
remain unchanged. Candidate outputs must not be used to refresh this reference.
## SSIM Tests
SSIM tests generate videos using specific models and parameters, then compare
@@ -102,9 +115,10 @@ the output against reference videos with Structural Similarity Index Measure.
Use them for pipeline-level visual regression coverage when output quality or
generation behavior must be preserved.
!!! note
Add enough prompts, seeds, backends, and parameter combinations to cover the
behavior you want to protect, but keep runtime reasonable for CI.
:::note
Add enough prompts, seeds, backends, and parameter combinations to cover the
behavior you want to protect, but keep runtime reasonable for CI.
:::
### Directory Structure
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Cosmos recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="cosmos" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/nvidia.webp" alt="NVIDIA" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Cosmos inference recipes</h2>
<p>NVIDIA Cosmos Predict 2.5 generates navigable world videos. The maintained example runs the 2B text-to-world checkpoint on a single GPU.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="cosmos" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Cosmos recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/nvidia">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>World-generation prompts work best describing a scene and camera motion; the built-in prompt in the example is a known-good starting point.</li>
<li>Gated or missing checkpoints: run <code>huggingface-cli login</code> and confirm you accepted the model's license on Hugging Face.</li>
@@ -132,9 +22,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+14 -15
View File
@@ -18,7 +18,7 @@ Native serving YAML + option schemas + deployment manifests
|
Generated JSON catalogs
|
MkDocs / static hosting
Blume / static hosting
|
cookbook-config.js + UI edits
|
@@ -34,7 +34,7 @@ Native serving YAML + option schemas + deployment manifests
| `docs/js/cookbook-config.mjs` | Load catalogs, preview fields, validate edits and produce output |
Catalog generation runs in Node without importing FastVideo or loading model
weights. The docs build generates assets before MkDocs copies them into the
weights. The docs build generates assets before Blume copies them into the
published site. Generated JSON, the combined API/validator bundle and its license notices
are Git-ignored build output published together under `assets/cookbook-config/`.
Website visitors need only the static assets. The schemas are author-maintained
@@ -74,7 +74,7 @@ to documentation, not embedded HTML.
## Shared JavaScript API
`docs/js/cookbook-config.mjs` exports the four functions below. The generated
`docs/js/cookbook-config.mjs` exports the five functions below. The generated
`assets/cookbook-config/cookbook-config.js` bundles them with Ajv and exposes
the browser global `FastVideoConfigCookbook` and CommonJS exports:
@@ -84,6 +84,12 @@ the browser global `FastVideoConfigCookbook` and CommonJS exports:
| `loadDeployment(index, id, {signal, fetcher} = {})` | Promise of the selected complete catalog |
| `getOptions(catalog, config = catalog.base_config, edits = {})` | `[{path, schema, value}]`, including GPU-dependent degree limits |
| `resolveConfig(catalog, edits = {})` | `config`, `yaml`, `argv`, `command`, `clientRequest`, `clientCommand`, optional `websocketUrl` |
| `sampleRequest(metadata, config)` | `{body, command}` and optional `websocketUrl` for a native serving baseline |
`sampleRequest()` is also used by model pages backed by a checked-in serving
YAML rather than a full editable catalog. Its metadata supplies `model.id`,
`workload`, and `runtime.interface`/`runtime.server_defaults`. It builds client
instructions only; it does not validate the configuration or run a request.
For WebSocket deployments, `clientRequest` is `null` and `websocketUrl` is
present. REST deployments return a request object and omit `websocketUrl`.
@@ -209,16 +215,9 @@ Memory estimates, measured hardware support, format versioning and HTTP cache
freshness remain separate follow-ups. No general arithmetic validator or CLI
override workflow is introduced here.
## Coexistence with the current cookbook
## Cookbook interface
Family pages still load `docs/assets/cookbook-recipes.json` through `cookbook.js`
for generate commands, hardware evidence, and the H3 "Run a server" panel.
Those pages are unchanged. This catalog and API sit beside them: manifests under
`docs/cookbook/recipes/` point at the same `examples/serving/*.yaml` baselines
the family pages already advertise, plus authored editable-field metadata.
A later UI change can call `loadIndex` / `resolveConfig` from those pages so
users download a customized serving YAML instead of copying a static file. Until
then, keep recipe IDs aligned with the family catalog (`fasth3-8step`,
`fastwan21`, `wan21-i2v`, `ltx2-distilled`) and treat `cookbook-recipes.json`
`serving.source` as the native baseline, not a second options schema.
Family and model pages use the React cookbook in `docs-site/`. The model inventory
comes from `docs/assets/cookbook-recipes.json`; the shared configuration API
continues to own editable serving recipes. The old family JavaScript selector
and site-theme assets have been removed.
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# FLUX recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="flux" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/black-forest-labs.webp" alt="Black Forest Labs" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>FLUX inference recipes</h2>
<p>Black Forest Labs' FLUX family covers FLUX.1 dev and FLUX.2 (dev and distilled Klein) text-to-image. FLUX.1 registers no `model_family` in the registry and is grouped under FLUX for documentation only.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="flux" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading FLUX recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/black-forest-labs">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>FLUX.1 dev defaults to a local <code>official_weights/FLUX.1-dev</code> directory in the example; the cookbook command passes the Hugging Face ID explicitly instead.</li>
<li>Image outputs land under <code>outputs/</code>; adjust <code>--output</code> if that path is not writable.</li>
@@ -133,9 +23,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Hunyuan recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="hunyuan" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/tencent-hunyuan.webp" alt="Tencent Hunyuan" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Hunyuan inference recipes</h2>
<p>HunyuanVideo 1.5 is Tencent's video generation family. The maintained examples cover 480p text-to-video with CPU offload and a full 480p-to-1080p upscale chain.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="hunyuan" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Hunyuan recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/Tencent-Hunyuan">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>Out of memory: <code>basic_hy15.py</code> already enables dit/VAE/text-encoder CPU offload; further tradeoffs are described in <a href="../../inference/optimizations/">Optimizations</a>.</li>
<li><code>pin_cpu_memory</code> errors on low-RAM machines are documented inline in the example; set it to false as the source comment suggests.</li>
@@ -133,9 +23,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+13 -369
View File
@@ -1,372 +1,16 @@
---
hide:
- toc
---
# Cookbook
# Inference Cookbook
Choose a model recipe for video, image, audio, or world generation.
The interactive catalog groups models by family and generates YAML for supported serving recipes.
<div class="cookbook-shell cookbook-catalog" data-cookbook-gallery>
<header class="cookbook-hero">
<p class="cookbook-eyebrow">FastVideo inference cookbook</p>
<h2>Choose by output, then by family.</h2>
<p class="cookbook-hero__lede">
Open a family to pick a maintained recipe and a runtime FastVideo
actually supports. Every command runs a checked-in source, so the model,
platform, offload, and attention settings stay tied to that example.
Cards below group video, image, audio, and interactive world models.
Mode chips on each card are the workloads FastVideo maintains for that
family, not a promise that every flag works on every runtime.
</p>
<a class="cookbook-inline-link" href="../inference/support_matrix/">
View the full support matrix <span aria-hidden="true">→</span>
</a>
<a class="cookbook-inline-link" href="./openai-api/">Run FastH3 with a playground and API <span aria-hidden="true">→</span></a>
</header>
- [FastH3](minimax-h3.md)
- [Wan and FastMetal](wan.md)
- [LTX](ltx.md)
- [FLUX](flux.md)
- [Stable Diffusion](stable-diffusion.md)
- [Stable Audio](stable-audio.md)
- [MMAudio](mmaudio.md)
- [Cosmos](cosmos.md)
- [Matrix Game](matrix-game.md)
<section class="cookbook-section" id="video-models" aria-labelledby="video-models-heading">
<div class="cookbook-section__heading">
<h2 id="video-models-heading">Video</h2>
</div>
<div class="cookbook-family-grid">
<a class="cookbook-family-tile cookbook-family-tile--ready cookbook-family-tile--featured" href="./minimax-h3/" aria-label="Open MiniMax H3 recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/minimax.webp" alt="" width="132" height="132" loading="eager">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>MiniMax H3</strong><small>Video + stereo audio</small></span>
<span class="cookbook-count">9 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2VA</li>
<li>FL2VA</li>
<li>Ref2VA</li>
<li>MLX T2VA</li>
<li>DGX Spark</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./wan/" aria-label="Open Wan recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/wan-ai.webp" alt="" width="132" height="132" loading="eager">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Wan</strong><small>FastWan CUDA and FastMetal MLX</small></span>
<span class="cookbook-count">7 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2V</li>
<li>I2V</li>
<li>TI2V</li>
<li>MLX T2V</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./ltx/" aria-label="Open LTX recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/ltx.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>LTX</strong><small>Video with synchronized audio</small></span>
<span class="cookbook-count">2 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2V</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./hunyuan/" aria-label="Open Hunyuan recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/tencent-hunyuan.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Hunyuan</strong><small>480p and 1080p upscale</small></span>
<span class="cookbook-count">2 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2V</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./kandinsky5/" aria-label="Open Kandinsky 5 recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/kandinsky.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Kandinsky 5</strong><small>Text and image to video</small></span>
<span class="cookbook-count">2 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2V</li>
<li>I2V</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./kandinsky6/" aria-label="Open Kandinsky 6 recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/kandinsky.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Kandinsky 6</strong><small>Video with audio, and video SR</small></span>
<span class="cookbook-count">4 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2VA</li>
<li>I2VA</li>
<li>VSR</li>
</ul>
</span>
</a>
</div>
</section>
<section class="cookbook-section" id="image-models" aria-labelledby="image-models-heading">
<div class="cookbook-section__heading">
<h2 id="image-models-heading">Image</h2>
</div>
<div class="cookbook-family-grid">
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./flux/" aria-label="Open FLUX recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/black-forest-labs.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>FLUX</strong><small>FLUX.1 and FLUX.2</small></span>
<span class="cookbook-count">3 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2I</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./stable-diffusion/" aria-label="Open Stable Diffusion recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/stabilityai.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Stable Diffusion</strong><small>SD 3.5 Medium</small></span>
<span class="cookbook-count">1 recipe</span>
</span>
<ul class="cookbook-mode-row">
<li>T2I</li>
</ul>
</span>
</a>
</div>
</section>
<section class="cookbook-section" id="audio-models" aria-labelledby="audio-models-heading">
<div class="cookbook-section__heading">
<h2 id="audio-models-heading">Audio</h2>
</div>
<div class="cookbook-family-grid">
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./stable-audio/" aria-label="Open Stable Audio recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/stabilityai.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Stable Audio</strong><small>Open 1.0 and Small</small></span>
<span class="cookbook-count">2 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>T2A</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./mmaudio/" aria-label="Open MMAudio recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/fastvideo.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>MMAudio</strong><small>Video or text to audio</small></span>
<span class="cookbook-count">1 recipe</span>
</span>
<ul class="cookbook-mode-row">
<li>V2A</li>
<li>T2A</li>
</ul>
</span>
</a>
</div>
</section>
<section class="cookbook-section" id="world-models" aria-labelledby="world-models-heading">
<div class="cookbook-section__heading">
<h2 id="world-models-heading">World and interactive</h2>
</div>
<div class="cookbook-family-grid">
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./cosmos/" aria-label="Open Cosmos recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/nvidia.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Cosmos</strong><small>Text to world video</small></span>
<span class="cookbook-count">1 recipe</span>
</span>
<ul class="cookbook-mode-row">
<li>T2W</li>
</ul>
</span>
</a>
<a class="cookbook-family-tile cookbook-family-tile--ready" href="./matrix-game/" aria-label="Open Matrix Game recipes">
<span class="cookbook-family-tile__visual" data-evervault>
<span class="cookbook-evervault" aria-hidden="true">
<span class="cookbook-evervault__gradient"></span>
<span class="cookbook-evervault__noise" data-cookbook-pattern></span>
</span>
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/fastvideo.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>Matrix Game</strong><small>Image-conditioned worlds</small></span>
<span class="cookbook-count">2 recipes</span>
</span>
<ul class="cookbook-mode-row">
<li>I2W</li>
</ul>
</span>
</a>
</div>
</section>
<section class="cookbook-section" id="planned-families" aria-labelledby="planned-families-heading">
<div class="cookbook-section__heading">
<h2 id="planned-families-heading">Pages still to write</h2>
<p>These families already have runnable examples. The cookbook page is not ready, so the cards are not links.</p>
</div>
<div class="cookbook-family-grid">
<article class="cookbook-family-tile cookbook-family-tile--coming" aria-label="GEN3C cookbook page planned; runnable examples exist">
<span class="cookbook-family-tile__visual">
<span class="cookbook-family-tile__logo-wrap">
<img class="off-glb" src="../assets/logos/nvidia.webp" alt="" width="132" height="132" loading="lazy">
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>GEN3C</strong><small>Novel-view video</small></span>
<span class="cookbook-count">Page planned</span>
</span>
</span>
</article>
<article class="cookbook-family-tile cookbook-family-tile--coming" aria-label="HY-World cookbook page planned; runnable examples exist">
<span class="cookbook-family-tile__visual">
<span class="cookbook-family-tile__logo-wrap">
<span class="cookbook-family-tile__monogram" aria-hidden="true">HY</span>
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>HY-World</strong><small>Interactive world play</small></span>
<span class="cookbook-count">Page planned</span>
</span>
</span>
</article>
<article class="cookbook-family-tile cookbook-family-tile--coming" aria-label="LingBot cookbook page planned; runnable examples exist">
<span class="cookbook-family-tile__visual">
<span class="cookbook-family-tile__logo-wrap">
<span class="cookbook-family-tile__monogram" aria-hidden="true">LB</span>
</span>
</span>
<span class="cookbook-family-tile__footer">
<span class="cookbook-family-tile__footer-top">
<span><strong>LingBot</strong><small>Video and world models</small></span>
<span class="cookbook-count">Page planned</span>
</span>
</span>
</article>
</div>
</section>
</div>
<small class="cookbook-logo-credit">
Catalog marks come from the official model publishers' Hugging Face
organizations; typographic tiles are placeholders, never invented logos. See
<a href="https://github.com/hao-ai-lab/FastVideo/blob/main/docs/assets/logos/SOURCES.md">docs/assets/logos/SOURCES.md</a>.
</small>
See the [support matrix](../inference/support_matrix.md) for runtime capabilities.
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Kandinsky 5 recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="kandinsky5" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/kandinsky.webp" alt="Kandinsky Lab" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Kandinsky 5 inference recipes</h2>
<p>Kandinsky 5.0 from the Kandinsky Lab covers text-to-video and image-to-video in Lite and Pro variants, including distilled checkpoints for faster sampling.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="kandinsky5" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Kandinsky 5 recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/kandinskylab">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>Alternative Lite/Pro checkpoints are listed inline in the maintained examples; swap the model string only after checking its Hugging Face card.</li>
<li>Gated or missing checkpoints: run <code>huggingface-cli login</code> and confirm you accepted the model's license on Hugging Face.</li>
@@ -132,9 +22,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>Both recipes map to checked-in FastVideo examples and recorded single-GPU B200 runs. The image-to-video run also records 10,365.89 MB peak GPU memory. These measurements describe the recorded runs; they are not minimum hardware requirements.</p>
</div>
</details>
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Kandinsky 6 recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="kandinsky6" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/kandinsky.webp" alt="Kandinsky Lab" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Kandinsky 6 inference recipes</h2>
<p>Kandinsky 6 from the Kandinsky Lab generates five-second video with synchronized audio from text or an image, in Pro (30.1B) and Lite (3.2B) sizes with base and distilled pi-Flow checkpoints, and upscales existing clips with base or distilled video super-resolution.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="kandinsky6" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Kandinsky 6 recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/kandinskylab">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -123,9 +13,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>Text and image conditioning use the same TI2VA pipeline and example. Set <code>IMAGE_PATH</code> in the script to condition on an image; both base and distilled outputs include synchronized audio.</li>
<li>For pi-Flow, set <code>KANDINSKY6_MODEL_PATH</code> to the distilled model ID when running <code>basic_kandinsky6_ti2va.py</code>. Its registered preset uses 10 inference steps, guidance 1.0, <code>eps=1e-6</code>, <code>final_step_size_scale=0.5</code>, and <code>num_policy_substeps=128</code>.</li>
@@ -136,9 +26,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# LTX recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="ltx2" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/ltx.webp" alt="Lightricks" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>LTX inference recipes</h2>
<p>LTX-2 from Lightricks generates video with synchronized audio. FastVideo maintains both a distilled four-GPU path and a base 1088p single-GPU path.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="ltx2" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading LTX recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/Lightricks">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>The distilled recipe is source-configured for four GPUs; running it on fewer GPUs is unverified and may fail during distributed setup.</li>
<li>Audio-less output usually means the base checkpoint resolved instead of the distilled LTX-2 checkpoint with audio; check the loaded model ID in the logs.</li>
@@ -133,9 +23,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Matrix Game recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="matrixgame" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/fastvideo.webp" alt="FastVideo org (converted weights)" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Matrix Game inference recipes</h2>
<p>Matrix Game generates playable interactive worlds from an input image and prompt. Maintained examples cover Matrix Game 2.0 variants and Matrix Game 3.0 at 720p.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="matrixgame" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Matrix Game recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/FastVideo">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>Matrix Game 3.0 downloads its reference input image from GitHub; offline machines should pre-download it and edit the <code>IMAGE_URL</code> constant locally.</li>
<li>Streaming variants of Matrix Game 2.0 exist under <code>examples/inference/basic/basic_matrixgame2_streaming.py</code> but are not included as cookbook recipes.</li>
@@ -132,9 +22,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+12 -198
View File
@@ -1,55 +1,8 @@
---
hide:
- toc
---
# MiniMax H3 recipes
FastH3 is two distilled MiniMax-H3 checkpoints. **V1** is the four-step
launch. Some Hub repo names still say Preview. That name is historical. V1 is
a full model, not a demo. **V2** is the eight-step checkpoint. More forwards
is why V2 is the higher-quality FastH3. The V2 schedule contract is in
[FastH3 distilled checkpoint schedules](../inference/fasth3-distilled.md).
**CompactH3** is the 42-block 20B NVFP4 H3 checkpoint for one Blackwell GPU
(RTX 5090 or RTX PRO 6000). The FastH3 V1 and V2 recipes are unchanged.
<RecipeFamily family="minimax_h3" />
The 42-block pruned checkpoint has an [MLX INT8/INT6 conversion and
eight-forward T2VA command](../getting_started/installation/mlx.md#pruned-eight-forward-checkpoint).
It reads `fastvideo_inference.json` for the trained schedule. The command
uses native 832x480 resolution and all requested frames.
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="minimax_h3" data-default-recipe="fasth3-preview-cuda" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/minimax.webp" alt="MiniMax" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Primary focus · Inference</p>
<h2>MiniMax H3 recipes</h2>
<p>Generate video and audio with H3. Run a server on CUDA, one Blackwell GPU, one DGX Spark, or Apple Silicon MLX to iterate on prompts, or call the pipeline directly from Python.</p>
<span class="cookbook-count" data-cookbook-count>11 maintained recipes</span>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<details class="cookbook-modes">
<details >
<summary>Compare H3 modes and options</summary>
<h2 id="h3-modes-heading">Supported modes</h2>
<p>
@@ -59,7 +12,7 @@ uses native 832x480 resolution and all requested frames.
Temporal <code>--fast</code>, spatial <code>--fast-spatial</code>, and opt-in VSA are flags on the same
MLX script, not extra recipes.
</p>
<div class="cookbook-modes__table-wrap">
<div >
<table>
<thead>
<tr>
@@ -119,158 +72,19 @@ uses native 832x480 resolution and all requested frames.
</div>
</details>
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick an H3 recipe and runtime</h2>
<p>Choose the result you want, then use a maintained CUDA, Blackwell, DGX Spark, or MLX path.
Device claims stay tied to checked-in sources and recorded runs.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading MiniMax H3 recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<div class="cookbook-selection-row" data-cookbook-device-row hidden>
<div class="cookbook-selection-row__label">
<strong>Devices</strong>
<span data-cookbook-device-caption>1 Spark or a QSFP pair</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-device-options role="group" aria-label="Devices">
</div>
</div>
<div data-cookbook-knobs></div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<div class="cookbook-selection-row" data-cookbook-usage>
<div class="cookbook-selection-row__label">
<strong>Workflow</strong>
<span>Both can run locally</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" role="group" aria-label="How to run this recipe">
<button type="button" data-cookbook-mode="server" aria-pressed="false"><strong>Run a server</strong><span data-cookbook-server-hint>Playground, cURL, or an API client</span></button>
<button type="button" data-cookbook-mode="python" aria-pressed="false"><strong>Use Python directly</strong><span>Call the model in your own process</span></button>
</div>
</div>
<p class="cookbook-hardware-note" data-cookbook-serving-availability></p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Hardware</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre id="cookbook-local-command"><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<p class="cookbook-hardware-note" data-cookbook-python-note>Running this script again starts a new process and reloads the model. To iterate in Python, create the generator once and reuse it for multiple prompts.</p>
<div class="cookbook-serving" data-cookbook-serving hidden>
<p class="cookbook-serving__intro" data-cookbook-server-lifetime>Start once, then change prompts in the playground or your app. You can run the server and clients on the same machine.</p>
<section class="cookbook-serving__step" aria-labelledby="serving-install-heading">
<h4 id="serving-install-heading"><span aria-hidden="true">1</span> Prepare the machine</h4>
<p>Run from your FastVideo clone in an activated Python environment. See <a data-cookbook-install-guide href="../../getting_started/installation/gpu/">installation requirements</a>.</p>
<div class="cookbook-command"><div class="cookbook-command__bar"><span>GPU machine · Terminal</span></div><pre id="cookbook-server-install"><code class="language-bash" data-cookbook-server-install></code></pre></div>
<details class="cookbook-serving__prepare" data-cookbook-prepare hidden><summary>Download and convert MLX weights once</summary><p>Skip this if the weights are already prepared. Edit the paths in <code>examples/serving/mlx_fasth3.yaml</code> to use your existing files. Install <code>ffmpeg</code> for video and audio output.</p><div class="cookbook-command"><pre id="cookbook-server-prepare"><code class="language-bash" data-cookbook-server-prepare></code></pre></div></details>
</section>
<section class="cookbook-serving__step" aria-labelledby="serving-start-heading">
<h4 id="serving-start-heading"><span aria-hidden="true">2</span> Start the server</h4>
<p>Keep this terminal running while you use <span data-cookbook-playground-only>the playground or </span>API clients.</p>
<div class="cookbook-command"><div class="cookbook-command__bar"><span>GPU machine · Terminal</span></div><pre id="cookbook-server-command"><code class="language-bash" data-cookbook-server-command></code></pre></div>
<details class="cookbook-serving__check"><summary>Check that the server is ready</summary><p>In another terminal, this returns <code>{"status":"ok"}</code> after startup.</p><div class="cookbook-command"><pre id="cookbook-health-command"><code class="language-bash" data-cookbook-health-command></code></pre></div></details>
</section>
<section class="cookbook-serving__step" aria-labelledby="serving-client-heading">
<h4 id="serving-client-heading"><span aria-hidden="true">3</span> Generate and download a video</h4>
<div class="cookbook-serving__playground" data-cookbook-playground-only>
<div><strong>Try prompts in your browser</strong><p>Edit a prompt, generate, and watch the result. The playground uses the same server as cURL and your app.</p></div>
<a class="cookbook-serving__launch" data-cookbook-playground href="http://127.0.0.1:8000/playground/" target="_blank" rel="noopener">Open playground <span aria-hidden="true">↗</span></a>
</div>
<p class="cookbook-serving__local-hint"><span data-cookbook-playground-only>Open after the server is ready. </span>On a remote GPU machine, <a href="../openai-api/#connect-your-app">forward port 8000</a> to your computer first.<span data-cookbook-playground-only> This opens a local page, not a hosted demo.</span></p>
<details class="cookbook-serving__code"><summary>Use cURL or an SDK</summary>
<p>Each example submits a job, checks its status, and saves the MP4. The Python and JavaScript examples use OpenAI-compatible clients; no OpenAI account is needed.</p>
<div class="cookbook-serving__clients" role="group" aria-label="API client language">
<button type="button" data-cookbook-client="curl" aria-pressed="false">cURL</button>
<button type="button" data-cookbook-client="python" aria-pressed="true">Python</button>
<button type="button" data-cookbook-client="javascript" aria-pressed="false">JavaScript</button>
</div>
<div class="cookbook-command"><div class="cookbook-command__bar"><span>Client dependencies</span></div><pre id="cookbook-client-install"><code class="language-bash" data-cookbook-client-install></code></pre></div>
<div class="cookbook-command cookbook-command--client"><div class="cookbook-command__bar"><span data-cookbook-client-filename>video.py</span><a data-cookbook-client-source href="https://github.com/hao-ai-lab/FastVideo/tree/main/examples/serving/clients">View source</a></div><pre id="cookbook-client-code"><code data-cookbook-client-code></code></pre></div>
<p data-cookbook-client-run></p>
</details>
</section>
<p class="cookbook-serving__boundary">This is a local development server without built-in API-key authentication. The client key <code>local</code> is a placeholder. Keep the server on loopback; use an authenticated TLS proxy before exposing it publicly. Run the JavaScript client in your webapp's backend, not in a browser with a private key.</p>
<a href="../openai-api/">Server guide and API compatibility →</a>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/MiniMaxAI">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../openai-api/">H3 server guide</a> or the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
<p>Use <a href="../../inference/configuration/">Configuration</a> for supported Python and CLI settings, <a href="../../inference/optimizations/">Optimizations</a> for attention and memory tradeoffs, and the <a href="../../inference/support_matrix/">support matrix</a> for the supported model and optimization surface.</p>
<p class="cookbook-eyebrow">CUDA</p>
<p >CUDA</p>
<pre><code>UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"</code></pre>
<p class="cookbook-eyebrow">Apple Silicon</p>
<p >Apple Silicon</p>
<pre><code>uv pip install -e ".[mlx]"</code></pre>
<p>Follow the <a href="../../getting_started/installation/mlx/">MLX install guide</a> for the extra, <code>ffmpeg</code>, and a clone. Then pick FastH3 V1 or V2 in the builder above.</p>
<p class="cookbook-eyebrow">NVIDIA DGX Spark</p>
<p >NVIDIA DGX Spark</p>
<pre><code>UV_TORCH_BACKEND=cu130 uv pip install -e .</code></pre>
<p>Follow the <a href="../../getting_started/installation/spark/">DGX Spark install guide</a> for ARM64 CUDA 13. One Spark is a local process. Two Sparks need Ray on the QSFP link:</p>
<pre><code>uv pip install ray</code></pre>
@@ -278,9 +92,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>The full CUDA H3 examples request four GPUs by default. Their sources do not claim a GPU model or memory minimum.</li>
<li>The FastH3 CUDA performance profile was measured on four GB200 GPUs. Use its strict profile when exact operation order matters more than the measured performance configuration.</li>
@@ -294,9 +108,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>Every command, model ID, and flag on this page maps to a checked-in FastVideo source. Recipes marked <strong>Verified</strong> also have a recorded hardware path in linked FastVideo evidence. The full H3 CUDA examples remain <strong>Source-backed</strong> where the source records a GPU count but no GPU model or memory requirement. Unlisted hardware is unknown, not unsupported.</p>
</div>
</details>
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# MMAudio recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="mmaudio" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<span class="cookbook-family-tile__monogram" aria-hidden="true">MMA</span>
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>MMAudio inference recipes</h2>
<p>MMAudio adds synchronized audio to video or generates audio from text. The checkpoint must be in Diffusers layout; the recipe loads the converted FastVideo repo through the `MMAUDIO_MODEL_PATH` environment variable the example reads.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="mmaudio" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading MMAudio recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/FastVideo">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>If the example reports a missing local <code>converted_weights/mmaudio/large_44k_v2</code> path, the <code>MMAUDIO_MODEL_PATH</code> env var from the cookbook command was not applied; export it in the same shell.</li>
<li>Alternatively convert upstream weights yourself with <code>scripts/checkpoint_conversion/convert_mmaudio_to_diffusers.py</code> and point the env var at the result.</li>
@@ -132,9 +22,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+3 -9
View File
@@ -181,9 +181,7 @@ python -m pip install openai==3.6.0
python examples/serving/clients/video.py
```
```python
--8<-- "examples/serving/clients/video.py"
```
<include>/_snippets/clients/video.py.md</include>
### OpenAI JavaScript
@@ -195,9 +193,7 @@ npm ci --prefix examples/serving/clients
node examples/serving/clients/video.mjs
```
```javascript
--8<-- "examples/serving/clients/video.mjs"
```
<include>/_snippets/clients/video.mjs.md</include>
### cURL
@@ -207,9 +203,7 @@ Install `curl` and `jq`, then run:
bash examples/serving/clients/video.sh
```
```bash
--8<-- "examples/serving/clients/video.sh"
```
<include>/_snippets/clients/video.sh.md</include>
## Connect your app
+51
View File
@@ -0,0 +1,51 @@
title: FastH3 Trim
summary: Eight-step video and audio from the released NVFP4 checkpoint.
guide: docs/getting_started/installation/spark_performance.md
deployments:
spark-rest:
label: DGX Spark REST
runtime: fastvideo-cuda-rest
workload: t2v
defaults: examples/serving/openai_fasth3_trim_spark.yaml
env:
FASTVIDEO_MINIMAX_H3_FUSIONS: all
FASTVIDEO_NVFP4_MM_BACKEND: cutlass
FASTVIDEO_H3_VAE_TILE_BATCH: "1"
FASTVIDEO_VSA_TRITON: "1"
FASTVIDEO_VSA_SM100A: "0"
FASTVIDEO_FA4: "0"
FASTVIDEO_ATTENTION_BACKEND: VIDEO_SPARSE_ATTN_H3
FASTVIDEO_STAGE_LOGGING: "1"
requirements:
- Install FastVideo using the DGX Spark guide before running these commands.
- Based on the released one-Spark inference recipe. This serving adaptation and edited hardware combinations have not been GPU-validated.
- The checkpoint includes NVFP4 weights and encoder, a lightweight video VAE, and its trained eight-forward schedule. No local CompactH3 assembly is needed.
options:
- server.*
- generator.engine.num_gpus
- generator.engine.parallelism.tp_size
- generator.engine.parallelism.sp_size
- generator.engine.offload.text_encoder
- generator.engine.offload.vae
- default_request.prompt
- default_request.sampling.num_frames
- default_request.sampling.height
- default_request.sampling.width
- default_request.sampling.seed
overrides:
default_request.prompt:
title: Prompt
type: string
minLength: 1
default_request.sampling.num_frames:
enum: [124, 243, 345]
default: 124
description: Native H3 frame counts. Hardware fit has not been verified.
default_request.sampling.height:
minimum: 32
multipleOf: 32
default: 480
default_request.sampling.width:
minimum: 32
multipleOf: 32
default: 832
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Stable Audio recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="stable_audio" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/stabilityai.webp" alt="Stability AI" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Stable Audio inference recipes</h2>
<p>Stable Audio Open generates audio from text. FastVideo requires the converted Diffusers-format repos published by the FastVideo organization; upstream monolithic checkpoints are not loader-compatible.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="stable_audio" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Stable Audio recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/stabilityai">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>Loader errors about monolithic checkpoints mean an upstream <code>stabilityai/stable-audio-open-*</code> ID was used; use the FastVideo converted repos from the recipes.</li>
<li>Duration and step knobs (<code>audio_end_in_s</code>, <code>num_inference_steps</code>) are documented inline in the example source.</li>
@@ -132,9 +22,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The Stable Audio Open 1.0 recipe maps to a checked-in example and a recorded single-GPU B200 run. The Stable Audio Open Small recipe remains <strong>Source-backed</strong> because the implementation PR did not record a full run for that gated checkpoint. Neither recipe claims a minimum VRAM requirement.</p>
</div>
</details>
+7 -117
View File
@@ -1,120 +1,10 @@
---
hide:
- toc
---
# Stable Diffusion recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="sd35" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/stabilityai.webp" alt="Stability AI" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Stable Diffusion inference recipes</h2>
<p>Stable Diffusion 3.5 Medium is Stability AI's text-to-image model in this catalog. The maintained example sweeps a small prompt set across seeds.</p>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="sd35" />
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Stable Diffusion recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Source configuration</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/stabilityai">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -122,9 +12,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>The example writes several PNGs under <code>outputs/sd35/samples/</code>; make sure the output directory is writable.</li>
<li>Gated checkpoints: Stability AI models require accepting the license and <code>huggingface-cli login</code>.</li>
@@ -132,9 +22,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>All recipes on this page are <strong>Source-backed</strong>: their commands, model IDs, and flags were validated against the checked-in FastVideo sources listed above (static validation). No runtime GPU validation is recorded for these recipes, so GPU model fit, memory use, throughput, and runtime duration are <strong>Unknown</strong> and deliberately not claimed. Runtime buttons show only the GPU counts configured in checked-in sources.</p>
</div>
</details>
+9 -169
View File
@@ -1,42 +1,8 @@
---
hide:
- toc
---
# Wan recipes
<div class="cookbook-shell cookbook-family-page" data-cookbook data-family="wan" data-recipes="../../assets/cookbook-recipes.json?v=16">
<header class="cookbook-family-header">
<a class="cookbook-back-link" href="../"><span aria-hidden="true">←</span> All model families</a>
<div class="cookbook-family-header__body">
<span class="cookbook-family-header__logo">
<img class="off-glb" src="../../assets/logos/wan-ai.webp" alt="Wan-AI" width="112" height="112">
</span>
<div>
<p class="cookbook-eyebrow">Maintained family · Inference</p>
<h2>Wan inference recipes</h2>
<p>CUDA covers FastWan and Wan2.1/2.2 text and image recipes. Apple Silicon uses the released FastMetal 1.3B, 5B, and 14B MLX T2V paths. The speed flags below are switches on those same scripts, not extra recipes.</p>
<span class="cookbook-count" data-cookbook-count>7 maintained recipes</span>
</div>
</div>
<div class="cookbook-lifecycle" aria-label="Lifecycle stages">
<span class="cookbook-lifecycle__stage cookbook-lifecycle__stage--active">Inference <small>live</small></span>
<span class="cookbook-lifecycle__stage">Distillation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Fine-tuning <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Training <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Evaluation <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Optimization <small>planned</small></span>
<span class="cookbook-lifecycle__stage">Deployment <small>planned</small></span>
</div>
</header>
<nav class="cookbook-jumpnav" aria-label="Recipe page sections">
<a href="#recipe-builder">Builder</a>
<a href="#cookbook-setup">Setup</a>
<a href="#cookbook-troubleshooting">Troubleshooting</a>
<a href="#cookbook-evidence">Evidence</a>
</nav>
<RecipeFamily family="wan" />
<details class="cookbook-modes">
<details >
<summary>Compare Wan modes and options</summary>
<h2 id="wan-modes-heading">Supported modes</h2>
<p>
@@ -48,7 +14,7 @@ hide:
<code>basic_mps.py</code> is the older PyTorch MPS demo and is not a
FastMetal recipe.
</p>
<div class="cookbook-modes__table-wrap">
<div >
<table>
<thead>
<tr>
@@ -93,135 +59,9 @@ hide:
</div>
</details>
<section class="cookbook-builder" id="recipe-builder" aria-labelledby="builder-heading">
<div class="cookbook-builder__intro">
<h2 id="builder-heading">Pick a recipe and runtime</h2>
<p>Choose the result you want, then use a maintained CUDA or native MLX path.</p>
</div>
<div class="cookbook-builder__layout">
<div class="cookbook-controls">
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Recipe</strong>
<span>Task and checkpoint</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--models" data-cookbook-model-options role="group" aria-label="Recipe">
<button type="button" disabled>Loading Wan recipes...</button>
</div>
</div>
<div class="cookbook-selection-row">
<div class="cookbook-selection-row__label">
<strong>Runtime</strong>
<span>Maintained paths only</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" data-cookbook-hardware-options role="group" aria-label="Runtime">
<button type="button" disabled>Loading runtimes...</button>
</div>
</div>
<p class="cookbook-selection-description" data-cookbook-description>Loading recipe details...</p>
<div class="cookbook-selection-row" data-cookbook-usage>
<div class="cookbook-selection-row__label">
<strong>Workflow</strong>
<span>Both can run locally</span>
</div>
<div class="cookbook-option-grid cookbook-option-grid--hardware" role="group" aria-label="How to run this recipe">
<button type="button" data-cookbook-mode="server" aria-pressed="false"><strong>Run a server</strong><span data-cookbook-server-hint>Playground, cURL, or an API client</span></button>
<button type="button" data-cookbook-mode="python" aria-pressed="false"><strong>Use Python directly</strong><span>Call the model in your own process</span></button>
</div>
</div>
<p class="cookbook-hardware-note" data-cookbook-serving-availability></p>
<p class="cookbook-hardware-note">Exact device and memory details appear only when a recorded run supports them.</p>
<div class="cookbook-hardware-state" data-cookbook-hardware-state role="status" aria-live="polite">
Reading recipe evidence...
</div>
</div>
<article class="cookbook-result">
<div class="cookbook-result__header">
<h3 data-cookbook-label>Loading...</h3>
<div class="cookbook-result__badges">
<span class="cookbook-badge">Maintained</span>
<span class="cookbook-badge" data-cookbook-evidence>Source-backed</span>
<span class="cookbook-badge cookbook-badge--neutral" data-cookbook-hardware-badge>Source config</span>
</div>
</div>
<dl class="cookbook-result__facts">
<div><dt>Model</dt><dd data-cookbook-model>Loading...</dd></div>
<div><dt>Workload</dt><dd data-cookbook-task>Loading...</dd></div>
<div><dt>Hardware</dt><dd data-cookbook-gpus>Loading...</dd></div>
<div><dt>Expected output</dt><dd data-cookbook-artifact>Loading...</dd></div>
</dl>
<div class="cookbook-command">
<div class="cookbook-command__bar">
<span>Terminal</span>
</div>
<pre id="cookbook-local-command"><code class="language-bash" data-cookbook-command>Loading...</code></pre>
</div>
<p class="cookbook-hardware-note" data-cookbook-python-note>Running this script again starts a new process and reloads the model. To iterate in Python, create the generator once and reuse it for multiple prompts.</p>
<div class="cookbook-serving" data-cookbook-serving hidden>
<p class="cookbook-serving__intro" data-cookbook-server-lifetime>Start once, then change prompts in the playground or your app. You can run the server and clients on the same machine.</p>
<section class="cookbook-serving__step" aria-labelledby="serving-install-heading">
<h4 id="serving-install-heading"><span aria-hidden="true">1</span> Prepare the machine</h4>
<p>Run from your FastVideo clone in an activated Python environment. See <a data-cookbook-install-guide href="../../getting_started/installation/gpu/">installation requirements</a>.</p>
<div class="cookbook-command"><div class="cookbook-command__bar"><span>GPU machine · Terminal</span></div><pre id="cookbook-server-install"><code class="language-bash" data-cookbook-server-install></code></pre></div>
<details class="cookbook-serving__prepare" data-cookbook-prepare hidden><summary>Download and convert MLX weights once</summary><p>Skip this if the weights are already prepared. Edit the paths in the serving config to use your existing files.</p><div class="cookbook-command"><pre id="cookbook-server-prepare"><code class="language-bash" data-cookbook-server-prepare></code></pre></div></details>
</section>
<section class="cookbook-serving__step" aria-labelledby="serving-start-heading">
<h4 id="serving-start-heading"><span aria-hidden="true">2</span> Start the server</h4>
<p>Keep this terminal running while you use <span data-cookbook-playground-only>the playground or </span>API clients.</p>
<div class="cookbook-command"><div class="cookbook-command__bar"><span>GPU machine · Terminal</span></div><pre id="cookbook-server-command"><code class="language-bash" data-cookbook-server-command></code></pre></div>
<details class="cookbook-serving__check"><summary>Check that the server is ready</summary><p>In another terminal, this returns <code>{"status":"ok"}</code> after startup.</p><div class="cookbook-command"><pre id="cookbook-health-command"><code class="language-bash" data-cookbook-health-command></code></pre></div></details>
</section>
<section class="cookbook-serving__step" aria-labelledby="serving-client-heading">
<h4 id="serving-client-heading"><span aria-hidden="true">3</span> Generate and download a video</h4>
<div class="cookbook-serving__playground" data-cookbook-playground-only>
<div><strong>Try prompts in your browser</strong><p>Edit a prompt, generate, and watch the result. The playground uses the same server as cURL and your app.</p></div>
<a class="cookbook-serving__launch" data-cookbook-playground href="http://127.0.0.1:8000/playground/" target="_blank" rel="noopener">Open playground <span aria-hidden="true">↗</span></a>
</div>
<p class="cookbook-serving__local-hint"><span data-cookbook-playground-only>Open after the server is ready. </span>On a remote GPU machine, <a href="../openai-api/#connect-your-app">forward port 8000</a> to your computer first.<span data-cookbook-playground-only> This opens a local page, not a hosted demo.</span></p>
<details class="cookbook-serving__code"><summary>Use cURL or an SDK</summary>
<p>Each example submits a job, checks its status, and saves the MP4. The Python and JavaScript examples use OpenAI-compatible clients; no OpenAI account is needed.</p>
<div class="cookbook-serving__clients" role="group" aria-label="API client language">
<button type="button" data-cookbook-client="curl" aria-pressed="false">cURL</button>
<button type="button" data-cookbook-client="python" aria-pressed="true">Python</button>
<button type="button" data-cookbook-client="javascript" aria-pressed="false">JavaScript</button>
</div>
<div class="cookbook-command"><div class="cookbook-command__bar"><span>Client dependencies</span></div><pre id="cookbook-client-install"><code class="language-bash" data-cookbook-client-install></code></pre></div>
<div class="cookbook-command cookbook-command--client"><div class="cookbook-command__bar"><span data-cookbook-client-filename>video.py</span><a data-cookbook-client-source href="https://github.com/hao-ai-lab/FastVideo/tree/main/examples/serving/clients">View source</a></div><pre id="cookbook-client-code"><code data-cookbook-client-code></code></pre></div>
<p data-cookbook-client-run></p>
</details>
</section>
<p class="cookbook-serving__boundary">This is a local development server without built-in API-key authentication. The client key <code>local</code> is a placeholder. Keep the server on loopback; use an authenticated TLS proxy before exposing it publicly. Run the JavaScript client in your webapp's backend, not in a browser with a private key.</p>
<a href="../openai-api/">Server guide and API compatibility →</a>
</div>
<div class="cookbook-result__footer">
<a data-cookbook-source href="../../inference/examples/basic/">Open example source</a>
<a data-cookbook-model-link href="https://huggingface.co/Wan-AI">View model card</a>
</div>
<p class="cookbook-picker__status" role="status" aria-live="polite" data-cookbook-status></p>
</article>
</div>
<noscript>
<div class="cookbook-noscript">
JavaScript is needed for the guided selector. You can still browse the
<a href="../../inference/examples/examples_inference_index/">maintained inference examples</a>.
</div>
</noscript>
</section>
</div>
<details class="cookbook-collapsible" id="cookbook-setup">
<details id="cookbook-setup">
<summary>Setup</summary>
<div class="cookbook-collapsible__body">
<div >
<p>The generated commands expect a local clone:</p>
<pre><code>git clone https://github.com/hao-ai-lab/FastVideo.git
cd FastVideo</code></pre>
@@ -229,9 +69,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-troubleshooting">
<details id="cookbook-troubleshooting">
<summary>Troubleshooting</summary>
<div class="cookbook-collapsible__body">
<div >
<ul>
<li>Out of memory on the A14B recipes: the checked-in sources already enable CPU offload; see <a href="../../inference/configuration/">Configuration</a> for the offload surface before reducing resolution or frames.</li>
<li>The FastWan2.1 recipe requires <code>VIDEO_SPARSE_ATTN</code>; confirm the environment variable in the command was set in the same shell.</li>
@@ -242,9 +82,9 @@ cd FastVideo</code></pre>
</div>
</details>
<details class="cookbook-collapsible" id="cookbook-evidence">
<details id="cookbook-evidence">
<summary>Evidence status</summary>
<div class="cookbook-collapsible__body">
<div >
<p>Every recipe on this page maps to a checked-in FastVideo source. The FastMetal MLX releases include the recorded M4 Max system memory, documented unified-memory floor, and measured peak MLX memory. CUDA entries remain <strong>Source-backed</strong> where the examples record a GPU count but no exact GPU model or VRAM. Unlisted hardware is unknown, not unsupported.</p>
</div>
</details>
+6 -5
View File
@@ -1,10 +1,11 @@
# Training Architecture
!!! warning "Work in Progress"
This training architecture (`fastvideo/train/`) is under active development
and is replacing the older `fastvideo/training/` module. APIs, config
formats, and supported methods may change. See the
[Current Status](#current-status) section for what is implemented so far.
:::warning[Work in Progress]
This training architecture (`fastvideo/train/`) is under active development
and is replacing the older `fastvideo/training/` module. APIs, config
formats, and supported methods may change. See the
[Current Status](#current-status) section for what is implemented so far.
:::
FastVideo's training framework (`fastvideo/train/`) is built around a
**pluggable, YAML-driven architecture** that cleanly separates **models**,

Some files were not shown because too many files have changed in this diff Show More