Adding chunking to main render loop
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
+116
-4
@@ -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"
|
||||
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
|
||||
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,
|
||||
)
|
||||
|
||||
plan.output_path = output_path
|
||||
|
||||
if dry_run:
|
||||
|
||||
Reference in New Issue
Block a user