[docs] Unify cookbook model editors and launch commands (#1970)

Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
This commit is contained in:
Aryan Kumar
2026-10-16 17:26:51 -07:00
committed by GitHub
co-authored by Aryan Kumar
parent 3f45f2da21
commit 25940a7e86
24 changed files with 1274 additions and 233 deletions
@@ -1,4 +1,4 @@
import { useId, useState } from "react";
import { useEffect, useId, useState } from "react";
import { Input } from "@/components/ui/input";
export default function HardwarePicker({
@@ -14,10 +14,11 @@ export default function HardwarePicker({
}) {
const [search, setSearch] = useState("");
const [adding, setAdding] = useState(false);
const [custom, setCustom] = useState(() => {
const [custom, setCustom] = useState("");
useEffect(() => {
const requested = new URL(window.location.href).searchParams.get("hardware");
return requested?.startsWith("custom:") ? requested.slice(7) : "";
});
setCustom(requested?.startsWith("custom:") ? requested.slice(7) : "");
}, []);
const id = useId();
const known = options.filter((option) => option.join(" ").toLowerCase().includes(search.toLowerCase()));
function add() {
@@ -1,43 +0,0 @@
import ModelHeader from "./ModelHeader";
import ComponentPlan from "./ComponentPlan";
import { sitePath, type DirectoryModel } from "@/lib/cookbook-pages";
export default function ModelRelease({ model }: { model: DirectoryModel }) {
return (
<div className="cookbook not-prose recipe-detail">
<ModelHeader model={model} />
<div className="release-intro">
<p>FastH3 Trim is the smaller FastH3 model, previously called CompactH3.</p>
<p>A maintained serving recipe is being prepared. Use the published checkpoint instructions for setup.</p>
</div>
<section className="recipe-matrix" aria-label="Model releases">
<div className="matrix-row">
<h2>Weights</h2>
<div className="matrix-row-content">
<div className="option-chips">
{[
["BF16", "FastVideo-FastH3-Trim-8-Step"],
["FP8", "FastVideo-FastH3-Trim-8-Step-FP8"],
["NVFP4", "FastVideo-FastH3-Trim-8-Step-NVFP4"],
].map(([name, repo]) => (
<a className="option-chip-static" key={repo} href={`https://huggingface.co/FastVideo/${repo}`}>
{name} ↗
</a>
))}
</div>
</div>
</div>
<div className="matrix-row">
<h2>Components</h2>
<div className="matrix-row-content">
<ComponentPlan />
</div>
</div>
</section>
<div className="recipe-source-links">
<a href={sitePath("/inference/fasth3-distilled")}>FastH3 checkpoint guide →</a>
<a href="https://huggingface.co/collections/FastVideo/fasth3-v2">Release collection ↗</a>
</div>
</div>
);
}
@@ -0,0 +1,7 @@
---
title: FastH3 Trim
description: Configure FastH3 Trim NVFP4 and copy inference or serving commands.
mode: frame
---
<Cookbook modelId="fasth3-trim" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 121 KiB

+181 -46
View File
@@ -8,8 +8,8 @@ import { Textarea } from "@/components/ui/textarea";
import SourceRecipe from "./SourceRecipe";
import ComponentPlan from "@/components/cookbook/ComponentPlan";
import HardwarePicker from "@/components/cookbook/HardwarePicker";
import ModelRelease from "@/components/cookbook/ModelRelease";
import ModelHeader from "@/components/cookbook/ModelHeader";
import { configAndLaunch, inferenceOutput, sdkCommand } from "@/lib/cookbook-commands";
import {
api,
guideUrl,
@@ -194,14 +194,23 @@ function Editor({
}) {
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") ?? "unspecified",
() =>
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 {
@@ -209,11 +218,18 @@ function Editor({
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("");
@@ -240,8 +256,8 @@ function Editor({
}
}
function download() {
if (!result) return;
const url = URL.createObjectURL(new Blob([result.yaml], { type: "application/yaml" }));
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";
@@ -250,16 +266,20 @@ function Editor({
setNotice("Downloaded config.yaml.");
}
const mlx = catalog.runtime.backend === "mlx";
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"],
];
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) => (
@@ -289,24 +309,78 @@ function Editor({
model={model}
actions={
<>
<Button variant="outline" disabled={!result} onClick={() => result && copy(result.yaml)}>
<Button variant="outline" disabled={!output} onClick={() => output && copy(output.yaml)}>
Copy YAML
</Button>
<Button disabled={!result} onClick={download}>
<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 defaultValue="commands">
<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>
@@ -316,38 +390,50 @@ function Editor({
</div>
)}
<Tabs.Content value="commands" className="recipe-command-content">
{result && (
{result && output && (
<>
<div className="recipe-code-heading">
<span>Serve</span>
<button type="button" onClick={() => copy(result.command)}>
Copy server command
</button>
<span>{runMode === "sdk" ? "Python SDK" : runMode === "inference" ? "Generate" : "Serve"}</span>
</div>
<pre tabIndex={0} aria-label="Server command">
<code>{result.command}</code>
<pre
tabIndex={0}
aria-label={
runMode === "sdk" ? "SDK command" : runMode === "inference" ? "Inference command" : "Server command"
}
>
<code>{launchCommand}</code>
</pre>
<details className="recipe-request-code">
<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>
<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">
{result && (
{output && (
<pre tabIndex={0} aria-label="Generated YAML">
<code>{result.yaml}</code>
<code>{output.yaml}</code>
</pre>
)}
</Tabs.Content>
@@ -358,7 +444,7 @@ function Editor({
</section>
<details className="recipe-install">
<summary>
Installation <span>{catalog.runtime.label}</span>
Installation <span>{catalog.runtime.label.replace(/\s*REST/gi, "")}</span>
</summary>
{catalog.requirements?.length ? (
<ul>
@@ -385,7 +471,7 @@ function Editor({
if (catalog.id !== deployment.id) selectRuntime(deployment.id);
}}
>
{deployment.runtime.includes("mlx") ? "MLX" : "NVIDIA"}
{deployment.runtime.includes("mlx") ? "MLX" : "CUDA"}
</button>
))}
</div>
@@ -415,8 +501,43 @@ function Editor({
</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>
)}
@@ -461,11 +582,12 @@ function Editor({
</details>
</section>
<div className="recipe-bottom-actions">
<span>Save config.yaml before running the server.</span>
<span>Copy config and launch together, or download config.yaml first.</span>
<Button
variant="ghost"
onClick={() => {
setEdits({});
setSourceImage("");
setComponentDraft(false);
setParseErrors({});
setReset(reset + 1);
@@ -489,6 +611,8 @@ function MatrixRow({ title, children }: { title: string; children: ReactNode })
}
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>();
@@ -555,8 +679,19 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
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?.releaseOnly) return <ModelRelease model={directoryModel} />;
if (directoryModel && !directoryModel.configurable) return <SourceRecipe model={directoryModel} />;
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 ? (
@@ -586,7 +721,7 @@ export default function Cookbook({ modelId }: { modelId?: string }) {
aria-pressed={selected === deployment.id}
onClick={() => select(deployment.id)}
>
{deployment.runtime.includes("mlx") ? "MLX" : "NVIDIA"}
{deployment.runtime.includes("mlx") ? "MLX" : "CUDA"}
</button>
))}
</div>
+454 -106
View File
@@ -1,35 +1,124 @@
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";
/** Existing inference recipes stay runnable while editable serving catalogs are added. */
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("inference");
const [yaml, setYaml] = useState("");
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(`/assets/cookbook-sources/${recipe.id}-${mode}.yaml`);
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();
setYaml("");
setError("");
if (yamlSource)
fetch(sourceUrl, { signal: controller.signal })
.then(async (response) => {
if (!response.ok) throw new Error(`YAML could not be loaded: HTTP ${response.status}.`);
const text = await response.text();
if (!controller.signal.aborted) setYaml(text);
})
.catch((error) => {
if (!controller.signal.aborted) setError(error.message);
});
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]);
}, [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);
@@ -38,140 +127,399 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) {
setNotice("Copy failed. Select and copy the text below.");
}
}
const command =
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={
<>
{yamlSource ? (
<>
<Button variant="outline" disabled={!yaml} onClick={() => copy(yaml)}>
Copy YAML
</Button>
<Button asChild>
<a href={sourceUrl} download={yamlSource?.split("/").at(-1)}>
Download YAML
</a>
</Button>
</>
) : (
<Button variant="outline" onClick={() => copy(command)}>
Copy command
</Button>
)}
<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>
</>
}
/>
<section className="recipe-command-box" aria-label="Source recipe">
<div className="recipe-command-toolbar">
<span className="recipe-unverified">Unverified recipe</span>
<button type="button" onClick={() => copy(command)}>
Copy command
<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>
<div className="recipe-command-content">
<pre tabIndex={0} aria-label="Inference command">
<code>{command}</code>
</pre>
{yamlSource && (
<details>
<summary>View source YAML</summary>
{error ? (
<p role="alert">{error}</p>
) : (
<pre tabIndex={0} aria-label="Source YAML">
<code>{yaml || "Loading YAML…"}</code>
</pre>
))}
</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>
)}
</details>
{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>
)}
</div>
{(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</summary>
<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="Recipe options">
<div className="matrix-row">
<h2>Recipe</h2>
<div className="matrix-row-content">
<div className="option-chips" role="group" aria-label="Recipe variant">
{model.recipes.map((entry) => (
<button
key={entry.id}
type="button"
aria-pressed={selected === entry.id}
onClick={() => {
setSelected(entry.id);
setMode("inference");
setNotice("");
setYaml("");
}}
>
{entry.label}
</button>
))}
</div>
</div>
</div>
<section className="recipe-matrix" aria-label="Configuration settings">
<div className="matrix-row">
<h2>Hardware</h2>
<div className="matrix-row-content">
<div className="option-chips">
{recipe.gpu_types.map((platform) => (
<span className="option-chip-static" key={platform}>
{platform === "Apple Silicon" ? "MLX" : platform}
</span>
<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">
{recipe.hardware?.accelerator ??
(recipe.hardware?.gpu_count
? `${recipe.hardware.gpu_count} GPU${recipe.hardware.gpu_count === 1 ? "" : "s"} in the source recipe`
: "See the model guide for resources")}
Hardware fit has not been verified. Settings edit the source example; device choice is a preference.
</p>
</div>
</div>
<div className="matrix-row">
<h2>Run mode</h2>
<div className="matrix-row-content">
<div className="option-chips" role="group" aria-label="Run mode">
<button type="button" aria-pressed={mode === "inference"} onClick={() => setMode("inference")}>
Inference
</button>
{recipe.serving && (
<button type="button" aria-pressed={mode === "serving"} onClick={() => setMode("serving")}>
Serving
</button>
)}
{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-source-links">
<a href={sitePath(model.familyPath)}>Model guide →</a>
<div className="recipe-bottom-actions">
<a
href={`https://github.com/hao-ai-lab/FastVideo/blob/main/${mode === "serving" ? recipe.serving?.source : recipe.source}`}
>
View source ↗
View original example ↗
</a>
<span>
Run from the FastVideo checkout. Source recipe. Interactive YAML configuration is not available for this model
yet.
</span>
<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",
),
};
}
+19 -22
View File
@@ -12,6 +12,15 @@ export const recipePages = [
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",
@@ -56,7 +65,15 @@ export interface SourceRecipe {
command: string;
gpu_types: string[];
hardware?: { gpu_count?: number; platform?: string; accelerator?: string };
serving?: { source: string; install: string; env?: 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 }> = {
@@ -75,6 +92,7 @@ const familyInfo: Record<string, { title: string; path: string; logo: string; ca
};
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",
@@ -116,7 +134,6 @@ export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.m
path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`,
logo,
provider: providers[logo],
releaseOnly: false,
category: family.category,
description: backend?.description ?? first.summary,
tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))],
@@ -125,26 +142,6 @@ export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.m
featured: backend?.featured ?? false,
};
})
.concat([
{
id: "fasth3-trim",
model: "FastVideo/FastVideo-FastH3-Trim-8-Step",
recipes: [] as SourceRecipe[],
title: "FastH3 Trim",
family: "FastH3",
familyPath: "/cookbook/minimax-h3",
path: "/cookbook/minimax-h3/fasth3-trim",
logo: "fastvideo",
provider: "FastVideo",
releaseOnly: true,
category: "Video",
description: "The smaller FastH3 release, previously called CompactH3.",
tags: ["Text to video", "Audio", "8-step"],
platforms: ["NVIDIA", "Apple Silicon"],
configurable: false,
featured: false,
},
])
.sort(
(a, b) =>
Number(b.featured) - Number(a.featured) ||
+11
View File
@@ -36,6 +36,8 @@ export interface Catalog {
id: string;
title: string;
summary?: string;
workload: string;
env: Record<string, string>;
base_config: Record<string, Value>;
runtime: {
label: string;
@@ -48,12 +50,21 @@ export interface Catalog {
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[];
+4
View File
@@ -142,6 +142,10 @@
"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"
+1 -1
View File
@@ -15,7 +15,7 @@
"test": "playwright test",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test:content": "node --test tests/content.test.mjs"
"test:content": "node --test tests/content.test.mjs tests/commands.test.mjs"
},
"dependencies": {
"blume": "2.2.3",
+3
View File
@@ -22,6 +22,9 @@ await cp(new URL("docs/assets/cookbook-config/", repo), destination, {
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);
+85
View File
@@ -2,10 +2,12 @@
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
@@ -18,6 +20,68 @@ 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:
@@ -159,6 +223,27 @@ def prepare() -> None:
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,
+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);
});
+114 -4
View File
@@ -117,12 +117,12 @@ test("an obsolete deployment response cannot replace the selected deployment", a
await output(page, "config.yaml");
await expect(page.getByLabel("Generated YAML", { exact: true })).toBeVisible();
await backend(page, "MLX");
await backend(page, "NVIDIA");
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: "NVIDIA", exact: true }),
page.getByRole("group", { name: "Hardware backend" }).getByRole("button", { name: "CUDA", exact: true }),
).toHaveAttribute("aria-pressed", "true");
});
@@ -245,10 +245,18 @@ test("image and audio models have logos and dedicated source recipe pages", asyn
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.getByText(/Interactive YAML configuration is not available for this model yet/)).toBeVisible();
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 }) => {
@@ -259,10 +267,112 @@ test("provider labels match FastVideo logos and CompactH3 is replaced by FastH3
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: "NVFP4 ↗", exact: true })).toHaveAttribute(
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 }) => {
+19
View File
@@ -1,4 +1,5 @@
"""Regression checks for source-only API rendering, without importing model code."""
import ast
import importlib.util
from pathlib import Path
import tempfile
@@ -69,6 +70,24 @@ class Empty:
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")]:
+24
View File
@@ -687,6 +687,30 @@
margin-block-end: 1rem;
overflow: clip;
}
.recipe-run-modes {
margin-block-end: 1rem;
}
.recipe-command-tools {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
order: 3;
}
.recipe-command-tools button {
min-block-size: 1.75rem;
padding: 0.25rem 0.625rem;
border: 1px solid var(--recipe-line);
border-radius: 0.375rem;
color: var(--blume-foreground);
background: var(--blume-background);
}
.recipe-command-tools button[aria-pressed="true"] {
border-color: currentColor;
}
.recipe-environment {
padding: 1rem;
border-block-end: 1px solid var(--recipe-line);
}
.recipe-command-toolbar {
display: flex;
flex-wrap: wrap;
+26 -1
View File
@@ -1,5 +1,5 @@
{
"version": 16,
"version": 17,
"recipes": [
{
"id": "fastwan21-t2v",
@@ -832,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."
}
}
]
}
+23 -3
View File
@@ -328,8 +328,13 @@ 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 exact checked-in YAML under
`assets/cookbook-sources/`; it does not synthesize settings for these pages.
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
@@ -351,7 +356,22 @@ 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; do not restore its local assembly recipes.
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.
+7 -1
View File
@@ -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`.
+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
+5 -2
View File
@@ -336,7 +336,7 @@ function toYaml(value, depth = 0) {
}
/** Build same-machine client instructions; a bind-all address is not a client destination. */
function sampleRequest(catalog, config) {
export function sampleRequest(catalog, config) {
const server = { ...catalog.runtime.server_defaults, ...config.server };
let host = server.host;
if (host === "0.0.0.0") host = "127.0.0.1";
@@ -351,7 +351,10 @@ function sampleRequest(catalog, config) {
}
const components = config.generator?.pipeline?.components;
const model = components?.lora_path ? components.lora_nickname : server.served_model_name || catalog.model.id;
const body = { ...(model === undefined ? {} : { model }), prompt: "A river flowing through a peaceful forest" };
const body = {
...(model === undefined ? {} : { model }),
prompt: config.default_request?.prompt ?? "A river flowing through a peaceful forest",
};
if (catalog.workload === "i2v") body.input_reference = "/absolute/path/to/first-frame.png";
const argv = [
"curl",
+9
View File
@@ -103,6 +103,7 @@ test("browser bundle exposes reusable methods and its error class with no DOM or
"loadDeployment",
"loadIndex",
"resolveConfig",
"sampleRequest",
]);
const api = context.FastVideoConfigCookbook;
assert.equal(api.resolveConfig(catalog(), { "server.port": 9000 }).config.server.port, 9000);
@@ -485,6 +486,14 @@ test("LoRA sample requests use the startup adapter instead of an edited server a
assert.equal(resolveConfig(data, edits).clientRequest.model, "different-server-name");
});
test("sample requests honor an explicitly configured prompt", () => {
const data = catalog();
data.controls.push({ path: "default_request.prompt", schema: { type: "string" } });
const result = resolveConfig(data, { "default_request.prompt": "A potter finishes a blue bowl." });
assert.equal(result.clientRequest.prompt, "A potter finishes a blue bowl.");
assert.match(result.clientCommand, /A potter finishes a blue bowl/);
});
test("environment variables are present and shell quoted without evaluation", () => {
const data = catalog();
data.env = { BACKEND: "it's literal $(touch /never-run)" };
@@ -0,0 +1,55 @@
# FastH3 Trim NVFP4. Generator and sampling defaults follow the released
# one-Spark recipe: examples/inference/basic/basic_fasth3_spark_pruned_nvfp4.yaml.
# Install with the DGX Spark guide. This serving adaptation is not GPU-validated.
generator:
model_path: FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4
engine:
num_gpus: 1
use_fsdp_inference: false
quantization:
transformer_quant: NVFP4
layer_profile: h3_dit_vsa
parallelism:
tp_size: 1
sp_size: 1
offload:
dit: false
dit_layerwise: false
text_encoder: false
image_encoder: false
vae: false
pin_cpu_memory: false
lazy_module_load: false
compile:
enabled: false
vae_enabled: false
pipeline:
workload_type: t2v
vae_tiling: true
experimental:
attention_backend: VIDEO_SPARSE_ATTN_H3
VSA_sparsity: 0.8
VSA_tile_size: 64
h3_sequential_load: false
inference_torch_compile: false
vae_parallel_decode: false
video_decode_backend: h3-vae
server:
host: 127.0.0.1
port: 8000
output_dir: outputs/fasth3_trim_server
served_model_name: fasth3-trim
default_request:
prompt: A quiet pottery studio with a potter finishing a bowl at the wheel.
negative_prompt: ''
sampling:
seed: 1234
height: 480
width: 832
num_frames: 124
fps: 24
num_inference_steps: 9
guidance_scale: 1.0
batch_cfg: false
output:
return_frames: false