TheBird

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)}