| |
|
|
| import subprocess |
| from pathlib import Path |
|
|
| from config import ( |
| TEMP_DIR, |
| OUTPUT_DIR, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def build_concat_file( |
| timeline, |
| concat_file=None |
| ): |
| """ |
| timeline: |
| [ |
| { |
| "path": "clips/1_A.mp4", |
| "duration": 7.1, |
| ... |
| } |
| ] |
| """ |
|
|
| if concat_file is None: |
| concat_file = ( |
| TEMP_DIR / "concat.txt" |
| ) |
|
|
| concat_file = Path(concat_file) |
|
|
| with open( |
| concat_file, |
| "w", |
| encoding="utf-8" |
| ) as f: |
|
|
| for scene in timeline: |
|
|
| clip_path = ( |
| Path(scene["path"]) |
| .resolve() |
| .as_posix() |
| ) |
|
|
| f.write( |
| f"file '{clip_path}'\n" |
| ) |
|
|
| return str(concat_file) |
|
|
|
|
| |
| |
| |
|
|
| def estimate_duration( |
| timeline |
| ): |
|
|
| total = 0 |
|
|
| for scene in timeline: |
| total += scene["duration"] |
|
|
| return round(total, 2) |
|
|
|
|
| |
| |
| |
|
|
| def render_timeline( |
| timeline, |
| output_file=None |
| ): |
| """ |
| Fast concat mode. |
| |
| Semua clip HARUS: |
| - 1280x720 |
| - 30fps |
| - h264 |
| - yuv420p |
| - no audio |
| """ |
|
|
| if not timeline: |
| raise ValueError( |
| "Timeline is empty" |
| ) |
|
|
| if output_file is None: |
|
|
| output_file = ( |
| OUTPUT_DIR |
| / "video_only.mp4" |
| ) |
|
|
| output_file = Path(output_file) |
|
|
| concat_file = build_concat_file( |
| timeline |
| ) |
|
|
| cmd = [ |
| "ffmpeg", |
| "-y", |
|
|
| "-f", |
| "concat", |
|
|
| "-safe", |
| "0", |
|
|
| "-i", |
| concat_file, |
|
|
| "-c", |
| "copy", |
|
|
| str(output_file) |
| ] |
|
|
| subprocess.run( |
| cmd, |
| check=True, |
| capture_output=True |
| ) |
|
|
| return str(output_file) |
|
|
|
|
| |
| |
| |
|
|
| def render_timeline_safe( |
| timeline, |
| output_file=None |
| ): |
| """ |
| Gunakan jika concat copy gagal. |
| |
| Lebih lambat, |
| tetapi paling kompatibel. |
| """ |
|
|
| if not timeline: |
| raise ValueError( |
| "Timeline is empty" |
| ) |
|
|
| if output_file is None: |
|
|
| output_file = ( |
| OUTPUT_DIR |
| / "video_only.mp4" |
| ) |
|
|
| output_file = Path(output_file) |
|
|
| concat_file = build_concat_file( |
| timeline |
| ) |
|
|
| cmd = [ |
| "ffmpeg", |
| "-y", |
|
|
| "-f", |
| "concat", |
|
|
| "-safe", |
| "0", |
|
|
| "-i", |
| concat_file, |
|
|
| "-c:v", |
| "libx264", |
|
|
| "-preset", |
| "ultrafast", |
|
|
| "-crf", |
| "28", |
|
|
| "-pix_fmt", |
| "yuv420p", |
|
|
| str(output_file) |
| ] |
|
|
| subprocess.run( |
| cmd, |
| check=True, |
| capture_output=True |
| ) |
|
|
| return str(output_file) |
|
|
|
|
| |
| |
| |
|
|
| def render( |
| timeline, |
| output_file=None |
| ): |
| """ |
| Coba fast mode dulu. |
| Jika gagal, fallback ke safe mode. |
| """ |
|
|
| try: |
|
|
| return render_timeline( |
| timeline, |
| output_file |
| ) |
|
|
| except Exception as e: |
|
|
| print( |
| "[Renderer] Fast concat failed:" |
| ) |
|
|
| print(e) |
|
|
| print( |
| "[Renderer] Falling back to re-encode..." |
| ) |
|
|
| return render_timeline_safe( |
| timeline, |
| output_file |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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 |
| }, |
|
|
| ] |
|
|
| print( |
| "Estimated:", |
| estimate_duration( |
| timeline |
| ) |
| ) |
|
|
| result = render( |
| timeline |
| ) |
|
|
| print( |
| "Output:", |
| result |
| ) |