video.py
3.6 kB · python · 89 lines
1import glob2import os3import subprocess4from config import CRF, WEB, WEB_MIN, FORMAT, FPS, FRAMES_DIR, FREEZE_DURATION, HEATMAP_DIR, HEATMAP_FPS, INTER_SEGMENT_FREEZE, MASKS_DIR, MASTER, PRESET, RATE, SIZE5from frames import flash_count, mask_path6from music import compose_saga_audio78# FFMPEG910def run_ffmpeg(ffmpeg, args):11 done = subprocess.run([ffmpeg, "-y", "-hide_banner", "-loglevel", "error", *args], capture_output=True, text=True)12 if done.returncode != 0:13 raise RuntimeError(f"ffmpeg failed: {done.stderr.strip()}")1415# CONCAT1617def _entries(frame_dir, fps, outer_freeze, boundary_freeze, segment_lengths, masks=None):18 frames = sorted(glob.glob(f"{frame_dir}/*.{FORMAT}"))[:sum(segment_lengths)]19 if not frames:20 return []21 entries = [] if masks else [(frames[0], outer_freeze)]22 cursor = 023 for seg_i, seg_len in enumerate(segment_lengths):24 if masks:25 for _ in range(masks[seg_i][1]):26 entries.append((frames[cursor], 1.0 / fps))27 entries.append((masks[seg_i][0], 1.0 / fps))28 for path in frames[cursor:cursor + seg_len]:29 entries.append((path, 1.0 / fps))30 if seg_i < len(segment_lengths) - 1:31 entries.append((frames[cursor + seg_len - 1], boundary_freeze))32 cursor += seg_len33 entries.append((frames[-1], outer_freeze))34 return entries3536def _write_concat(entries, path):37 with open(path, "w") as handle:38 for frame, duration in entries:39 handle.write(f"file '{os.path.abspath(frame)}'\n")40 handle.write(f"duration {duration}\n")41 handle.write(f"file '{os.path.abspath(entries[-1][0])}'\n")42 return path4344def _steps(entries):45 steps = []46 clock = 0.047 last = None48 for frame, duration in entries:49 if frame != last:50 steps.append(round(clock, 4))51 last = frame52 clock += duration53 return steps5455# SIZE5657def web_size(canvas):58 scale = -(-WEB_MIN // canvas)59 if canvas * scale % 2:60 scale += 161 return canvas * scale6263# ENCODE6465VIDEO = ["-c:v", "libx264", "-crf", str(CRF), "-preset", PRESET, "-pix_fmt", "yuv420p", "-movflags", "+faststart"]66AUDIO = ["-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-af", "afade=t=in:st=0:d=0.25,areverse,afade=t=in:st=0:d=0.25,areverse"]6768def _encode(ffmpeg, concat, audio, frames, size, output):69 args = ["-f", "concat", "-safe", "0", "-i", concat, "-i", audio, "-frames:v", str(frames)]70 args += ["-vf", f"scale={size}:{size}:flags=neighbor", "-r", str(RATE)]71 run_ffmpeg(ffmpeg, args + VIDEO + AUDIO + [output])72 return output7374# SAGA VIDEOS7576def create_saga_videos(saga, work_dir, out_dir, ffmpeg):77 os.makedirs(out_dir, exist_ok=True)78 audio = compose_saga_audio(saga, f"{work_dir}/{saga.key}.wav")79 masks = [(mask_path(f"{work_dir}/{MASKS_DIR}", saga.key, i), flash_count(seg)) for i, seg in enumerate(saga.segments)]80 entries = _entries(f"{work_dir}/{FRAMES_DIR}", FPS, FREEZE_DURATION, INTER_SEGMENT_FREEZE, saga.segment_lengths, masks)81 entries += _entries(f"{work_dir}/{HEATMAP_DIR}", HEATMAP_FPS, FREEZE_DURATION, INTER_SEGMENT_FREEZE, saga.segment_lengths)82 concat = _write_concat(entries, f"{work_dir}/concat.txt")83 duration = round(sum(duration for _, duration in entries), 3)84 frames = round(duration * RATE)85 size = web_size(int(saga.grids[0].types.shape[0]))86 _encode(ffmpeg, concat, audio, frames, SIZE, f"{out_dir}/{saga.key}{MASTER}")87 _encode(ffmpeg, concat, audio, frames, size, f"{out_dir}/{saga.key}{WEB}")88 print(f"videos {duration} s, {frames} frames, web {size} px -> {out_dir}")89 return {"duration": duration, "frames": frames, "size": size, "steps": _steps(entries)}