Mercurial > codedump
annotate create.py @ 133:0d8eabdd12ab default tip
create: write H:MM:SS timestamps, add option to fill with gaussian-blur instead of black
many albums are longer than one hour so writing H:MM:SS is a
necessity. if anything there will just be verbose info that
isn't important for my use-case.
however the gaussian-blur is simply broken. It works, and it plays
locally just fine, but YouTube in particular elongates the video
to fit the full width. I'm not entirely sure why it does this, but
it makes it useless and ugly.
| author | Paper <paper@tflc.us> |
|---|---|
| date | Sat, 03 Jan 2026 20:25:38 -0500 |
| parents | 71df0cf3aa05 |
| 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 | |
| 9 | |
|
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
|
10 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
|
11 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
|
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"] |
|
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 |
|
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 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
|
15 |
| 132 | 16 # Build filter string |
| 17 def build_filter_string(length: int, cover: bool) -> str: | |
| 18 f = "" | |
| 19 i = 1 if cover else 0 | |
| 20 for x in range(length): | |
| 21 f += ("[%d:0]" % (x + i)) | |
| 22 f += ("concat=n=%d:v=0:a=1[out]" % length) | |
| 23 return f | |
| 24 | |
|
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
|
25 def build_arguments_list(audio_files: list[str], cover, output, blur) -> list: |
| 132 | 26 args = ["ffmpeg", "-y"] |
| 27 if not cover is None: | |
| 28 args.extend(["-r", "1", "-loop", "1", "-i", cover]) | |
| 29 # I want you to play that song again | |
| 30 for f in audio_files: | |
| 31 args.extend(["-i", f]) | |
| 32 args.extend(["-filter_complex", build_filter_string(len(audio_files), False if cover is None else True), "-map", "[out]"]) | |
| 33 if not cover is None: | |
| 34 # Letterbox to 1920x1080 | |
|
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
|
35 args.extend(["-map", "0:v", "-tune", "stillimage", "-c:v", "libx264"]) |
|
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
|
36 args.extend(build_vf(blur)) |
| 132 | 37 args.extend(["-shortest"]) |
| 38 args.append(output + ".mkv") | |
| 39 return args | |
| 40 | |
| 41 | |
| 42 # Returns a Popen object that should be waited on | |
|
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
|
43 def run_ffmpeg(audio_files: list[str], cover, output, blur) -> subprocess.Popen: |
|
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
|
44 args = build_arguments_list(audio_files, cover, output, blur) |
| 132 | 45 print(args) |
| 46 return subprocess.Popen(args) | |
| 47 | |
| 48 | |
| 49 # Returns a Popen object that should be waited on. | |
| 50 # stdout is also piped... | |
| 51 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) | |
| 53 | |
| 54 | |
| 55 def get_title_from_tags(j, default: str) -> str: | |
| 56 # Is there an easier way to do this...? | |
| 57 for tag in j: | |
| 58 if tag.lower() == "title": | |
| 59 return j[tag] | |
| 60 | |
| 61 # Ugh | |
| 62 return default | |
| 63 | |
|
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
|
64 def doit(audio_files: list[str], cover, output, blur) -> int: |
| 132 | 65 ffprobes = [run_ffprobe(x) for x in audio_files] |
|
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
|
66 ffmpeg = run_ffmpeg(audio_files, cover, output, blur) |
| 132 | 67 |
| 68 # Wait for all ffprobe processes to finish their jobs | |
| 69 for f in ffprobes: | |
| 70 f.wait() | |
| 71 | |
| 72 # Iterate the list again, and add up all the times. | |
| 73 # This also accounts for milliseconds etc. to be | |
| 74 # more accurate | |
| 75 with open(output + ".txt", "w", encoding="utf-8") as fw: | |
| 76 dur = 0.0 | |
| 77 for f in ffprobes: | |
| 78 j = json.load(f.stdout) | |
|
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
|
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...."))) |
| 132 | 80 dur += float(j["format"]["duration"]) |
| 81 | |
| 82 # Finally, wait on the big ffmpeg process | |
| 83 ffmpeg.wait() | |
| 84 | |
| 85 def main() -> int: | |
| 86 parser = argparse.ArgumentParser(prog='create', description='Creates album videos', epilog='(c) paper 2025 all rights reversed') | |
| 87 parser.add_argument('-c', '--cover', required=True) | |
| 88 parser.add_argument('-o', '--output', required=True) | |
|
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
|
89 parser.add_argument('-b', '--blur', action='store_true') |
| 132 | 90 parser.add_argument('audio', action='extend', nargs='+', type=str) |
| 91 args = parser.parse_args() | |
|
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
|
92 doit(args.audio, args.cover, args.output, args.blur) |
| 132 | 93 |
| 94 if __name__ == "__main__": | |
| 95 exit(main()) |
