"""Render local test video for Papercraft Minimal theme with Solid Hydraulic Press Compactor.""" from __future__ import annotations import os import shutil import subprocess import sys import time from pathlib import Path from raceforge.audio.mix import AudioSynthesizer from raceforge.director.base import TrackDirective from raceforge.director.procedural import ProceduralDirector from raceforge.encode.ffmpeg_pipe import encode_full_mp4 from raceforge.render.frames import CairoFrameRenderer from raceforge.sim.engine import SimulationEngine from raceforge.track.builder import build_track from raceforge.track.clearance import compute_walkable_manifold from raceforge.track.solver import GeodesicSolver def main() -> None: seed = 513 print("=" * 65) print(f" Rendering Papercraft Minimal Test Video (Seed {seed})") print("=" * 65) out_dir = Path("out") out_dir.mkdir(parents=True, exist_ok=True) mp4_path = out_dir / "papercraft_hydraulic_test.mp4" wav_path = out_dir / "papercraft_hydraulic_test.wav" director = ProceduralDirector() directive = director.direct(seed) directive.theme = "paper" print("\n[1/4] Building track and simulating physics...") spec, grid = build_track(seed, directive) spec.countdown_s = 0.0 # Direct start spec.flood_wall_enabled = True spec.flood_wall_duration_s = 45.0 goal_cell = grid.world_to_cell( (spec.track.goal_tile[0] + spec.track.goal_tile[2]) / 2.0, (spec.track.goal_tile[1] + spec.track.goal_tile[3]) / 2.0, ) assert goal_cell is not None walkable = compute_walkable_manifold(grid, spec.physics.racer_size) solver = GeodesicSolver(grid, walkable, goal_cell) engine = SimulationEngine(spec, grid, solver, fps=60, max_duration_s=70.0) timeline = engine.run() outcome = timeline.outcome winner_name = outcome.winner.value.upper() if outcome.winner else "NONE" dur_s = outcome.total_duration_s print(f" • Duration: {dur_s:.2f}s ({timeline.total_frames} frames @ 60 FPS)") print(f" • Winner: {winner_name}") print(f" • Eliminated racers: {len(outcome.eliminated)} ({[r.value for r in outcome.eliminated]})") print(f" • Survivors: {len(spec.racers) - len(outcome.eliminated)}") print(f" • Total Bounces: {outcome.total_bounces}") print("\n[2/4] Synthesizing pentatonic audio with exact contact synchronization...") synth = AudioSynthesizer(spec, timeline) synth.synthesize(wav_path) print(f" ✓ Audio synthesized: {wav_path} ({wav_path.stat().st_size / 1024:.1f} KB)") print("\n[3/4] Rendering 1080x1920 60 FPS video frames with Cairo...") t0 = time.time() renderer = CairoFrameRenderer(spec, grid, timeline) encode_full_mp4( renderer.iter_raw_bgra_frames(), wav_path, mp4_path, width=1080, height=1920, fps=60, ) render_s = time.time() - t0 print(f" ✓ Video encoded in {render_s:.1f}s ({timeline.total_frames / max(0.1, render_s):.1f} fps)") print(f" ✓ Output size: {mp4_path.stat().st_size / 1024 / 1024:.2f} MB") print("\n[4/4] Extracting high-resolution preview frames...") frames_to_extract = [ ("frame_1_t1s.png", 1.0), ("frame_2_t12s.png", 12.0), ("frame_3_t24s.png", 24.0), ("frame_4_t35s.png", 35.0), ("frame_5_finish.png", max(1.0, dur_s - 0.5)), ] for fname, ts in frames_to_extract: fpath = out_dir / fname cmd = [ "ffmpeg", "-y", "-ss", f"{ts:.2f}", "-i", str(mp4_path), "-frames:v", "1", "-q:v", "2", str(fpath), ] subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) print(f" ✓ Extracted preview: {fpath.name} at t={ts:.1f}s") print("\nDone! All test assets generated successfully.") if __name__ == "__main__": main()