Adding chunking to main render loop

This commit is contained in:
2026-05-12 20:45:36 +02:00
parent 60e2f20b0f
commit 87424a6531
2 changed files with 137 additions and 3 deletions
+22
View File
@@ -49,6 +49,28 @@ def get_ffmpeg_thread_count() -> int:
return max(1, int(cpu_count * cpu_limit))
def get_render_chunk_size() -> Optional[int]:
"""Return slides-per-chunk for auto-chunked rendering, or None if not configured.
When set, cmd_render splits the filter graph into chunks of this many slides
to avoid OOM from allocating filter buffers for the entire video at once.
Example ~/.gnommo.conf:
[performance]
render_chunk_slides = 15
"""
global _perf_config
if _perf_config is None:
get_ffmpeg_thread_count() # populates _perf_config
val = _perf_config.get("render_chunk_slides")
if val is None:
return None
try:
return max(1, int(val))
except (ValueError, TypeError):
return None
def load_cache_config() -> Optional[Path]:
"""Load gnommo.conf and return cache path if configured.
+115 -3
View File
@@ -133,6 +133,13 @@ Examples:
type=str,
help="Render only a range of slides (e.g., S1:S10, S5:, S10:S20)",
)
parser.add_argument(
"--chunk-slides",
type=int,
default=0,
dest="chunk_slides",
help="Split render into chunks of N slides each and concatenate (overrides render_chunk_slides in .gnommo.conf)",
)
parser.add_argument(
"--res",
type=str,
@@ -261,6 +268,7 @@ Examples:
args.slides,
args.res,
args.force,
chunk_slides=args.chunk_slides,
)
elif action == "transcribe":
return cmd_transcribe(project_path, args.verbose, args.res, args.final)
@@ -2441,6 +2449,90 @@ def _writeback_video_metadata(plan, project_path, config) -> None:
print(f" Updated videos.json: {written}")
def _chunked_render(
project_path: Path,
verbose: bool,
dry_run: bool,
res: str,
force: bool,
chunk_size: int,
slide_ids: list[str],
out_dir: Path,
final_output: Path,
) -> int:
"""Render in slide-based chunks then concatenate — avoids filter graph OOM."""
import math
# 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)]
print(
f"\n Auto-chunking: {len(slide_ids)} slides → {len(groups)} chunks of ≤{chunk_size}"
)
chunks_dir = out_dir / "chunks"
chunks_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"
print(f"\n {'='*56}")
print(f" Chunk {i+1}/{len(groups)}: {slides_arg}{chunk_path.name}")
print(f" {'='*56}")
result = cmd_render(
project_path,
verbose,
dry_run,
slides_arg=slides_arg,
res=res,
force=force,
_output_path_override=chunk_path,
)
if result != 0:
print(f"\n Chunk {i+1} failed — aborting.", file=sys.stderr)
return result
chunk_paths.append(chunk_path)
if dry_run:
print(f"\n [dry-run] Would concatenate {len(chunk_paths)} chunks → {final_output}")
return 0
# Concatenate chunks
print(f"\n Concatenating {len(chunk_paths)} chunks → {final_output.name}...")
concat_list = chunks_dir / "concat.txt"
with open(concat_list, "w") as f:
for p in chunk_paths:
f.write(f"file '{p.resolve()}'\n")
concat_cmd = [
"ffmpeg", "-y",
"-f", "concat", "-safe", "0",
"-i", str(concat_list),
"-c", "copy",
str(final_output),
]
result = subprocess.run(concat_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f" Concatenation failed:\n{result.stderr}", file=sys.stderr)
return 1
# Clean up chunk files
for p in chunk_paths:
p.unlink(missing_ok=True)
concat_list.unlink(missing_ok=True)
try:
chunks_dir.rmdir()
except OSError:
pass
print(f" Output: {final_output}")
return 0
def cmd_render(
project_path: Path,
verbose: bool,
@@ -2448,6 +2540,8 @@ def cmd_render(
slides_arg: str = None,
res: str = "full",
force: bool = False,
chunk_slides: int = 0,
_output_path_override: Path = None,
) -> int:
"""Render final video."""
from .parser import (
@@ -2702,17 +2796,35 @@ def cmd_render(
# Stage 4: Render
# Determine output filename and directory
if config.output_video:
if _output_path_override:
output_path = _output_path_override
out_dir = output_path.parent
out_filename = output_path.name
elif config.output_video:
out_filename = config.output_video
out_dir = project_path / "out" / res if res != "full" else project_path / "out"
output_path = out_dir / out_filename
elif slide_range:
start, end = slide_range
range_suffix = f"_{start}-{end}" if end else f"_{start}-end"
out_filename = f"final{range_suffix}.mp4"
out_dir = project_path / "out" / res if res != "full" else project_path / "out"
output_path = out_dir / out_filename
else:
out_filename = f"{config.co}.mp4"
out_dir = project_path / "out" / res if res != "full" else project_path / "out"
output_path = out_dir / out_filename
# 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]
if _chunk_size > 0 and not slide_range and len(_slide_ids) > _chunk_size:
return _chunked_render(
project_path, verbose, dry_run, res, force,
_chunk_size, _slide_ids, out_dir, output_path,
)
out_dir = project_path / "out" / res if res != "full" else project_path / "out"
output_path = out_dir / out_filename
plan.output_path = output_path
if dry_run: