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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ docgen validate --pre-push # validate all outputs before committing
| `docgen tts [--segment 01] [--dry-run]` | Generate TTS audio |
| `docgen manim [--scene StackDAGScene]` | Render Manim animations |
| `docgen vhs [--tape 02-quickstart.tape] [--strict]` | Render VHS terminal recordings |
| `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) |
| `docgen validate [--max-drift 2.75] [--pre-push]` | Run all validation checks |
| `docgen concat [--config full-demo]` | Concatenate full demo files |
Expand All @@ -75,6 +76,13 @@ manim:

vhs:
vhs_path: "" # optional explicit binary path (relative to docgen.yaml or absolute)
sync_from_timing: false # opt-in: allow tape Sleep rewrites from timing.json
typing_ms_per_char: 55 # typing estimate used by sync-vhs
max_typing_sec: 3.0 # per block cap for typing estimate
min_sleep_sec: 0.05 # floor for rewritten Sleep values

pipeline:
sync_vhs_after_timestamps: false # opt-in: run sync-vhs automatically in generate-all/rebuild-after-audio

compose:
ffmpeg_timeout_sec: 300 # can also be overridden with: docgen compose --ffmpeg-timeout N
Expand All @@ -83,6 +91,15 @@ compose:

If you edit a `.tape` file, run `docgen vhs` before `docgen compose` so compose does not use stale rendered terminal video.

To auto-align tape pacing with generated narration:

```bash
docgen timestamps
docgen sync-vhs --dry-run
docgen sync-vhs
docgen vhs
docgen compose
```
## System dependencies

- **ffmpeg** — composition and probing
Expand Down
4 changes: 4 additions & 0 deletions docs/demos/docgen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ manim:

vhs:
vhs_path: ""
sync_from_timing: true
typing_ms_per_char: 45
max_typing_sec: 2.0
min_sleep_sec: 0.2

compose:
ffmpeg_timeout_sec: 300
Expand Down
33 changes: 29 additions & 4 deletions src/docgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ def vhs(ctx: click.Context, tape: str | None, strict: bool) -> None:
click.echo(f" {e}")


@main.command("sync-vhs")
@click.option("--segment", default=None, help="Sync tape(s) for one segment ID/name.")
@click.option("--dry-run", is_flag=True, help="Preview updates without writing files.")
@click.pass_context
def sync_vhs(ctx: click.Context, segment: str | None, dry_run: bool) -> None:
"""Sync VHS Sleep durations from animations/timing.json."""
from docgen.tape_sync import TapeSynchronizer

cfg = ctx.obj["config"]
TapeSynchronizer(cfg).sync(segment=segment, dry_run=dry_run)


@main.command()
@click.argument("segments", nargs=-1)
@click.option(
Expand Down Expand Up @@ -225,22 +237,35 @@ def pages(ctx: click.Context, force: bool) -> None:
@click.option("--skip-tts", is_flag=True)
@click.option("--skip-manim", is_flag=True)
@click.option("--skip-vhs", is_flag=True)
@click.option("--skip-tape-sync", is_flag=True, help="Skip optional sync-vhs stage after timestamps.")
@click.pass_context
def generate_all(ctx: click.Context, skip_tts: bool, skip_manim: bool, skip_vhs: bool) -> None:
def generate_all(
ctx: click.Context,
skip_tts: bool,
skip_manim: bool,
skip_vhs: bool,
skip_tape_sync: bool,
) -> None:
"""Run full pipeline: TTS -> Manim -> VHS -> compose -> validate -> concat -> pages."""
from docgen.pipeline import Pipeline

cfg = ctx.obj["config"]
pipeline = Pipeline(cfg)
pipeline.run(skip_tts=skip_tts, skip_manim=skip_manim, skip_vhs=skip_vhs)
pipeline.run(
skip_tts=skip_tts,
skip_manim=skip_manim,
skip_vhs=skip_vhs,
skip_tape_sync=skip_tape_sync,
)


@main.command("rebuild-after-audio")
@click.option("--skip-tape-sync", is_flag=True, help="Skip optional sync-vhs stage after timestamps.")
@click.pass_context
def rebuild_after_audio(ctx: click.Context) -> None:
def rebuild_after_audio(ctx: click.Context, skip_tape_sync: bool) -> None:
"""Rebuild everything after new audio: Manim -> VHS -> compose -> validate -> concat."""
from docgen.pipeline import Pipeline

cfg = ctx.obj["config"]
pipeline = Pipeline(cfg)
pipeline.run(skip_tts=True)
pipeline.run(skip_tts=True, skip_tape_sync=skip_tape_sync)
36 changes: 35 additions & 1 deletion src/docgen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,46 @@ def manim_path(self) -> str | None:
value = self.raw.get("manim", {}).get("manim_path")
return str(value) if value else None

@property
def vhs_config(self) -> dict[str, Any]:
defaults: dict[str, Any] = {
"vhs_path": "",
"sync_from_timing": False,
"typing_ms_per_char": 35,
"max_typing_sec": 3.0,
"min_sleep_sec": 0.2,
}
defaults.update(self.raw.get("vhs", {}))
return defaults

@property
def vhs_path(self) -> str | None:
"""Optional absolute/relative path to the VHS executable."""
value = self.raw.get("vhs", {}).get("vhs_path")
value = self.vhs_config.get("vhs_path")
return str(value) if value else None

@property
def sync_from_timing(self) -> bool:
return bool(self.vhs_config.get("sync_from_timing", False))

@property
def typing_ms_per_char(self) -> int:
return int(self.vhs_config.get("typing_ms_per_char", 35))

@property
def max_typing_sec(self) -> float:
return float(self.vhs_config.get("max_typing_sec", 3.0))

@property
def min_sleep_sec(self) -> float:
return float(self.vhs_config.get("min_sleep_sec", 0.2))

@property
def sync_vhs_after_timestamps(self) -> bool:
pipeline_cfg = self.raw.get("pipeline", {})
if "sync_vhs_after_timestamps" in pipeline_cfg:
return bool(pipeline_cfg.get("sync_vhs_after_timestamps"))
return self.sync_from_timing
# -- Compose ----------------------------------------------------------------

@property
Expand Down
4 changes: 4 additions & 0 deletions src/docgen/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ def _write_config(plan: InitPlan) -> str:
},
"vhs": {
"vhs_path": "",
"sync_from_timing": False,
"typing_ms_per_char": 55,
"max_typing_sec": 3.0,
"min_sleep_sec": 0.2,
},
"compose": {
"ffmpeg_timeout_sec": 300,
Expand Down
6 changes: 6 additions & 0 deletions src/docgen/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def run(
skip_tts: bool = False,
skip_manim: bool = False,
skip_vhs: bool = False,
skip_tape_sync: bool = False,
) -> None:
if not skip_tts:
print("\n=== Stage: TTS ===")
Expand All @@ -27,6 +28,11 @@ def run(
from docgen.timestamps import TimestampExtractor
TimestampExtractor(self.config).extract_all()

if self.config.sync_vhs_after_timestamps and not skip_tape_sync:
print("\n=== Stage: Sync VHS tape sleep timings ===")
from docgen.tape_sync import TapeSynchronizer
TapeSynchronizer(self.config).sync()

if not skip_manim:
print("\n=== Stage: Manim ===")
from docgen.manim_runner import ManimRunner
Expand Down
Loading