diff --git a/gnommo/cli.py b/gnommo/cli.py index 44e8587..dc7fdb0 100644 --- a/gnommo/cli.py +++ b/gnommo/cli.py @@ -186,6 +186,17 @@ Examples: dest="chunk_slides", help="Split render into chunks of N slides each and concatenate (overrides render_chunk_slides in .gnommo.conf)", ) + parser.add_argument( + "-n", + "--chunk-debug", + type=int, + default=0, + dest="chunk_debug_slides", + metavar="N", + help="Debug: render the video in chunks of N slides each as SEPARATE files " + "on disk (suffixed with slide range, e.g. video1_S1_S4.mp4), without " + "concatenating — isolates where long-range renders OOM/fail.", + ) parser.add_argument( "--res", type=str, @@ -394,6 +405,7 @@ Examples: args.res, args.force, chunk_slides=args.chunk_slides, + chunk_debug_slides=args.chunk_debug_slides, realign=args.realign, ) elif action == "grade": @@ -4182,16 +4194,22 @@ def _chunked_render( out_dir: Path, final_output: Path, plan=None, + concat: bool = True, ) -> int: - """Render in slide-based chunks then concatenate — avoids filter graph OOM.""" - import math + """Render in slide-based chunks — avoids filter graph OOM on long videos. + concat=True (default) concatenates the chunks into `final_output` and deletes + the parts. concat=False (debug, -n) keeps each chunk as its own inspectable + file in `out_dir`, named `{stem}_{Sfirst}_{Slast}{suffix}`, and does not + concatenate — for isolating which slide range a long render fails on. + """ # Split slide IDs into groups of chunk_size groups = [ slide_ids[i : i + chunk_size] for i in range(0, len(slide_ids), chunk_size) ] + _mode = "concatenate" if concat else "separate files (debug, no concat)" print( - f"\n Auto-chunking: {len(slide_ids)} slides → {len(groups)} chunks of ≤{chunk_size}" + f"\n Chunking: {len(slide_ids)} slides → {len(groups)} chunks of ≤{chunk_size} [{_mode}]" ) # Report clips that span a chunk boundary. v2 seeks into them so they continue @@ -4212,15 +4230,22 @@ def _chunked_render( print(" If a seam looks off, verify frame alignment (concat uses -c copy).", file=sys.stderr) - chunks_dir = out_dir / "chunks" - chunks_dir.mkdir(parents=True, exist_ok=True) + if concat: + chunks_dir = out_dir / "chunks" + chunks_dir.mkdir(parents=True, exist_ok=True) + else: + out_dir.mkdir(parents=True, exist_ok=True) chunk_paths: list[Path] = [] for i, group in enumerate(groups): start = group[0] end = groups[i + 1][0] if i + 1 < len(groups) else None slides_arg = f"{start}:{end}" if end else f"{start}:" - chunk_path = chunks_dir / f"chunk_{i+1:03d}_{start}-{end or 'end'}.mp4" + if concat: + chunk_path = chunks_dir / f"chunk_{i+1:03d}_{start}-{end or 'end'}.mp4" + else: + # Debug: land next to the final output, named by this chunk's own range. + chunk_path = out_dir / f"{final_output.stem}_{start}_{group[-1]}{final_output.suffix}" print(f"\n {'='*56}") print(f" Chunk {i+1}/{len(groups)}: {slides_arg} → {chunk_path.name}") @@ -4237,9 +4262,25 @@ def _chunked_render( ) if result != 0: print(f"\n Chunk {i+1} failed — aborting.", file=sys.stderr) + if not concat: + print( + f" → the render fails somewhere in {slides_arg} " + f"({len(group)} slides). Narrow with a smaller -n.", + file=sys.stderr, + ) return result chunk_paths.append(chunk_path) + # Debug mode: leave the per-chunk files on disk for inspection, no concat. + if not concat: + if dry_run: + print(f"\n [dry-run] Would write {len(groups)} separate chunk file(s) to {out_dir}") + return 0 + print(f"\n Wrote {len(chunk_paths)} chunk file(s) to {out_dir}:") + for p in chunk_paths: + print(f" {p.name}") + return 0 + if dry_run: print( f"\n [dry-run] Would concatenate {len(chunk_paths)} chunks → {final_output}" @@ -4656,6 +4697,7 @@ def cmd_render( res: str = "full", force: bool = False, chunk_slides: int = 0, + chunk_debug_slides: int = 0, _output_path_override: Path = None, plan_only: bool = False, realign: bool = False, @@ -4672,6 +4714,7 @@ def cmd_render( res=res, force=force, chunk_slides=chunk_slides, + chunk_debug_slides=chunk_debug_slides, _output_path_override=_output_path_override, plan_only=plan_only, realign=realign, @@ -4730,6 +4773,7 @@ def _cmd_render_impl( res: str = "full", force: bool = False, chunk_slides: int = 0, + chunk_debug_slides: int = 0, _output_path_override: Path = None, plan_only: bool = False, realign: bool = False, @@ -5153,8 +5197,30 @@ def _cmd_render_impl( # Check if chunked rendering is needed (avoids filter graph OOM on long videos) from .cache import get_render_chunk_size - _chunk_size = chunk_slides or get_render_chunk_size() or 0 _slide_ids = [e.slide_id for e in plan.slide_events] + + # Debug mode (-n N): render each N-slide chunk to its own file on disk and + # stop — no concatenation. Lets you see exactly which slide range a long + # render dies on, and what chunking does to the memory profile. + if chunk_debug_slides > 0 and _output_path_override is None: + if slide_range: + print(" -n/--chunk-debug cannot be combined with --slides.", file=sys.stderr) + return 1 + return _chunked_render( + project_path, + verbose, + dry_run, + res, + force, + chunk_debug_slides, + _slide_ids, + out_dir, + output_path, + plan=plan, + concat=False, + ) + + _chunk_size = chunk_slides or get_render_chunk_size() or 0 if _chunk_size > 0 and not slide_range and len(_slide_ids) > _chunk_size: return _chunked_render( project_path,