Skip to content

Repository files navigation

docgen — documentation generator

Reusable Python library and CLI for narrated demo videos built around Manim, TTS (OpenAI or xAI Grok), and ffmpeg composition. Aimed at long-form, scripted explainers that walk through how a system works.

Suite handbook (Courseforge)

Prose + PlantUML sources for how Courseforge repositories fit together live under docs/suite/. Regenerate PNGs with ./scripts/render-suite-diagrams.shJava, Graphviz (dot), and the vendored JAR in third_party/plantuml/ (CI installs Graphviz and runs the same script; see .github/workflows/render-suite-diagrams.yml). The rendered site at courseforge.github.io pulls this tree on each publish from courseforge/infrastructure.

What changed: Playwright is gone

docgen no longer ships any Playwright-driven UI demo path. The previous demo-function, playwright, discover-tests, vhs, tape-lint, sync-vhs, per-function-*, and catalog commands — together with their config blocks (vhs:, playwright:, playwright_test:, discover_tests:, catalog:, per_function:) and the playwright, playwright_test, and vhs visual_map types — have been removed.

Why: a UI-test-driven recorder turned out to be a fragile, project-specific concern that pulled pytest-playwright, Node Playwright, VHS / ttyd, browser binaries, trace parsing, and a discovery catalog into a generic library. The same goal is now being prototyped in a consumer project (CourseForge tools/courseforge/demogen/) with the “LLM emits a validated automation spec, a deterministic runner translates it to Playwright” pattern. Once that contract stabilises a small portion may be backported into docgen, but docgen itself stays Playwright-free.

If you still need the legacy behaviour, pin a pre-removal commit (pip install docgen @ git+https://github.com/jmjava/documentation-generator.git@<sha>).

What docgen does today

  • TTS narration — generate MP3 audio from Markdown scripts via OpenAI gpt-4o-mini-tts, or xAI /v1/tts when ai.provider is grok.
  • Word-level timestamps without Whisper — the default local engine aligns the known narration text against the TTS mp3 offline (ffmpeg silencedetect
    • proportional interpolation); no API call or transcription. Network transcription (timestamps.engine: whisper) uses OpenAI whisper-1 or xAI /v1/stt when the provider is Grok. Both engines write the same timing.json shape.
  • Manim animations (default: declarative scene specs) — primary visual surface. Prefer animations/specs/*.scene.yaml via docgen scene-spec-generate
    • scene-compile. On generate-all, if no specs exist yet, the pipeline auto-runs scene-spec-generate; later runs retime-compile specs against fresh timing.json (no OpenAI) unless you pass --regen-scene-specs. Specs auto-paginate when they exceed the frame stack budget, reject unsafe width / subject-beat gaps, and map each paced label to a wait_word index. Hand-maintained custom Manim classes may still live in animations/scenes.py outside BEGIN/END GENERATED SCENE markers (use --skip-scene-retime to bypass the declarative stage).
  • Image assets in Manim scenes — a scene-spec box may be an image element (image: images/<name>.png + prompt:); docgen image-generate renders the prompt via OpenAI Images (default gpt-image-1) or xAI Imagine (grok-imagine-image-2.0 when ai.provider is grok). The compiled scene shows it with the _image helper. generate-all fills in missing assets automatically.
  • ffmpeg composition — combine narration audio and Manim video into final segments, with a freeze-tail guard.
  • Validation — A/V drift, freeze ratio, OCR error scan, layout, narration lint, Manim scene lint, timing_sync (stale timing.json vs regenerated mp3 — hard fail), story_end (paced visual story finishes long before narration — hard fail), and av_sync (OCR check that scene-spec label anchors appear on screen near their spoken time — soft warning).
  • GitHub Pages — auto-generate index.html, deploy workflow, LFS rules, .gitignore.
  • Wizard — local web GUI to bootstrap narration scripts from existing project docs.

No IDE lock-in: maintenance workflows are docgen CLI + YAML + shell/CI (and an LLM/TTS provider where a command calls the API). The wizard is a local Flask app, not a plugin tied to one editor.

Run against another repo (do not vendor)

Install docgen once (venv / pipx / this Cloud environment). Pass the consumer checkout or clone URL; nothing from this library is copied into that project's src/.

# Local checkout of the product repo
docgen --repo /path/to/course-builder init --defaults
docgen --repo /path/to/course-builder yaml-generate
docgen --repo /path/to/course-builder generate-all

# GitHub URL or org/repo shorthand (shallow clone into DOCGEN_REPO_CACHE)
docgen --repo github.com/acme/course-builder generate-all
# equivalent: DOCGEN_REPO=acme/course-builder docgen generate-all
# pasted GitHub page URLs work: https://github.com/acme/app/tree/main

--repo looks for docs/demos/docgen.yaml (then any other docgen.yaml under the checkout). repo_root in that yaml still points at the consumer so narration/scene prompts read their sources.

Where this runs (not Cursor-only)

The same docgen CLI runs in Cursor Cloud automation, local Cursor, and Claude Code / Copilot / a plain shell. It is not an editor plugin. docgen ai-status prints the resolved provider and which env var supplied the key (never the secret).

Host What to set
Cursor Cloud CURSOR_API_KEY is injected. Cloud OPENAI_API_KEY=crsr_… is skipped.
Local Cursor OPENAI_API_KEY in .env (or CURSOR_API_KEY if you have one).
Claude Code / Copilot / CI OPENAI_API_KEY in .env. Or only ANTHROPIC_API_KEY for chat (narration-generate, scene-spec-generate). TTS and images still need OpenAI or Grok.
Optional Grok ai.provider: grok and XAI_API_KEY.

Default provider is OpenAI Chat/Images. Auth order: CURSOR_API_KEY, then OPENAI_API_KEY. If neither is usable and ANTHROPIC_API_KEY is set, chat switches to Claude automatically.

Image models stay the same knob as OpenAI — they are not remapped when using the Cursor key:

image_generation:
  model: gpt-image-1      # or dall-e-3, gpt-image-1-mini, …
  size: 1536x1024
  # quality: high
docgen ai-status
docgen image-generate --all --model gpt-image-1
docgen image-generate --all --model dall-e-3 --size 1024x1024

To substitute Grok for chat, TTS, Whisper, and image calls:

ai:
  provider: grok          # openai | grok | anthropic
export DOCGEN_AI_PROVIDER=grok    # overrides yaml
export XAI_API_KEY=xai-...
docgen --repo /path/to/consumer generate-all

Existing OpenAI model names in YAML are remapped at call time (gpt-4ogrok-4.6, gpt-image-1grok-imagine-image-2.0, TTS voice coraleve). Chat and images use the OpenAI SDK at https://api.x.ai/v1. TTS and STT use xAI POST /v1/tts and POST /v1/stt. Keep timestamps.engine: local unless you specifically want network STT.

A Cursor Cloud environment that already has ffmpeg / tesseract / Manim build deps can run the full pipeline using the injected CURSOR_API_KEY. Local Cursor and Claude Code use .env (OPENAI_API_KEY, or ANTHROPIC_API_KEY for chat). Optional: XAI_API_KEY for Grok. Grant the consumer as a repository dependency if you clone by URL.

Install (external tool — do not vendor into project src/)

docgen is a pip-installable CLI/library. Consumer projects keep only a bundle (docs/demos/docgen.yaml, hints, narration, …). They should not copy or submodule this repository into application source.

# Project / CI venv
pip install 'docgen @ git+https://github.com/jmjava/documentation-generator.git'
# Prefer a SHA pin in CI:
# pip install 'docgen @ git+https://github.com/jmjava/documentation-generator.git@<sha>'

# Optional extras
pip install 'docgen[manim] @ git+https://github.com/jmjava/documentation-generator.git'
pip install 'docgen[gui] @ git+https://github.com/jmjava/documentation-generator.git'   # pywebview desktop window
# Freeze the Vue GUI only (not the full Manim CLI): pip install 'docgen[packaging]' && pyinstaller packaging/docgen-gui.spec

# Isolated global CLI (no project venv)
pipx install 'docgen @ git+https://github.com/jmjava/documentation-generator.git'
# or: uv tool install 'docgen @ git+https://github.com/jmjava/documentation-generator.git'

docgen --version

docgen init writes requirements-docgen.txt + a bundle README.md with the same guidance.

Development setup (this library)

git clone https://github.com/jmjava/documentation-generator.git
cd documentation-generator
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Optional: pip install -e ".[gui]"          # pywebview window for `docgen gui`
# Optional: pip install -e ".[packaging]"    # PyInstaller (see packaging/README.md)
pytest
docgen benchmark          # required: scene-timing corpus vs committed baseline

CI runs ruff, pytest, and a required benchmark job (docgen benchmark). CI also installs ffmpeg and tesseract via apt for unit tests — see .github/workflows/ci.yml.

Roadmap: milestones/README.md.

Quick start (in a consumer repo)

From inside the consumer bundle (library already on PATH):

cd your-project/docs/demos          # bundle only — library is on PATH via pip
pip install -r requirements-docgen.txt   # after docgen init, or use the pip line above
docgen wizard              # optional: bootstrap narration from project docs
docgen generate-all        # TTS → timestamps → scene retime → Manim → compose → validate
docgen validate --pre-push

From a machine / Cloud environment that has docgen installed but is not the consumer repo:

docgen --repo /path/to/your-project generate-all

CLI commands

Command Description
docgen --repo PATH_OR_URL … Target a consumer checkout or clone URL (also DOCGEN_REPO). Finds docs/demos/docgen.yaml. Does not copy this library into the consumer src/
docgen --version Show installed version + recommended pip install line (external tool)
docgen init [TARGET_DIR] [--defaults] [--segments-file FILE] Scaffold a bundle: docgen.yaml, requirements-docgen.txt, wrapper scripts, directories
docgen wizard [--port 8501] Local web GUI: focus files, revise narration, asset freshness / rebuild-from-here, Vue Benchmark view, and a Tool tab to upgrade the installed docgen package (pip) + rewrite requirements-docgen.txt
docgen gui [--view benchmark] [--browser] [--smoke] Desktop GUI (Vue + Flask). Install docgen[gui] for a pywebview window; --browser uses the system browser; --smoke is a headless HTTP check
docgen freeze [--dist DIR] [--smoke] PyInstaller onedir for docgen-gui only (pip install 'docgen[packaging]'). Not the full Manim CLI
docgen tts [--segment 01] [--dry-run] Generate TTS audio
docgen timestamps [--engine local|whisper] Extract word/segment timestamps from TTS audio → timing.json (default local: offline narration-text alignment; whisper: OpenAI transcription)
docgen image-generate [--segment 01 | --all | --spec PATH] [--force] [--dry-run] [--model …] [--size …] Generate scene-spec image assets (image: + prompt: boxes) via the OpenAI Images API into the bundle
docgen manim [--scene StackDAGScene] Render Manim animations
docgen compose [01 02 03] [--ffmpeg-timeout 900] Compose segments (audio + video)
docgen validate [--max-drift 2.75] [--pre-push] Run all validation checks
docgen lint [--segment 01] Narration lint only
docgen concat [--config full-demo] Concatenate full demo files
docgen pages [--force] Generate index.html, pages.yml, .gitattributes, .gitignore
docgen generate-all [--skip-tts] [--skip-manim] [--retry-manim] [--regen-scene-specs] [--skip-scene-retime] Full pipeline: TTS → timestamps → scene specs → Manim → compose → validate. Missing animations/specs/*.scene.yaml auto-generates; existing specs retime offline; --regen-scene-specs forces LLM rewrite
docgen rebuild-after-audio [--regen-scene-specs] Timestamps → scene retime → Manim → compose → validate (skips TTS)
docgen clean-bundle [-y] [--delete-config] [--keep-narration] Remove regenerable outputs under the bundle
docgen narration-generate --segment 01 [--extra-path REL] [--hint TEXT] [--dry-run] [--force] [--revise --revision-notes TEXT] Generate narration .md from repo sources + owner hints (OpenAI); --revise edits the existing script in place
docgen yaml-generate [--merge-defaults] [--llm] [--dry-run] [--list-gaps] Merge defaults into docgen.yaml; optional OpenAI refresh of tts.instructions / wizard.system_prompt (rewrites the file — review in Git)
docgen scene-compile [SPEC.scene.yaml | --all] [--retime] [--dry-run] Compile declarative scene YAML into animations/scenes.py. --all --retime re-derives wait_word from current timing.json with no OpenAI; unmatched labels fail closed (or set pace: none)
docgen scene-spec-generate [--segment 01 | --all] [--compile] [--print-only] [--output PATH] [--hint …] [--model …] Call OpenAI to emit YAML only (same schema as scene-compile); rejects frame-budget overflow and subject-beat coverage failures (hold board on same topic; cover topic shifts; no invented labels — not a blind count); auto-paginate + word-alignment; optionally writes animations/specs/<stem>.scene.yaml and --compiles into scenes.py
docgen benchmark [--case ID] [--format text|json] [--update-baseline] [--gui] Score the standard scene-timing corpus (execute compiled construct() on the real _TimedScene clock, no Manim). Diffs a committed baseline so clock changes are measured, not guessed. --gui opens the Vue view

Configuration

Create a docgen.yaml in your demos directory. Use docgen init to scaffold a fresh layout, then docgen yaml-generate to fill in defaults from the files already on disk. (docgen yaml-generate also keeps manim_scene_generation.segments in step with visual_map.)

The visual_map key is maintainer-owned per-segment wiring. Supported types are manim, mixed, still, and image.

env_file and the shell

If docgen.yaml sets env_file (often .env), variables are loaded with shell-first semantics: anything already exported in the process (including your IDE or CI) is not replaced by the file. To make the file win, set DOCGEN_ENV_OVERRIDES=1 so every key from env_file overwrites the environment, or DOCGEN_ENV_OVERRIDES=OPENAI_API_KEY,OTHER_KEY for specific keys only.

When CURSOR_API_KEY, OPENAI_API_KEY, XAI_API_KEY, or ANTHROPIC_API_KEY is present in both the shell and env_file, docgen prints a one-line hint to stderr so a silent 401 from the wrong key is easier to diagnose.

Narration from source (owner hints)

Under narration_from_source in docgen.yaml, the project owner lists optional hints (strings) that steer the model (audience, terminology, what to avoid). The chat model generates the narration .md from your repo context (context.paths / context.globs, relative to repo_root) plus those hints; the result is what docgen tts reads. See docgen.narrate_from_source.

narration_from_source:
  model: gpt-4o-mini
  temperature: 0.65
  max_context_bytes: 120000
  hints:
    - "Audience: contributors new to this repo."
    - "Do not mention unreleased product codenames."
  context:
    paths:
      - README.md
    globs:
      - "src/**/*.py"
  segments:
    "01":
      hints:
        - "This segment covers the install wizard only."
      context:
        paths:
          - docs/install.md

Useful pipeline options

validation:
  max_drift_sec: 2.75
  max_freeze_ratio: 0.25     # trailing-frame pad vs narration length (compose freeze guard + validate)
  timing_sync:               # audio ↔ timing.json staleness (hard fail in --pre-push)
    enabled: true
    max_tail_gap_sec: 3.0    # mp3 may run this much past the last transcribed word
    max_end_overrun_sec: 1.0 # transcript may extend this far past the mp3
  story_end:                 # last paced reveal vs audio end (hard fail in --pre-push)
    enabled: true
    max_early_sec: 40.0      # idle after last paced box
    max_early_ratio: 0.45    # and idle / audio_end (both must exceed to fail)
  av_sync:                   # OCR anchor check (soft warning in --pre-push)
    enabled: true
    tolerance_sec: 3.0
    prefer_scene_spec_labels: true  # OCR anchors from paced box labels when specs exist
    visual_types: [manim]    # only check types with on-screen text

```yaml
ai:
  provider: openai           # openai | grok (xAI). Override with DOCGEN_AI_PROVIDER.

timestamps:
  engine: local              # local (default, offline) or whisper (OpenAI whisper-1 / xAI STT)
  silence_noise_db: -35.0    # ffmpeg silencedetect threshold for the local engine
  min_silence_sec: 0.3

image_generation:            # scene-spec image elements (docgen image-generate)
  model: gpt-image-1         # Cursor/OpenAI Images; Grok remaps gpt-image-* to Imagine
  size: 1536x1024
  # quality: high            # optional, model-specific

manim:
  quality: 1080p30           # supports 480p15, 720p30, 1080p30, 1080p60, 1440p30, 1440p60, 2160p60
  manim_path: ""             # optional explicit binary path (relative to docgen.yaml or absolute)
  font: "Liberation Sans"
  min_font_size: 14

validation:
  subject_beat_coverage:
    enabled: true            # scene-spec-generate + validate: cover narration topic beats

compose:
  ffmpeg_timeout_sec: 300    # can also be overridden with: docgen compose --ffmpeg-timeout N

System dependencies

  • ffmpeg — composition and probing
  • tesseract-ocr — OCR validation
  • Manim — primary visuals (optional install: pip install docgen[manim])

Milestone spec

See milestone-doc-generator.md for the full design document.

About

Reusable Python library and CLI for producing narrated demo videos from Markdown, Manim, and VHS

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages