# SPDX-License-Identifier: Apache-2.0 # adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/docs/source/generate_examples.py import itertools import json import os import re from dataclasses import dataclass, field from pathlib import Path import yaml ROOT_DIR = Path(__file__).parent.parent.resolve() ROOT_DIR_RELATIVE = '../..' EXAMPLE_DIR = ROOT_DIR / "examples" EXAMPLE_DOC_DIR = ROOT_DIR / "docs/getting_started/examples" GITHUB_REPO = "hao-ai-lab/FastVideo" # Update this to your repo GENERATED_DOC_PREFIXES = ( "examples/", "getting_started/examples/", "inference/examples/", "training/examples/", "distillation/examples/", ) COOKBOOK_DATA = ROOT_DIR / "docs/assets/cookbook-recipes.json" COOKBOOK_SERVING_DATA = ROOT_DIR / "docs/assets/cookbook-serving.json" COOKBOOK_CLIENTS = { "python": ("video.py", "python -m pip install openai==3.6.0"), "javascript": ("video.mjs", "npm install openai@7.8.0"), "curl": ("video.sh", "# Requires curl and jq"), } COOKBOOK_SOURCE_ROOTS = ( ROOT_DIR / "examples/inference", ROOT_DIR / "scripts/inference", ) # Cookbook families mirror the `model_family` values declared in # fastvideo/registry.py, plus "flux" for models that register no family # (e.g. black-forest-labs/FLUX.1-dev) and are grouped for documentation only. COOKBOOK_FAMILIES = { "wan", "turbodiffusion", "ltx2", "hunyuan", "cosmos", "kandinsky5", "flux", "glm_image", "zimage", "sd35", "minimax_h3", "longcat", "stable_audio", "mmaudio", "matrixgame", } # Lifecycle stages a recipe can belong to. Only "inference" has recipes today; # the rest exist so the schema (and UI) can grow without another migration. COOKBOOK_STAGES = { "inference", "distillation", "fine-tuning", "training", "lora-training", "evaluation", "optimization", "deployment", } # Explicit evidence states; never conflate these in recipe entries. COOKBOOK_EVIDENCE_STATES = { "Verified", "Source-backed", "Estimated", "Community-reported", "Unknown", "Unsupported", } COOKBOOK_HARDWARE_EVIDENCE = {"validated", "source-configured", "estimated", "unknown"} COOKBOOK_HARDWARE_PLATFORMS = {"cuda", "mlx", "mps"} COOKBOOK_HARDWARE_DEVICES = {"spark"} COOKBOOK_GPU_TYPES = {"NVIDIA", "Apple Silicon"} COOKBOOK_HARDWARE_TEXT_FIELDS = { "accelerator", "system_memory", "minimum_memory", "peak_memory", "evidence_url", } def validate_cookbook() -> None: """Keep cookbook entries tied to checked-in runnable sources.""" data = json.loads(COOKBOOK_DATA.read_text(encoding="utf-8")) version = data.get("version") if not isinstance(version, int): raise ValueError(f"{COOKBOOK_DATA}: version must be an integer") recipes = data.get("recipes") if not isinstance(recipes, list) or not recipes: raise ValueError(f"{COOKBOOK_DATA}: recipes must be a non-empty list") seen: set[str] = set() for recipe in recipes: required = ("id", "family", "task", "label", "model", "source", "command") missing = {key for key in required if not recipe.get(key)} if missing: raise ValueError(f"Cookbook recipe is missing: {', '.join(sorted(missing))}") if recipe["id"] in seen: raise ValueError(f"Duplicate cookbook recipe id: {recipe['id']}") seen.add(recipe["id"]) if recipe["family"] not in COOKBOOK_FAMILIES: raise ValueError(f"Cookbook recipe has an unknown family: {recipe['id']}: {recipe['family']}") stage = recipe.get("stage", "inference") if stage not in COOKBOOK_STAGES: raise ValueError(f"Cookbook recipe has an unknown stage: {recipe['id']}: {stage}") evidence = recipe.get("evidence", "Source-backed") if evidence not in COOKBOOK_EVIDENCE_STATES: raise ValueError(f"Cookbook recipe has an unknown evidence state: {recipe['id']}: {evidence}") hardware = recipe.get("hardware") if not isinstance(hardware, dict): raise ValueError(f"Cookbook recipe is missing hardware info: {recipe['id']}") platform = hardware.get("platform", "cuda") if platform not in COOKBOOK_HARDWARE_PLATFORMS: raise ValueError(f"Cookbook recipe has an unknown hardware platform: {recipe['id']}: {platform}") gpu_count = hardware.get("gpu_count") if platform == "cuda" and (not isinstance(gpu_count, int) or gpu_count < 1): raise ValueError(f"CUDA cookbook recipe needs an integer gpu_count >= 1: {recipe['id']}") if platform != "cuda" and gpu_count is not None: raise ValueError(f"Non-CUDA cookbook recipe must not use gpu_count: {recipe['id']}") device = hardware.get("device") if device is not None: if device not in COOKBOOK_HARDWARE_DEVICES: raise ValueError(f"Cookbook recipe has an unknown hardware device: {recipe['id']}: {device}") if platform != "cuda": raise ValueError(f"Cookbook hardware device requires platform cuda: {recipe['id']}: {device}") hardware_evidence = hardware.get("evidence") if hardware_evidence not in COOKBOOK_HARDWARE_EVIDENCE: raise ValueError(f"Cookbook recipe has unknown hardware evidence: {recipe['id']}: {hardware_evidence}\n" f"Expected one of {sorted(COOKBOOK_HARDWARE_EVIDENCE)}. Never guess GPU compatibility.") for field_name in COOKBOOK_HARDWARE_TEXT_FIELDS: value = hardware.get(field_name) if value is not None and not (isinstance(value, str) and value.strip()): raise ValueError(f"Cookbook hardware field must be a non-empty string: {recipe['id']}: {field_name}") if hardware_evidence == "validated" and not hardware.get("accelerator"): raise ValueError(f"Validated cookbook hardware needs an exact accelerator: {recipe['id']}") if hardware_evidence == "source-configured": recorded_fields = {"accelerator", "system_memory", "peak_memory"}.intersection(hardware) if recorded_fields: raise ValueError(f"Source-configured hardware cannot claim recorded run details: {recipe['id']}: " f"{', '.join(sorted(recorded_fields))}") evidence_url = hardware.get("evidence_url") if evidence_url is not None and not evidence_url.startswith("https://github.com/hao-ai-lab/FastVideo/"): raise ValueError(f"Cookbook hardware evidence must link to the FastVideo repository: {recipe['id']}") gpu_types = recipe.get("gpu_types", []) if not isinstance(gpu_types, list) or any(gpu not in COOKBOOK_GPU_TYPES for gpu in gpu_types): raise ValueError(f"Cookbook recipe has an unknown gpu_types entry: {recipe['id']}: {gpu_types}") revision = recipe.get("revision") if revision is not None and not (isinstance(revision, str) and revision.strip()): raise ValueError(f"Cookbook recipe revision must be a non-empty string when present: {recipe['id']}") related = recipe.get("related", []) if not isinstance(related, list): raise ValueError(f"Cookbook recipe related must be a list of recipe ids: {recipe['id']}") modes = recipe.get("modes") if modes is not None and (not isinstance(modes, list) or not modes or any(not isinstance(item, str) or not item.strip() for item in modes)): raise ValueError(f"Cookbook recipe modes must be a non-empty list of strings: {recipe['id']}") knobs = recipe.get("knobs", []) if not isinstance(knobs, list): raise ValueError(f"Cookbook recipe knobs must be a list: {recipe['id']}") knob_keys: set[str] = set() for knob in knobs: if not isinstance(knob, dict): raise ValueError(f"Cookbook recipe knob must be an object: {recipe['id']}") required_knob = ("key", "label", "flag", "options", "default") missing_knob = {key for key in required_knob if key not in knob} if missing_knob: raise ValueError(f"Cookbook recipe knob is missing: {recipe['id']}: {', '.join(sorted(missing_knob))}") if knob["key"] in knob_keys: raise ValueError(f"Duplicate cookbook recipe knob key: {recipe['id']}: {knob['key']}") knob_keys.add(knob["key"]) if not isinstance(knob["flag"], str) or not knob["flag"].startswith("--"): raise ValueError(f"Cookbook recipe knob flag must start with --: {recipe['id']}: {knob['key']}") options = knob["options"] if not isinstance(options, list) or not options: raise ValueError( f"Cookbook recipe knob options must be a non-empty list: {recipe['id']}: {knob['key']}") option_values = [option["value"] if isinstance(option, dict) else option for option in options] if knob["default"] not in option_values: raise ValueError( f"Cookbook recipe knob default must be one of its options: {recipe['id']}: {knob['key']}") source = (ROOT_DIR / recipe["source"]).resolve() if not any(source.is_relative_to(root.resolve()) for root in COOKBOOK_SOURCE_ROOTS): raise ValueError(f"Cookbook source is outside an approved directory: {recipe['source']}") if not source.is_file(): raise ValueError(f"Cookbook source does not exist: {recipe['source']}") source_text = source.read_text(encoding="utf-8") if knobs: # A script that shares its argument parser with a sibling module # (e.g. `from . import basic_fasth3`) inherits that module's # flags without the flag text appearing in its own source. knob_source_text = source_text for match in re.finditer(r'(?:from\s+\.\s+import|^\s*import)\s+(\w+)', source_text, flags=re.MULTILINE): sibling = source.parent / f"{match.group(1)}.py" if sibling.is_file() and sibling != source: knob_source_text += "\n" + sibling.read_text(encoding="utf-8") for knob in knobs: if knob["flag"] not in knob_source_text: raise ValueError(f"Cookbook recipe knob flag is not in its source: {recipe['id']}: {knob['flag']}") # The model must be traceable to the checked-in source itself, or be # passed explicitly on the command line (e.g. --model-path or # MODEL_PATH=) when the source reads it from arguments/env. if recipe["model"] not in source_text and recipe["model"] not in recipe["command"]: raise ValueError(f"Cookbook model is not present in {recipe['source']} or its command: {recipe['id']}") if recipe["source"] not in recipe["command"]: raise ValueError(f"Cookbook command does not invoke its source: {recipe['id']}") if "serving" in recipe: cookbook_serving_profile(recipe) # Second pass so `related` may point forward at recipes defined later. ids = {recipe["id"] for recipe in recipes} for recipe in recipes: for related_id in recipe.get("related", []): if related_id not in ids: raise ValueError(f"Cookbook recipe references unknown related id: {recipe['id']}: {related_id}") cache_bust = f"cookbook-recipes.json?v={version}" cookbook_pages = ROOT_DIR / "docs/cookbook" for page in sorted(cookbook_pages.glob("*.md")): text = page.read_text(encoding="utf-8") if "cookbook-recipes.json" not in text: continue if cache_bust not in text: raise ValueError(f"{page}: recipe JSON cache-bust must be {cache_bust}") def cookbook_serving_profile(recipe: dict) -> dict: """Read server facts separately from the local inference run's evidence.""" serving = recipe["serving"] if not isinstance(serving, dict) or not serving.get("source") or not serving.get("install"): raise ValueError(f"Serving recipe needs source and install fields: {recipe['id']}") source = (ROOT_DIR / serving["source"]).resolve() if not source.is_relative_to(ROOT_DIR / "examples/serving") or not source.is_file(): raise ValueError(f"Serving config must exist under examples/serving: {recipe['id']}") config = yaml.safe_load(source.read_text(encoding="utf-8")) runtime = config.get("runtime", "cuda") if runtime not in {"cuda", "mlx"} or runtime != recipe["hardware"].get("platform", "cuda"): raise ValueError(f"Serving runtime does not match recipe: {recipe['id']}") generator = config["generator"] if generator["model_path"] != recipe["model"]: raise ValueError(f"Serving checkpoint does not match recipe: {recipe['id']}") if config.get("streaming") is not None: raise ValueError(f"Cookbook OpenAI serving requires a REST config: {recipe['id']}") server = config["server"] model = server["served_model_name"] if not isinstance(model, str) or not re.fullmatch(r"[A-Za-z0-9._/-]+", model): raise ValueError(f"Serving model alias must be safe for client examples: {recipe['id']}") port = server["port"] if type(port) is not int or not 1 <= port <= 65535: raise ValueError(f"Serving config needs a valid port: {recipe['id']}") hardware = {"platform": runtime, "evidence": "source-configured"} device = recipe["hardware"].get("device") if device: hardware["device"] = device if runtime == "cuda": count = generator["engine"]["num_gpus"] if type(count) is not int or count < 1: raise ValueError(f"Serving config needs a valid GPU count: {recipe['id']}") hardware["gpu_count"] = count env = serving.get("env") if env is not None: if not isinstance(env, str) or not env.strip() or "\n" in env: raise ValueError(f"Serving env must be a single-line command prefix: {recipe['id']}") prefix = env.strip() + " " else: prefix = "" command = f"{prefix}fastvideo serve --config {serving['source']} --server.host 127.0.0.1" else: if server.get("host") != "127.0.0.1": raise ValueError(f"MLX cookbook server must bind to loopback: {recipe['id']}") command = f"python -m fastvideo.entrypoints.openai.mlx_server --config {serving['source']}" base_url = f"http://127.0.0.1:{port}/v1" clients = {} for language, (filename, install) in COOKBOOK_CLIENTS.items(): path = ROOT_DIR / "examples/serving/clients" / filename text = path.read_text(encoding="utf-8") # Keep displayed snippets identical to executable sources for the # checked-in H3 profile, with only endpoint and alias substitutions. text = text.replace("http://127.0.0.1:8000/v1", base_url) text = text.replace('"fasth3"', json.dumps(model)).replace("${FASTVIDEO_MODEL:-fasth3}", "${FASTVIDEO_MODEL:-" + model + "}") clients[language] = {"source": path.relative_to(ROOT_DIR).as_posix(), "code": text, "install": install} return { "source": serving["source"], "install": serving["install"], "command": command, "runtime": runtime, "prepare": serving.get("prepare", ""), "model": model, "base_url": base_url, "playground_url": f"http://127.0.0.1:{port}/playground/", "health_command": f"curl --fail-with-body http://127.0.0.1:{port}/health", "hardware": hardware, "sampling": config["default_request"]["sampling"], "clients": clients, } def generate_cookbook_serving() -> None: """Publish executable clients and config facts through the docs build.""" data = json.loads(COOKBOOK_DATA.read_text(encoding="utf-8")) profiles = {recipe["id"]: cookbook_serving_profile(recipe) for recipe in data["recipes"] if "serving" in recipe} COOKBOOK_SERVING_DATA.write_text(json.dumps(profiles, indent=2) + "\n", encoding="utf-8") def fix_case(text: str) -> str: subs = { "api": "API", "cli": "CLI", "cpu": "CPU", "llm": "LLM", "tpu": "TPU", "aqlm": "AQLM", "gguf": "GGUF", "lora": "LoRA", "rlhf": "RLHF", "vllm": "vLLM", "openai": "OpenAI", "multilora": "MultiLoRA", "mlpspeculator": "MLPSpeculator", "finetune": "Finetune", "distillation": "Distillation", "wan": "Wan", "i2v": "I2V", "t2v": "T2V", "1.3b": "1.3B", "14b": "14B", "480p": "480P", "720p": "720P", r"fp\d+": lambda x: x.group(0).upper(), # e.g. fp16, fp32 r"int\d+": lambda x: x.group(0).upper(), # e.g. int8, int16 } for pattern, repl in subs.items(): text = re.sub(rf'\b{pattern}\b', repl, text, flags=re.IGNORECASE) # type: ignore[call-overload] return text @dataclass class Index: """ Index class to generate a structured document index. Attributes: path (Path): The path save the index file to. title (str): The title of the index. description (str): A brief description of the index. caption (str): An optional caption for the table of contents. maxdepth (int): The maximum depth of the table of contents. Defaults to 1. documents (list[str]): A list of document paths to include in the index. Defaults to an empty list. Methods: generate() -> str: Generates the index content as a string in the specified format. """ # noqa: E501 path: Path title: str description: str caption: str maxdepth: int = 1 documents: list[str] = field(default_factory=list) def generate(self) -> str: content = f"# {self.title}\n\n{self.description}\n\n" if self.caption: content += f"## {self.caption}\n\n" # Generate a simple list of links for MkDocs for doc in self.documents: # Convert document path to proper link doc_link = doc.replace("\\", "/") # Get just the filename for the link text doc_title = fix_case(Path(doc).stem.replace("_", " ").title()) content += f"- [{doc_title}]({doc_link}.md)\n" content += "\n" return content @dataclass class Example: """ Example class for generating documentation content from a given path. Attributes: path (Path): The path to the main directory or file. category (str): The category of the document. main_file (Path): The main file in the directory. other_files (list[Path]): list of other files in the directory. title (str): The title of the document. Methods: __post_init__(): Initializes the main_file, other_files, and title attributes. determine_main_file() -> Path: Determines the main file in the given path. determine_other_files() -> list[Path]: Determines other files in the directory excluding the main file. determine_title() -> str: Determines the title of the document. generate() -> str: Generates the documentation content. """ # noqa: E501 path: Path category: str | None = None main_file: Path = field(init=False) other_files: list[Path] = field(init=False) title: str = field(init=False) def __post_init__(self): self.main_file = self.determine_main_file() self.other_files = self.determine_other_files() self.title = self.determine_title() def determine_main_file(self) -> Path: """ Determines the main file in the given path. If the path is a file, it returns the path itself. Otherwise, it searches for Markdown files (*.md) in the directory and returns the first one found. Returns: Path: The main file path, either the original path if it's a file or the first Markdown file found in the directory. Raises: IndexError: If no Markdown files are found in the directory. """ # noqa: E501 if self.path.is_file(): return self.path markdown_files = sorted(self.path.glob("*.md")) if not markdown_files: raise IndexError(f"No Markdown files found in {self.path}") readme_files = [f for f in markdown_files if f.name.lower() == "readme.md"] if readme_files: return readme_files[0] return markdown_files[0] def determine_other_files(self) -> list[Path]: """ Determine other files in the directory excluding the main file. This method checks if the given path is a file. If it is, it returns an empty list. Otherwise, it recursively searches through the directory and returns a list of all files that are not the main file. Returns: list[Path]: A list of Path objects representing the other files in the directory. """ # noqa: E501 if self.path.is_file(): return [] other_files = [] for directory, subdirectories, filenames in os.walk(self.path): # Local client dependencies and caches are not example sources. subdirectories[:] = [ name for name in subdirectories if not name.startswith(".") and name not in {"node_modules", "__pycache__"} ] for name in filenames: file = Path(directory) / name if not name.startswith(".") and file != self.main_file: other_files.append(file) return other_files def determine_title(self) -> str: return fix_case(self.path.stem.replace("_", " ").title()) def generate(self) -> str: # Create GitHub link to source github_path = str(self.path.relative_to(ROOT_DIR)).replace("\\", "/") github_url = f"https://github.com/{GITHUB_REPO}/blob/main/{github_path}" content = f"**Source:** [{github_path}]({github_url})\n\n" # Add title for code files if self.main_file.suffix != ".md": content += f"# {self.title}\n\n" # Include main file content if self.main_file.suffix == ".md": # For markdown files, include the content directly with open(self.main_file, encoding='utf-8') as f: content += f.read() + "\n\n" else: # For code files, use code blocks language = self.main_file.suffix[1:] if self.main_file.suffix else "" with open(self.main_file, encoding='utf-8') as f: file_content = f.read() content += f"```{language}\n{file_content}\n```\n\n" if not self.other_files: return content content += "## Additional Files\n\n" # Define binary/non-text file extensions to skip binary_extensions = { '.mp4', '.avi', '.mov', '.mkv', '.gif', '.jpg', '.jpeg', '.png', '.webp', '.bmp', '.pdf', '.zip', '.tar', '.gz', '.mp3', '.wav' } for file in sorted(self.other_files): # Skip binary files if file.suffix.lower() in binary_extensions: continue file_rel_path = file.relative_to(self.path) # Use collapsible admonition syntax for MkDocs content += f"??? note \"{file_rel_path}\"\n\n" try: if file.suffix == ".md": # Include markdown content with indentation with open(file, encoding='utf-8') as f: for line in f: content += f" {line}" else: # Include code with proper formatting language = file.suffix[1:] if file.suffix else "" with open(file, encoding='utf-8') as f: file_content = f.read() # Indent the code block for the admonition content += f" ```{language}\n" for line in file_content.split('\n'): content += f" {line}\n" content += " ```\n" content += "\n" except UnicodeDecodeError: # Skip files that can't be decoded as UTF-8 continue return content @dataclass class NestedStructure: """Helper class to manage nested documentation structures for training/distillation.""" category: str method: str model: str dataset: str example: Example @property def filename(self) -> str: return f"{self.model}_{self.dataset}" @property def title(self) -> str: return fix_case(self.dataset.replace('_', ' ')) @property def description(self) -> str: category_name = self.category.title() return f"{category_name} example using the {self.dataset} dataset with the {self.model} model." def create_category_indices() -> dict[str, Index]: """Create category indices with their respective configurations.""" main_index_dir = ROOT_DIR / "docs/examples" if not main_index_dir.exists(): main_index_dir.mkdir(parents=True) category_indices = { "inference": Index( path=ROOT_DIR / "docs/inference/examples/examples_inference_index.md", title="🚀 Examples", description= "Inference examples demonstrate how to use FastVideo inference. We recommend starting with [basic.md](basic.md).", caption="Examples", maxdepth=1, ), "training": Index( path=ROOT_DIR / "docs/training/examples/examples_training_index.md", title="🚀 Examples", description="Training examples demonstrate how to use FastVideo training.", caption="Examples", maxdepth=3, ), "distillation": Index( path=ROOT_DIR / "docs/distillation/examples/examples_distillation_index.md", title="🚀 Examples", description="Distillation examples demonstrate how to use FastVideo distillation.", caption="Examples", maxdepth=3, ), } # Ensure all category doc directories exist for index in category_indices.values(): if not index.path.parent.exists(): index.path.parent.mkdir(parents=True) return category_indices def find_examples(category_indices: dict[str, Index], generate_main_index: bool) -> list[Example]: """Find all examples from the examples directory.""" examples = [] glob_patterns = ["*.py", "*.md", "*.sh"] # Map category names to actual directory names category_dir_mapping = { "distillation": "distill", # examples/distill/ -> distillation category } # Find categorised examples for category in category_indices: # Use mapped directory name if available, otherwise use category name dir_name = category_dir_mapping.get(category, category) category_dir = EXAMPLE_DIR / dir_name # Skip if directory doesn't exist if not category_dir.exists(): continue globs = [category_dir.glob(pattern) for pattern in glob_patterns] for path in itertools.chain(*globs): examples.append(Example(path, category)) # Find examples in subdirectories (recursively) for path in category_dir.glob("**/*.md"): examples.append(Example(path.parent, category)) # Find uncategorised examples only if we're generating a main index if generate_main_index: globs = [EXAMPLE_DIR.glob(pattern) for pattern in glob_patterns] for path in itertools.chain(*globs): examples.append(Example(path)) # Find examples in subdirectories for path in EXAMPLE_DIR.glob("*/*.md"): # Skip categorised examples if path.parent.name in category_indices: continue examples.append(Example(path.parent)) return examples def create_nested_structures(examples: list[Example]) -> dict[str, dict[str, dict[str, dict[str, NestedStructure]]]]: """Create nested structures for training and distillation categories.""" nested_structures: dict[str, dict[str, dict[str, dict[str, NestedStructure]]]] = {} # Map category names to actual directory names category_dir_mapping = { "distillation": "distill", } for example in examples: if example.category not in ["training", "distillation"]: continue # Use mapped directory name if available dir_name = category_dir_mapping.get(example.category, example.category) category_dir = EXAMPLE_DIR / dir_name relative_path = example.path.relative_to(category_dir) path_parts = relative_path.parts if example.category == "training": # For training examples like finetune/wan_i2v_14b_480p/crush_smol if len(path_parts) >= 3: method = path_parts[0] # e.g., "finetune" model = path_parts[1] # e.g., "wan_i2v_14b_480p" dataset = path_parts[2] # e.g., "crush_smol" # Initialize nested structure if example.category not in nested_structures: nested_structures[example.category] = {} if method not in nested_structures[example.category]: nested_structures[example.category][method] = {} if model not in nested_structures[example.category][method]: nested_structures[example.category][method][model] = {} # Store the nested structure nested_structures[example.category][method][model][dataset] = NestedStructure(category=example.category, method=method, model=model, dataset=dataset, example=example) elif example.category == "distillation" and len(path_parts) >= 2: # For distillation examples like Wan2.1-T2V/Wan-Syn-Data-480P model = path_parts[0] # e.g., "Wan2.1-T2V" dataset = path_parts[1] # e.g., "Wan-Syn-Data-480P" method = "DMD" # Default method for distillation # Initialize nested structure if example.category not in nested_structures: nested_structures[example.category] = {} if method not in nested_structures[example.category]: nested_structures[example.category][method] = {} if model not in nested_structures[example.category][method]: nested_structures[example.category][method][model] = {} # Store the nested structure nested_structures[example.category][method][model][dataset] = NestedStructure(category=example.category, method=method, model=model, dataset=dataset, example=example) return nested_structures def generate_flat_examples(examples: list[Example], category_indices: dict[str, Index], examples_index: Index | None, generate_main_index: bool) -> None: """Generate documentation for flat structure examples (inference, etc.).""" for example in examples: if example.category in ["training", "distillation"]: continue # Skip nested structure examples # Determine which index to use for this example if example.category is not None and example.category in category_indices: index = category_indices[example.category] elif generate_main_index: assert examples_index is not None index = examples_index else: continue # Generate the example documentation doc_path = index.path.parent / f"{example.path.stem}.md" with open(doc_path, "w+", encoding="utf-8") as f: f.write(example.generate()) index.documents.append(example.path.stem) def generate_nested_examples(nested_structures: dict[str, dict[str, dict[str, dict[str, NestedStructure]]]], category_indices: dict[str, Index]) -> None: """Generate documentation for nested structure examples (training, distillation).""" for category_name in ["training", "distillation"]: if category_name not in category_indices or category_name not in nested_structures: continue category_index = category_indices[category_name] category_base_dir = category_index.path.parent for method, models in nested_structures[category_name].items(): # Create method-level index method_index = Index(path=category_base_dir / f"{method}.md", title=fix_case(method), description=f"Examples using {method}.", caption=f"{fix_case(method)} Examples", maxdepth=2) for model, datasets in models.items(): # Generate dataset examples using the Example class for dataset, nested_struct in datasets.items(): doc_path = category_base_dir / f"{nested_struct.filename}.md" with open(doc_path, "w+", encoding="utf-8") as f: f.write(nested_struct.example.generate()) # Create model-level index model_index = Index(path=category_base_dir / f"{model}.md", title=fix_case(model.replace('_', ' ')), description=f"Examples for the {model} model.", caption=f"{fix_case(model.replace('_', ' '))} Datasets", maxdepth=1) # Add dataset indices to model index for dataset, nested_struct in datasets.items(): model_index.documents.append(nested_struct.filename) # Write model index with open(model_index.path, "w+", encoding="utf-8") as f: f.write(model_index.generate()) # Add model to method index method_index.documents.append(model) # Write method index with open(method_index.path, "w+", encoding="utf-8") as f: f.write(method_index.generate()) # Add method to main category index category_index.documents.append(method) def generate_examples(generate_main_index: bool = False) -> None: """ Generate example documentation. Args: generate_main_index (bool): Whether to generate the main examples index. If False, only category-specific indices will be generated. """ # Create category indices category_indices = create_category_indices() # Create the main examples index only if requested examples_index = None if generate_main_index: main_index_dir = ROOT_DIR / "docs/examples" examples_index = Index( path=main_index_dir / "examples_index.md", title="💡 Examples", description="A collection of examples demonstrating usage of FastVideo.\n\n" f"All documented examples are autogenerated using [generate_examples.py](https://github.com/{GITHUB_REPO}/blob/main/docs/generate_examples.py) " f"from examples found in the [examples](https://github.com/{GITHUB_REPO}/tree/main/examples) directory.", caption="Examples", maxdepth=2) # Find all examples examples = find_examples(category_indices, generate_main_index) # Create nested structures for training and distillation nested_structures = create_nested_structures(examples) # Generate flat structure examples (inference, etc.) generate_flat_examples(examples, category_indices, examples_index, generate_main_index) # Generate nested structure examples (training, distillation) generate_nested_examples(nested_structures, category_indices) # Generate the index files for categories for category_index in category_indices.values(): if category_index.documents: # Add to main index if it exists if generate_main_index and examples_index: main_index_dir = examples_index.path.parent rel_path = os.path.relpath(category_index.path, start=main_index_dir) examples_index.documents.insert(0, str(rel_path).replace("\\", "/").replace(".md", "")) # Write the category index file with open(category_index.path, "w+", encoding="utf-8") as f: f.write(category_index.generate()) # Write the main index file if requested if generate_main_index and examples_index: with open(examples_index.path, "w+", encoding="utf-8") as f: f.write(examples_index.generate()) def on_pre_build(config, **kwargs): """ MkDocs hook to generate examples before building the documentation. This function is called automatically by MkDocs' native hook system. """ validate_cookbook() generate_cookbook_serving() print("Generating example documentation...") generate_examples(generate_main_index=True) print("Example documentation generated successfully!") def on_page_context(context, page, **kwargs): """Hide repository edit actions for pages that only exist at build time.""" if page.file.src_uri.startswith(GENERATED_DOC_PREFIXES): page.edit_url = None return context if __name__ == "__main__": validate_cookbook() generate_cookbook_serving() print("Generating example documentation...") generate_examples(generate_main_index=True) print("Example documentation generated successfully!")