| |
|
|
| import os |
| import subprocess |
|
|
| from pathlib import Path |
|
|
| from renderer import render |
|
|
| from usage_tracker import ( |
| build_usage_map, |
| consume_scene, |
| ) |
|
|
| from config import ( |
| OUTPUT_DIR, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def chunk_list( |
| items, |
| size |
| ): |
|
|
| for i in range( |
| 0, |
| len(items), |
| size |
| ): |
|
|
| yield items[ |
| i:i+size |
| ] |
|
|
|
|
| |
| |
| |
|
|
| def build_part_path( |
| index |
| ): |
|
|
| return ( |
|
|
| OUTPUT_DIR |
|
|
| / |
|
|
| f"part_{index:03d}.mp4" |
|
|
| ) |
|
|
|
|
| |
| |
| |
|
|
| def render_parts( |
| timeline, |
| batch_size=50 |
| ): |
|
|
| if not timeline: |
| raise ValueError( |
| "Timeline is empty" |
| ) |
|
|
| usage_map = build_usage_map( |
| timeline |
| ) |
|
|
| part_files = [] |
|
|
| batches = list( |
|
|
| chunk_list( |
| timeline, |
| batch_size |
| ) |
|
|
| ) |
|
|
| total_batches = len( |
| batches |
| ) |
|
|
| print() |
|
|
| print( |
| f"[Batch] " |
| f"Total Batches: " |
| f"{total_batches}" |
| ) |
|
|
| print() |
|
|
| for idx, batch in enumerate( |
| batches, |
| start=1 |
| ): |
|
|
| part_path = build_part_path( |
| idx |
| ) |
|
|
| print( |
| f"[Batch] " |
| f"Rendering " |
| f"{idx}/{total_batches}" |
| ) |
|
|
| render( |
| batch, |
| part_path |
| ) |
|
|
| part_files.append( |
| str(part_path) |
| ) |
|
|
| |
| |
| |
|
|
| deleted = 0 |
|
|
| for scene in batch: |
|
|
| delete_file = consume_scene( |
| usage_map, |
| scene |
| ) |
|
|
| if not delete_file: |
| continue |
|
|
| try: |
|
|
| if os.path.exists( |
| scene["path"] |
| ): |
|
|
| os.remove( |
| scene["path"] |
| ) |
|
|
| deleted += 1 |
|
|
| except Exception as e: |
|
|
| print( |
| f"[Cleanup] " |
| f"{scene['path']}" |
| ) |
|
|
| print(e) |
|
|
| print( |
| f"[Batch] " |
| f"Deleted " |
| f"{deleted} clips" |
| ) |
|
|
| print() |
|
|
| return part_files |
|
|
|
|
| |
| |
| |
|
|
| def concat_parts( |
| part_files |
| ): |
|
|
| if not part_files: |
|
|
| raise ValueError( |
| "No part files" |
| ) |
|
|
| concat_txt = ( |
| OUTPUT_DIR |
| / "parts.txt" |
| ) |
|
|
| with open( |
| concat_txt, |
| "w", |
| encoding="utf-8" |
| ) as f: |
|
|
| for part in part_files: |
|
|
| path = ( |
| Path(part) |
| .resolve() |
| .as_posix() |
| ) |
|
|
| f.write( |
| f"file '{path}'\n" |
| ) |
|
|
| final_video = ( |
| OUTPUT_DIR |
| / "video_only.mp4" |
| ) |
|
|
| cmd = [ |
|
|
| "ffmpeg", |
| "-y", |
|
|
| "-f", |
| "concat", |
|
|
| "-safe", |
| "0", |
|
|
| "-i", |
| str(concat_txt), |
|
|
| "-c", |
| "copy", |
|
|
| str(final_video) |
|
|
| ] |
|
|
| subprocess.run( |
| cmd, |
| check=True, |
| capture_output=True |
| ) |
|
|
| |
| |
| |
|
|
| for part in part_files: |
|
|
| try: |
|
|
| os.remove( |
| part |
| ) |
|
|
| except: |
| pass |
|
|
| try: |
|
|
| os.remove( |
| concat_txt |
| ) |
|
|
| except: |
| pass |
|
|
| return str( |
| final_video |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def render_final_video( |
| timeline, |
| batch_size=50 |
| ): |
|
|
| parts = render_parts( |
| timeline, |
| batch_size |
| ) |
|
|
| return concat_parts( |
| parts |
| ) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
|
|
| timeline = [ |
|
|
| { |
| "path": "clips/1_A.mp4", |
| "duration": 7.0 |
| }, |
|
|
| { |
| "path": "clips/2_A.mp4", |
| "duration": 7.0 |
| }, |
|
|
| { |
| "path": "clips/3_A.mp4", |
| "duration": 7.0 |
| }, |
|
|
| ] |
|
|
| output = render_final_video( |
| timeline, |
| batch_size=2 |
| ) |
|
|
| print() |
|
|
| print( |
| "Output:", |
| output |
| ) |