Skip to content

Commit 7bb55f3

Browse files
committed
feat: upgrade compose with tpad freeze-frame, add lint CLI, env loading
- compose.py: port battle-tested tpad/freeze-frame logic from compose.sh (replaces -stream_loop which repeats video awkwardly), add image compose type, use resolve_segment_name for output paths - cli.py: add `docgen lint` command for standalone narration linting, auto-load .env from config for OPENAI_API_KEY, improve VHS output formatting, fix rebuild-after-audio to skip only TTS (not Manim/VHS) - concat.py: use resolve_segment_name for segment lookup, auto-add .mp4 extension to output filenames Made-with: Cursor
1 parent 714d413 commit 7bb55f3

File tree

3 files changed

+214
-56
lines changed

3 files changed

+214
-56
lines changed

src/docgen/cli.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@
22

33
from __future__ import annotations
44

5+
import os
6+
57
import click
68

79
from docgen.config import Config
810

911

12+
def _load_env(cfg: Config | None) -> None:
13+
"""Load .env file from config if specified, so OPENAI_API_KEY etc. are available."""
14+
if cfg and cfg.env_file and cfg.env_file.exists():
15+
for line in cfg.env_file.read_text().splitlines():
16+
line = line.strip()
17+
if line and not line.startswith("#") and "=" in line:
18+
key, _, val = line.partition("=")
19+
os.environ.setdefault(key.strip(), val.strip().strip('"').strip("'"))
20+
21+
1022
@click.group()
1123
@click.option(
1224
"--config",
@@ -24,6 +36,7 @@ def main(ctx: click.Context, config_path: str | None) -> None:
2436
except FileNotFoundError:
2537
cfg = None
2638
ctx.obj["config"] = cfg
39+
_load_env(cfg)
2740

2841

2942
@main.command()
@@ -74,19 +87,28 @@ def vhs(ctx: click.Context, tape: str | None, strict: bool) -> None:
7487

7588
cfg = ctx.obj["config"]
7689
runner = VHSRunner(cfg)
77-
runner.render(tape=tape, strict=strict)
90+
results = runner.render(tape=tape, strict=strict)
91+
for r in results:
92+
status = "ok" if r.success else "FAIL"
93+
click.echo(f" [{status}] {r.tape}")
94+
for e in r.errors:
95+
click.echo(f" {e}")
7896

7997

8098
@main.command()
8199
@click.argument("segments", nargs=-1)
82100
@click.pass_context
83101
def compose(ctx: click.Context, segments: tuple[str, ...]) -> None:
84-
"""Compose segments (audio + video)."""
102+
"""Compose segments (audio + video via ffmpeg).
103+
104+
Pass segment IDs to compose specific ones, or omit for the default set.
105+
"""
85106
from docgen.compose import Composer
86107

87108
cfg = ctx.obj["config"]
88109
comp = Composer(cfg)
89110
target = list(segments) if segments else cfg.segments_default
111+
click.echo(f"=== Composing {len(target)} segments ===")
90112
comp.compose_segments(target)
91113

92114

@@ -95,7 +117,7 @@ def compose(ctx: click.Context, segments: tuple[str, ...]) -> None:
95117
@click.option("--pre-push", is_flag=True, help="Run all checks; exit non-zero on any failure.")
96118
@click.pass_context
97119
def validate(ctx: click.Context, max_drift: float | None, pre_push: bool) -> None:
98-
"""Run validation checks on composed videos."""
120+
"""Run validation checks on composed videos (streams, drift, narration lint)."""
99121
from docgen.validate import Validator
100122

101123
cfg = ctx.obj["config"]
@@ -107,11 +129,46 @@ def validate(ctx: click.Context, max_drift: float | None, pre_push: bool) -> Non
107129
v.print_report(report)
108130

109131

132+
@main.command()
133+
@click.option("--segment", default=None, help="Lint a single segment.")
134+
@click.pass_context
135+
def lint(ctx: click.Context, segment: str | None) -> None:
136+
"""Run narration lint on all (or one) segment narration files."""
137+
from docgen.narration_lint import NarrationLinter
138+
139+
cfg = ctx.obj["config"]
140+
linter = NarrationLinter(cfg)
141+
segments = [segment] if segment else cfg.segments_all
142+
issues_total = 0
143+
144+
for seg_id in segments:
145+
seg_name = cfg.resolve_segment_name(seg_id)
146+
narr_dir = cfg.narration_dir
147+
if not narr_dir.exists():
148+
continue
149+
path = narr_dir / f"{seg_name}.md"
150+
if not path.exists():
151+
candidates = list(narr_dir.glob(f"{seg_id}-*.md"))
152+
path = candidates[0] if candidates else None
153+
if not path or not path.exists():
154+
click.echo(f" [{seg_id}] no narration file")
155+
continue
156+
result = linter.lint_text(path.read_text(encoding="utf-8"))
157+
status = "PASS" if result.passed else "FAIL"
158+
click.echo(f" [{seg_id}] {status} {seg_name}")
159+
for issue in result.issues:
160+
click.echo(f" {issue}")
161+
issues_total += 1
162+
163+
if issues_total:
164+
raise SystemExit(1)
165+
166+
110167
@main.command()
111168
@click.option("--config-name", "concat_name", default=None, help="Concat config name.")
112169
@click.pass_context
113170
def concat(ctx: click.Context, concat_name: str | None) -> None:
114-
"""Concatenate full demo files."""
171+
"""Concatenate full demo files from composed segments."""
115172
from docgen.concat import ConcatBuilder
116173

117174
cfg = ctx.obj["config"]
@@ -137,7 +194,7 @@ def pages(ctx: click.Context, force: bool) -> None:
137194
@click.option("--skip-vhs", is_flag=True)
138195
@click.pass_context
139196
def generate_all(ctx: click.Context, skip_tts: bool, skip_manim: bool, skip_vhs: bool) -> None:
140-
"""Run full pipeline: TTS Manim VHS compose validate concat pages."""
197+
"""Run full pipeline: TTS -> Manim -> VHS -> compose -> validate -> concat -> pages."""
141198
from docgen.pipeline import Pipeline
142199

143200
cfg = ctx.obj["config"]
@@ -148,9 +205,9 @@ def generate_all(ctx: click.Context, skip_tts: bool, skip_manim: bool, skip_vhs:
148205
@main.command("rebuild-after-audio")
149206
@click.pass_context
150207
def rebuild_after_audio(ctx: click.Context) -> None:
151-
"""Recompose + validate + concat (skip TTS/Manim/VHS)."""
208+
"""Rebuild everything after new audio: Manim -> VHS -> compose -> validate -> concat."""
152209
from docgen.pipeline import Pipeline
153210

154211
cfg = ctx.obj["config"]
155212
pipeline = Pipeline(cfg)
156-
pipeline.run(skip_tts=True, skip_manim=True, skip_vhs=True)
213+
pipeline.run(skip_tts=True)

0 commit comments

Comments
 (0)