comparison 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
comparison
equal deleted inserted replaced
135:0c3cd90e91f7 136:da4f7200665f
4 # track numbers etc. 4 # track numbers etc.
5 5
6 import argparse 6 import argparse
7 import json 7 import json
8 import subprocess 8 import subprocess
9 import os
10 import math #ceil
11 import typing
9 12
10 def build_vf(blur) -> list[str]: 13 def build_vf(blur) -> list[str]:
11 if blur: 14 if blur:
12 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"] 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"]
13 16
14 return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"] 17 return ["-vf", "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2"]
15 18
16 # Build filter string 19 # Build filter string
17 def build_filter_string(length: int, cover: bool) -> str: 20 def build_filter_string(length: int, offset: int) -> str:
18 f = "" 21 f = ""
19 i = 1 if cover else 0 22 i = offset
20 for x in range(length): 23 for x in range(length):
21 f += ("[%d:0]" % (x + i)) 24 f += ("[%d:0]" % (x + i))
22 f += ("concat=n=%d:v=0:a=1[out]" % length) 25 f += ("concat=n=%d:v=0:a=1[out]" % length)
23 return f 26 return f
24 27
25 def build_arguments_list(audio_files: list[str], cover, output, blur) -> list: 28 def build_arguments_list(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> list:
26 args = ["ffmpeg", "-y"] 29 args = ["ffmpeg", "-y"]
27 if not cover is None: 30 # I want you to play that song again
28 args.extend(["-r", "1", "-loop", "1", "-i", cover]) 31 if not cover is None:
29 # I want you to play that song again 32 args.extend(["-r", "1", "-loop", "1", "-i", cover])
30 for f in audio_files: 33 for f in audio_files:
31 args.extend(["-i", f]) 34 args.extend(["-i", f])
32 args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"]) 35 args.extend(["-filter_complex", build_filter_string(len(audio_files), 1 if cover else 0), "-map", "[out]"])
33 if not cover is None: 36 if not cover is None:
34 # Letterbox to 1920x1080 37 # Letterbox to 1920x1080
35 args.extend(["-map", "0:v", "-tune", "stillimage", "-c:v", "libx264"]) 38 args.extend(["-map", "0:v",
36 args.extend(build_vf(blur)) 39 "-tune", "stillimage",
37 args.extend(["-shortest"]) 40 "-c:v", "libx264",
38 args.append(output + ".mkv") 41 # Add a second to account for inconsistencies in durations
39 return args 42 "-t", "%f" % (duration + 1.0)])
43 args.extend(build_vf(blur))
44 args.append(output + ".mkv")
45 return args
40 46
41 47
42 # Returns a Popen object that should be waited on 48 # Returns a Popen object that should be waited on
43 def run_ffmpeg(audio_files: list[str], cover, output, blur) -> subprocess.Popen: 49 def run_ffmpeg(audio_files: list[str], cover: typing.Optional[str], output: str, blur: bool, duration: float) -> subprocess.Popen:
44 args = build_arguments_list(audio_files, cover, output, blur) 50 args = build_arguments_list(audio_files, cover, output, blur, duration)
45 print(args) 51 print(args)
46 return subprocess.Popen(args) 52 return subprocess.Popen(args)
47 53
48 54
49 # Returns a Popen object that should be waited on. 55 # Returns a Popen object that should be waited on.
50 # stdout is also piped... 56 # stdout is also piped...
51 def run_ffprobe(audio_file: str) -> subprocess.Popen: 57 def run_ffprobe(audio_file: str) -> subprocess.Popen:
52 return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True) 58 return subprocess.Popen(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", audio_file], stdout=subprocess.PIPE, text=True)
53 59
54 60
55 def get_title_from_tags(j, default: str) -> str: 61 def get_title_from_tags(j: list, default: str) -> str:
56 # Is there an easier way to do this...? 62 # Is there an easier way to do this...?
57 for tag in j: 63 for tag in j:
58 if tag.lower() == "title": 64 if tag.lower() == "title":
59 return j[tag] 65 return j[tag]
60 66
61 # Ugh 67 # Ugh
62 return default 68 return default
63 69
64 def doit(audio_files: list[str], cover, output, blur) -> int: 70 def doit(audio_files: list[str], cover, output, blur) -> int:
65 ffprobes = [run_ffprobe(x) for x in audio_files] 71 dur = 0.0
66 ffmpeg = run_ffmpeg(audio_files, cover, output, blur)
67 72
68 # Wait for all ffprobe processes to finish their jobs 73 ffprobes = [run_ffprobe(x) for x in audio_files]
69 for f in ffprobes:
70 f.wait()
71 74
72 # Iterate the list again, and add up all the times. 75 # Create output txt file with timestamps; we also use this duration
73 # This also accounts for milliseconds etc. to be 76 # to force ffmpeg to do the right thing instead of elongating our
74 # more accurate 77 # video
75 with open(output + ".txt", "w", encoding="utf-8") as fw: 78 with open(output + ".txt", "w", encoding="utf-8") as fw:
76 dur = 0.0 79 for f in ffprobes:
77 for f in ffprobes: 80 f.wait()
78 j = json.load(f.stdout) 81 j = json.load(f.stdout)
79 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...."))) 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....")))
80 dur += float(j["format"]["duration"]) 83 dur += float(j["format"]["duration"])
81 84
82 # Finally, wait on the big ffmpeg process 85 ffmpeg = run_ffmpeg(audio_files, cover, output, blur, dur)
83 ffmpeg.wait() 86
87 # Finally, wait on the big ffmpeg process
88 ffmpeg.wait()
84 89
85 def main() -> int: 90 def main() -> int:
86 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') 91 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed')
87 parser.add_argument('-c', '--cover', required=True) 92 parser.add_argument('-c', '--cover', required=True)
88 parser.add_argument('-o', '--output', required=True) 93 parser.add_argument('-o', '--output', required=True)
89 parser.add_argument('-b', '--blur', action='store_true') 94 parser.add_argument('-b', '--blur', action='store_true')
90 parser.add_argument('audio', action='extend', nargs='+', type=str) 95 parser.add_argument('audio', action='extend', nargs='+', type=str)
91 args = parser.parse_args() 96 args = parser.parse_args()
92 doit(args.audio, args.cover, args.output, args.blur) 97 doit(args.audio, args.cover, args.output, args.blur)
93 98
94 if __name__ == "__main__": 99 if __name__ == "__main__":
95 exit(main()) 100 exit(main())