Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ docgen validate --pre-push # validate all outputs before committing
| `docgen manim [--scene StackDAGScene]` | Render Manim animations |
| `docgen vhs [--tape 02-quickstart.tape] [--strict] [--timeout 120]` | Render VHS terminal recordings |
| `docgen playwright --script scripts/capture.py --url http://localhost:3000 --source demo.mp4` | Capture browser demo video with Playwright script |
| `docgen demo-function --manifest <path> --output-dir <dir> [--cache-dir <dir>] [--no-narration]` | Render one short, single-purpose video per function from a declarative manifest |
| `docgen tape-lint [--tape 02-quickstart.tape]` | Lint tapes for commands likely to hang in VHS |
| `docgen sync-vhs [--segment 01] [--dry-run]` | Rewrite VHS `Sleep` values from `animations/timing.json` |
| `docgen compose [01 02 03] [--ffmpeg-timeout 900]` | Compose segments (audio + video) |
Expand Down Expand Up @@ -132,6 +133,32 @@ Script contract:
`DOCGEN_PLAYWRIGHT_WIDTH`, `DOCGEN_PLAYWRIGHT_HEIGHT`, and optional segment metadata
- must write an MP4 to the requested output path
- should use headless Playwright for CI compatibility
### Per-function video docs (`docgen demo-function`)

`docgen demo-function` renders **one short, single-purpose MP4 per function** —
the docs-site analogue of a single Playwright `test('…')` describing one
behavior. Inputs are declarative: either a `*.docgen.yaml` sidecar or a
`@pytest.mark.docgen(...)` decorator on a Python test (read statically via
`ast` — never imported / `exec`'d). Outputs are five files in `--output-dir`:
`rendered.mp4` (real ISO MP4), `poster.png`, `fragment.txt` (`fn-<slug>`),
`manifest.json` (snapshot), and `cache-status.txt` (`hit` / `miss`).

```bash
docgen demo-function \
--manifest examples/lesson_compile.docgen.yaml \
--output-dir /tmp/out \
--cache-dir /tmp/docgen-cache
```

Exit codes: `0` success, `1` invalid manifest / render failure, `2` missing
`ffmpeg` / `playwright`, `78` neutral skip (placeholder manifest with no
`url` — useful in CI). When `OPENAI_API_KEY` is set, the intent line is
narrated via `gpt-4o-mini-tts` and muxed onto the video; pass
`--no-narration` to skip TTS even if the key is set. See
[`examples/lesson_compile.docgen.yaml`](examples/lesson_compile.docgen.yaml)
and [`examples/sample_test.py`](examples/sample_test.py) for both input
shapes.

### VHS safety: avoid real long-running commands in tapes

VHS executes commands in a real shell session. For demos, prefer simulated output with `echo`
Expand Down
21 changes: 21 additions & 0 deletions examples/lesson_compile.docgen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
identifier: "course-builder/src/lessons/compileLesson.ts:compileLesson"
intent: |
Compiles a lesson markdown file into structured checkpoints and emits a status badge.
setup:
fixtures:
- tests/fixtures/lessons/intro.md
demonstration:
kind: playwright
url: "http://127.0.0.1:3000/lessons/new"
actions:
- {kind: type, selector: '[data-testid="title"]', value: "Intro to TS", delay_ms: 30}
- {kind: click, selector: '[data-testid="compile"]'}
- {kind: wait_for_text, selector: '[data-testid="status"]', text: "compiled", timeout_ms: 10000}
- {kind: wait, ms: 600}
assertions_to_surface:
- "lesson.status === 'compiled'"
- "checkpoint count = 3"
output_budget:
duration_seconds: 30
segments: 1
resolution: "1280x720"
37 changes: 37 additions & 0 deletions examples/sample_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Sample test demonstrating the @pytest.mark.docgen(...) marker shape.

The marker is read statically by `docgen demo-function --manifest <file>::<test>`
via `ast` (no import / no exec). The keyword args mirror the YAML keys.

Note: this file does NOT need pytest to be installed at read time — the marker
is parsed from source. The triple-quoted text below intentionally talks about
``pytest.mark.docgen`` to verify the AST-based loader is not fooled by
docstring text (regression guard for F7).
"""

import pytest


@pytest.mark.docgen(
identifier="course-builder/src/lessons/compileLesson.ts:compileLesson",
intent="Compiles a lesson markdown file into structured checkpoints and emits a status badge.",
setup={"fixtures": ["tests/fixtures/lessons/intro.md"]},
demonstration={
"kind": "playwright",
"url": "http://127.0.0.1:3000/lessons/new",
"actions": [
{"kind": "type", "selector": '[data-testid="title"]', "value": "Intro to TS", "delay_ms": 30},
{"kind": "click", "selector": '[data-testid="compile"]'},
{"kind": "wait_for_text", "selector": '[data-testid="status"]', "text": "compiled", "timeout_ms": 10000},
{"kind": "wait", "ms": 600},
],
},
assertions_to_surface=[
"lesson.status === 'compiled'",
"checkpoint count = 3",
],
output_budget={"duration_seconds": 30, "segments": 1, "resolution": "1280x720"},
)
def test_lesson_compile():
"""Render the compileLesson demo (sample for docgen demo-function)."""
pass
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "docgen"
version = "0.1.0"
version = "0.2.0"
description = "Reusable demo generation pipeline: TTS, Manim, VHS, ffmpeg composition, validation, GitHub Pages publishing"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
47 changes: 47 additions & 0 deletions src/docgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,53 @@ def playwright(
click.echo(f"[playwright] captured: {video}")


@main.command("demo-function")
@click.option(
"--manifest",
"manifest_arg",
required=True,
help="Path to *.docgen.yaml sidecar OR <path>.py::<test_name> for @pytest.mark.docgen.",
)
@click.option(
"--output-dir",
"output_dir_arg",
required=True,
type=click.Path(file_okay=False),
help="Directory to write rendered.mp4, poster.png, fragment.txt, manifest.json, cache-status.txt.",
)
@click.option(
"--cache-dir",
"cache_dir_arg",
default=None,
type=click.Path(file_okay=False),
help="Optional cache directory keyed by sha256(identifier+intent+fixtures).",
)
@click.option(
"--no-narration",
is_flag=True,
help="Skip TTS even if OPENAI_API_KEY is set.",
)
@click.pass_context
def demo_function(
ctx: click.Context,
manifest_arg: str,
output_dir_arg: str,
cache_dir_arg: str | None,
no_narration: bool,
) -> None:
"""Render a single per-function demo video from a declarative manifest."""
from docgen.demo_function import run_cli

code = run_cli(
manifest_arg=manifest_arg,
output_dir_arg=output_dir_arg,
cache_dir_arg=cache_dir_arg,
no_narration=no_narration,
)
if code != 0:
raise SystemExit(code)


@main.command("tape-lint")
@click.option("--tape", default=None, help="Lint a single tape name or pattern.")
@click.pass_context
Expand Down
Loading