Skip to content

Commit cb9f145

Browse files
jmjavacursoragent
andauthored
Fail closed when CLI compose walks segments.default instead of segments.all. (#168)
Bare compose now uses the same segments.all set as generate-all, so a mapped 02 missing visuals cannot hide behind a one-segment default list. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bc4a21e commit cb9f145

4 files changed

Lines changed: 48 additions & 6 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Commands registered on the **`docgen`** CLI include:
7272
- **`timestamps`** — word/segment timing (`timing.json`). Default engine **`local`** aligns the known narration text against the mp3 offline (ffmpeg silencedetect, no API); **`--engine whisper`** uses OpenAI whisper-1 or xAI `/v1/stt` when `ai.provider` is grok. Both emit the same Whisper-shaped blocks. Failed ffmpeg silencedetect raises `AlignmentError` (empty stderr is not treated as full-span speech). OpenAI whisper-1 word/segment `start`/`end` must be finite JSON numbers (bool/NaN raise `AIError`). Grok `/v1/stt` rejects empty word tokens and inverted `end < start` intervals (`AIError`). Empty ``segments.all`` raises ``TimestampError`` (same as TTS) and does not leave a stale ``timing.json`` as success.
7373
- **`image-generate`** — render scene-spec **image elements** (`image:` + `prompt:` boxes) via OpenAI Images or xAI Imagine into the bundle (also runs for missing assets inside `generate-all`).
7474
- **`manim`** — render Manim scenes declared in config.
75-
- **`compose`** — mux narration audio with visual sources via ffmpeg. A ``type: mixed`` row raises ``ComposeError`` if any listed source is missing (no silent subset mux).
75+
- **`compose`** — mux narration audio with visual sources via ffmpeg. With no segment ids, uses ``segments.all`` (same as ``generate-all``), not ``segments.default``. A ``type: mixed`` row raises ``ComposeError`` if any listed source is missing (no silent subset mux).
7676
- **`validate`** / **`validate --pre-push`** — drift, narration lint, Manim hints, **`timing_sync`**, **`story_end`** (last paced reveal vs audio end; hard fail), **`scene_assets`** (pre-render: stuck-board cadence, frame-budget overlaps, `MANIM_FONT` consistency, stale helpers / stale compiled class including hand-edited generated-region labels and `run_time` — hard fail; also a `generate-all` gate before Manim), **`av_sync`** (hard fail on `--pre-push` / `generate-all`; prefers scene-spec labels as OCR anchors), **`subject_beat_coverage`** (declarative specs vs narration topic beats; hard fail when enabled), and related visual-sync checks (`ocr_scan`, `layout`, `freeze_ratio` — hard fail on `--pre-push` / `generate-all`). Missing tesseract fails `ocr_scan` / `av_sync` / `layout` (not skip-PASS). Missing audio or an LFS pointer fails `timing_sync` and recording media gates (`stream_presence`, `av_drift`, `ocr_scan`, `av_sync`) (not skip-PASS). Missing `*.scene.yaml` fails `story_end` / `subject_beat_coverage` for `type: manim` (not skip-PASS).
7777
- **`lint`** — narration lint helper.
7878
- **`narration-generate`** — LLM-assisted narration from hints and repo context; optional **`--revise --revision-notes`** for in-place edits (same contract as the wizard Revise button).

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ docgen --repo /path/to/your-project generate-all
243243
| `docgen timestamps [--engine local\|whisper]` | Extract word/segment timestamps from TTS audio → `timing.json` (default `local`: offline narration-text alignment; `whisper`: OpenAI transcription). Empty `segments.all` is `TimestampError` (stale `timing.json` is not success) |
244244
| `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 |
245245
| `docgen manim [--scene StackDAGScene]` | Render Manim animations |
246-
| `docgen compose [01 02 03] [--ffmpeg-timeout 900]` | Compose segments (audio + video) |
246+
| `docgen compose [01 02 03] [--ffmpeg-timeout 900]` | Compose segments (audio + video). Omit ids to walk `segments.all` (same as `generate-all`); a mapped segment with missing audio/visuals is a hard fail |
247247
| `docgen validate [--max-drift 2.75] [--pre-push]` | Run all validation checks |
248248
| `docgen lint [--segment 01]` | Narration lint only |
249249
| `docgen concat [--config full-demo]` | Concatenate full demo files |

‎src/docgen/cli.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -486,13 +486,14 @@ def compose(
486486
) -> None:
487487
"""Compose segments (audio + video via ffmpeg).
488488
489-
Pass segment IDs to compose specific ones, or omit for the default set.
489+
Pass segment IDs to compose specific ones, or omit for ``segments.all``
490+
(same set as ``generate-all``).
490491
"""
491492
from docgen.compose import ComposeError, Composer, filter_segments_by_visual_types
492493

493494
cfg = _require_config(ctx)
494495
comp = Composer(cfg, ffmpeg_timeout_sec=ffmpeg_timeout)
495-
target = list(segments) if segments else list(cfg.segments_default or cfg.segments_all)
496+
target = list(segments) if segments else list(cfg.segments_all)
496497
target = filter_segments_by_visual_types(cfg, target, only_visual_types)
497498
if only_visual_types and not target:
498499
raise click.ClickException(
@@ -501,8 +502,7 @@ def compose(
501502
)
502503
if not target:
503504
raise click.ClickException(
504-
"[compose] no segments to compose — set segments.default or "
505-
"segments.all, or pass segment ids"
505+
"[compose] no segments to compose — set segments.all, or pass segment ids"
506506
)
507507
click.echo(f"=== Composing {len(target)} segments ===")
508508
try:

‎tests/test_compose.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,48 @@ def test_cli_compose_falls_back_to_segments_all_when_default_empty(tmp_path: Pat
213213
assert "Composing 1 segments" in result.output
214214

215215

216+
def test_cli_compose_uses_segments_all_not_default(tmp_path: Path, monkeypatch) -> None:
217+
"""Bare compose walks ``segments.all``, matching generate-all.
218+
219+
``default: ["01"]`` plus ``all: ["01", "02"]`` must try both. With 01
220+
muxable and 02 missing visuals, the CLI fail-closes on 1/2 instead of
221+
exiting 0 after only the default set.
222+
"""
223+
from click.testing import CliRunner
224+
225+
from docgen.cli import main
226+
from docgen.compose import Composer
227+
228+
cfg = {
229+
"dirs": {"animations": "animations", "audio": "audio", "recordings": "recordings"},
230+
"segments": {"default": ["01"], "all": ["01", "02"]},
231+
"segment_names": {"01": "01-demo", "02": "02-next"},
232+
"visual_map": {
233+
"01": {"type": "manim", "source": "Scene01.mp4"},
234+
"02": {"type": "manim", "source": "Scene02.mp4"},
235+
},
236+
"manim": {"quality": "1080p30"},
237+
}
238+
c = _write_cfg(tmp_path, cfg)
239+
audio_dir = tmp_path / "audio"
240+
audio_dir.mkdir(parents=True, exist_ok=True)
241+
(audio_dir / "01-demo.mp3").write_bytes(b"audio")
242+
video_dir = tmp_path / "animations" / "media" / "videos" / "scenes" / "1080p30"
243+
video_dir.mkdir(parents=True, exist_ok=True)
244+
(video_dir / "Scene01.mp4").write_bytes(b"video")
245+
(tmp_path / "recordings").mkdir(parents=True, exist_ok=True)
246+
monkeypatch.setattr(Composer, "_probe_duration", lambda self, _p: 10.0)
247+
monkeypatch.setattr(Composer, "_run_ffmpeg", lambda self, _cmd: None)
248+
249+
runner = CliRunner()
250+
result = runner.invoke(main, ["--config", str(c.yaml_path), "compose"])
251+
combined = result.output + result.stderr
252+
assert result.exit_code != 0
253+
assert "Composing 2 segments" in combined
254+
assert "1/2" in combined
255+
assert "02" in combined
256+
257+
216258
def test_cli_lint_exits_nonzero_when_narration_missing(tmp_path: Path) -> None:
217259
from click.testing import CliRunner
218260

0 commit comments

Comments
 (0)