Mercurial > codedump
annotate create.py @ 136:da4f7200665f default tip
buncha shit
| author | Paper <paper@tflc.us> |
|---|---|
| date | Sat, 07 Mar 2026 18:04:10 -0500 |
| parents | 0d8eabdd12ab |
| children |
| rev | line source |
|---|---|
| 132 | 1 #!/usr/bin/env python3 |
| 2 # Simple python script to create a video from a bunch of wav files | |
| 3 # and an image. Also creates a cue-like track listing that contains | |
| 4 # track numbers etc. | |
| 5 | |
| 6 import argparse | |
| 7 import json | |
| 8 import subprocess | |
| 136 | 9 import os |
| 10 import math #ceil | |
| 11 import typing | |
| 132 | 12 |
|
133
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
13 def build_vf(blur) -> list[str]: |
|
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
14 if blur: |
|
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
15 return ["-filter_complex", "[0:v]split=2[bg][fg];[bg]scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,boxblur=luma_radius=10:chroma_radius=10[bg_final];[fg]scale=1920:1080:force_original_aspect_ratio=decrease[fg_final];[bg_final][fg_final]overlay=(W-w)/2:(H-h)/2"] |
|
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
16 |
|
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
17 return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"] |
|
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
18 |
| 132 | 19 # Build filter string |
| 136 | 20 def build_filter_string(length: int, offset: int) -> str: |
| 21 f = "" | |
| 22 i = offset | |
| 23 for x in range(length): | |
| 24 f += ("[%d:0]" % (x + i)) | |
| 25 f += ("concat=n=%d:v=0:a=1[out]" % length) | |
| 26 return f | |
| 132 | 27 |
| 136 | 28 def build_arguments_list(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> list: |
| 29 args = ["ffmpeg", "-y"] | |
| 30 # I want you to play that song again | |
| 31 if not cover is None: | |
| 32 args.extend(["-r", "1", "-loop", "1", "-i", cover]) | |
| 33 for f in audio_files: | |
| 34 args.extend(["-i", f]) | |
| 35 args.extend(["-filter_complex", build_filter_string(len(audio_files), 1 if cover else 0), "-map", "[out]"]) | |
| 36 if not cover is None: | |
| 37 # Letterbox to 1920x1080 | |
| 38 args.extend(["-map", "0:v", | |
| 39 "-tune", "stillimage", | |
| 40 "-c:v", "libx264", | |
| 41 # Add a second to account for inconsistencies in durations | |
| 42 "-t", "%f" % (duration + 1.0)]) | |
| 43 args.extend(build_vf(blur)) | |
| 44 args.append(output + ".mkv") | |
| 45 return args | |
| 132 | 46 |
| 47 | |
| 48 # Returns a Popen object that should be waited on | |
| 136 | 49 def run_ffmpeg(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> subprocess.Popen: |
| 50 args = build_arguments_list(audio_files, cover, output, blur, duration) | |
| 51 print(args) | |
| 52 return subprocess.Popen(args) | |
| 132 | 53 |
| 54 | |
| 55 # Returns a Popen object that should be waited on. | |
| 56 # stdout is also piped... | |
| 57 def run_ffprobe(audio_file: str) -> subprocess.Popen: | |
| 136 | 58 return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True) |
| 132 | 59 |
| 60 | |
| 136 | 61 def get_title_from_tags(j: list, default: str) -> str: |
| 62 # Is there an easier way to do this...? | |
| 63 for tag in j: | |
| 64 if tag.lower() == "title": | |
| 65 return j[tag] | |
| 132 | 66 |
| 136 | 67 # Ugh |
| 68 return default | |
| 132 | 69 |
|
133
0d8eabdd12ab
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
Paper <paper@tflc.us>
parents:
132
diff
changeset
|
70 def doit(audio_files: list[str], cover, output, blur) -> int: |
| 136 | 71 dur = 0.0 |
| 132 | 72 |
| 136 | 73 ffprobes = [run_ffprobe(x) for x in audio_files] |
| 132 | 74 |
| 136 | 75 # Create output txt file with timestamps; we also use this duration |
| 76 # to force ffmpeg to do the right thing instead of elongating our | |
| 77 # video | |
| 78 with open(output + ".txt", "w", encoding="utf-8") as fw: | |
| 79 for f in ffprobes: | |
| 80 f.wait() | |
| 81 j = json.load(f.stdout) | |
| 82 fw.write("%d:%02d:%02d %s\n" % (int(dur) // 3600, int(dur) % 3600 // 60, int(dur) % 60, get_title_from_tags(j["format"]["tags"], "OOPS...."))) | |
| 83 dur += float(j["format"]["duration"]) | |
| 132 | 84 |
| 136 | 85 ffmpeg = run_ffmpeg(audio_files, cover, output, blur, dur) |
| 86 | |
| 87 # Finally, wait on the big ffmpeg process | |
| 88 ffmpeg.wait() | |
| 132 | 89 |
| 90 def main() -> int: | |
| 136 | 91 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') |
| 92 parser.add_argument('-c', '--cover', required=True) | |
| 93 parser.add_argument('-o', '--output', required=True) | |
| 94 parser.add_argument('-b', '--blur', action='store_true') | |
| 95 parser.add_argument('audio', action='extend', nargs='+', type=str) | |
| 96 args = parser.parse_args() | |
| 97 doit(args.audio, args.cover, args.output, args.blur) | |
| 132 | 98 |
| 99 if __name__ == "__main__": | |
| 136 | 100 exit(main()) |
