diff --git a/gnommo/cli.py b/gnommo/cli.py index 25d6530..9b93668 100644 --- a/gnommo/cli.py +++ b/gnommo/cli.py @@ -50,7 +50,7 @@ Examples: gnommo -p video1 transcode --processed --alpha-quality 0.5 More aggressive alpha compression gnommo -p video1 transcode --processed --dry-run Preview what would be compressed gnommo -p video1 transcode --force Re-transcode even if output already exists - gnommo -p video1 all Full pipeline: transcribe → align → render + gnommo -p video1 all Full pipeline: import → preprocess → trim → stitch → render → handoff gnommo -p video1 render --dry-run Show FFmpeg command without running gnommo -p video1 description Generate YouTube description file gnommo -p video1 transcribe Narration file for timing of slides @@ -833,7 +833,6 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No Folder structure: media/narration/raw_mov/ ← raw recordings from iPhone/QuickTime - media/narration/compressed/ ← H.265 copies (transcode 1st pass) media/narration/processed/ ← chroma-keyed output (preprocess) media/narration/narration.json @@ -2757,7 +2756,7 @@ def cmd_all( res: str = "full", force: bool = False, ) -> int: - """Run full pipeline: import → transcode → preprocess → transcode --processed → trim → stitch → render → handoff. + """Run full pipeline: import → preprocess → trim → stitch → render → handoff. Cascade rule: if any stage produces output, all subsequent stages are forced to re-run (cascade_force=True), regardless of whether --force was passed. @@ -2771,22 +2770,17 @@ def cmd_all( # True so all downstream stages re-run unconditionally. cascade_force = force - print(">>> Step 1/8: Import\n") + print(">>> Step 1/6: Import\n") + t0 = time.time() result = cmd_import(project_path, cascade_force, verbose) if result != 0: return result + if _files_modified_since(project_path, t0, "slides.json") or _files_modified_since( + project_path, t0, "narration.json" + ): + cascade_force = True - print("\n>>> Step 2/8: Transcode narration (H.265)\n") - t0 = time.time() - result = cmd_transcode( - project_path, verbose, dry_run, replace=False, crf=23, force=cascade_force - ) - if result != 0: - return result - # Step 2 does not cascade: preprocess already checks its own output existence. - # A broad *_compressed.mp4 pattern would falsely match pre-existing raw_mp4/ sources. - - print("\n>>> Step 3/8: Preprocess\n") + print("\n>>> Step 2/6: Preprocess\n") t0 = time.time() result = cmd_preprocess(project_path, verbose, dry_run, cascade_force, workers=1, res=res) if result != 0: @@ -2797,24 +2791,7 @@ def cmd_all( ): cascade_force = True - print("\n>>> Step 4/8: Transcode processed (HEVC+alpha)\n") - t0 = time.time() - result = cmd_transcode( - project_path, - verbose, - dry_run, - replace=False, - crf=23, - force=cascade_force, - processed=True, - alpha_quality=1.0, - ) - if result != 0: - return result - if _files_modified_since(project_path, t0, "*_processed.mov"): - cascade_force = True - - print("\n>>> Step 5/8: Trim\n") + print("\n>>> Step 3/6: Trim\n") t0 = time.time() result = cmd_trim(project_path, verbose, force=cascade_force, threshold_db=-40.0) if result != 0: @@ -2823,7 +2800,7 @@ def cmd_all( if _files_modified_since(project_path, t0, "narration.json"): cascade_force = True - print("\n>>> Step 6/8: Stitch\n") + print("\n>>> Step 4/6: Stitch\n") t0 = time.time() result = cmd_stitch(project_path, verbose, cascade_force, res=res) if result != 0: @@ -2831,12 +2808,12 @@ def cmd_all( if _files_modified_since(project_path, t0, "narration_combined.mov"): cascade_force = True - print("\n>>> Step 7/8: Render\n") + print("\n>>> Step 5/6: Render\n") result = cmd_render(project_path, verbose, dry_run, res=res, force=cascade_force) if result != 0: return result - print("\n>>> Step 8/8: Handoff\n") + print("\n>>> Step 6/6: Handoff\n") return cmd_handoff(project_path, verbose, file_override=None, prod=False, res=res) diff --git a/gnommo/preprocessor.py b/gnommo/preprocessor.py index f42f7d4..f5aab99 100644 --- a/gnommo/preprocessor.py +++ b/gnommo/preprocessor.py @@ -1349,8 +1349,6 @@ def _process_chunk_to_prores4444( "yuva444p10le", # must carry alpha "-vendor", "apl0", # optional; helps some NLEs tag as Apple ProRes - "-movflags", - "+faststart", # optional; makes MOV streamable ] ) @@ -1378,6 +1376,22 @@ def _process_chunk_to_prores4444( stderr=result.stderr, ) + # Validate the output file is a readable MOV (moov atom present). + # FFmpeg can return 0 but write a corrupt/incomplete file (e.g. moov atom + # missing) when faststart rewrite fails or disk is under pressure. + probe = subprocess.run( + ["ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "csv=p=0", str(output_path)], + capture_output=True, text=True, + ) + if probe.returncode != 0 or not probe.stdout.strip(): + raise PreprocessError( + f"Chunk output file is unreadable or missing moov atom: {output_path.name}", + filter_type="chunk", + command=" ".join(cmd), + stderr=probe.stderr, + ) + def _process_chunk_to_webm( input_path: Path, diff --git a/render_all.sh b/render_all.sh new file mode 100755 index 0000000..f9092e5 --- /dev/null +++ b/render_all.sh @@ -0,0 +1,10 @@ +#!/bin/sh + + +./gnommo.sh -p video1 all +./gnommo.sh -p video2 all +./gnommo.sh -p video3 all +./gnommo.sh -p video4 all +./gnommo.sh -p video5 all +./gnommo.sh -p video6 all +