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))
|
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]:
|
def load_cache_config() -> Optional[Path]:
|
||||||
"""Load gnommo.conf and return cache path if configured.
|
"""Load gnommo.conf and return cache path if configured.
|
||||||
|
|
||||||
|
|||||||
+116
-4
@@ -133,6 +133,13 @@ Examples:
|
|||||||
type=str,
|
type=str,
|
||||||
help="Render only a range of slides (e.g., S1:S10, S5:, S10:S20)",
|
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(
|
parser.add_argument(
|
||||||
"--res",
|
"--res",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -261,6 +268,7 @@ Examples:
|
|||||||
args.slides,
|
args.slides,
|
||||||
args.res,
|
args.res,
|
||||||
args.force,
|
args.force,
|
||||||
|
chunk_slides=args.chunk_slides,
|
||||||
)
|
)
|
||||||
elif action == "transcribe":
|
elif action == "transcribe":
|
||||||
return cmd_transcribe(project_path, args.verbose, args.res, args.final)
|
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}")
|
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(
|
def cmd_render(
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
@@ -2448,6 +2540,8 @@ def cmd_render(
|
|||||||
slides_arg: str = None,
|
slides_arg: str = None,
|
||||||
res: str = "full",
|
res: str = "full",
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
|
chunk_slides: int = 0,
|
||||||
|
_output_path_override: Path = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Render final video."""
|
"""Render final video."""
|
||||||
from .parser import (
|
from .parser import (
|
||||||
@@ -2702,17 +2796,35 @@ def cmd_render(
|
|||||||
|
|
||||||
# Stage 4: Render
|
# Stage 4: Render
|
||||||
# Determine output filename and directory
|
# 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_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:
|
elif slide_range:
|
||||||
start, end = slide_range
|
start, end = slide_range
|
||||||
range_suffix = f"_{start}-{end}" if end else f"_{start}-end"
|
range_suffix = f"_{start}-{end}" if end else f"_{start}-end"
|
||||||
out_filename = f"final{range_suffix}.mp4"
|
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"
|
out_dir = project_path / "out" / res if res != "full" else project_path / "out"
|
||||||
output_path = out_dir / out_filename
|
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
|
plan.output_path = output_path
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
|||||||
Reference in New Issue
Block a user