tags: python media ffmpeg title: Make ASS Subtitles Example 1 All these generate files with resolution `960x540`. The intent is to have something to watch in a VR headset while playing piano scales, as an exercise in weaning myself off the habit of watching my hands, and consequently compromising posture. Set `MERGE=y` to use ffmpeg, else it will simply generate the subtitles files. The purpose of the timer is so that I can e.g. practise 5 mins LH, 5 mins RH, 5 mins hands together, etc. # 16:9 version ```py #!/usr/bin/env python ''' We can use \\fs to change font size, and \\c to change colour. So we can have white 60px text most of the time, and then change each second approaching a 5m: ''' import sys import os import math merge = os.getenv("MERGE","n").lower() in ["y","yes","true","1"] from subprocess import run, DEVNULL import json def getvinfo(fn): fn = str(fn) m = run(["ffprobe","-print_format","json","-show_streams","-show_format",fn],capture_output=True,stdin=DEVNULL) if m.returncode != 0: print(f"ffprobe on {fn} returned {m.returncode}") return None return json.loads(m.stdout.decode()) def render(ifn,sfn,ofn,vwidth,vheight,opts=[]): cmd = ["ffmpeg","-i",ifn,"-vf",f"scale={vwidth}:{vheight},subtitles={sfn}","-aspect",f"{vwidth}:{vheight}"]+opts+[ofn] m = run(cmd,stdin=DEVNULL) rc = m.returncode if rc != 0: print(f"ffmpeg returned",rc) return rc def main(): args = sys.argv[1:] if len(args) == 0: args = ["gm.mp4"] if len(args) == 0: print(f"{sys.argv[0]} [ ...]") exit() for fn in args: proc(fn) def proc(fn): if not os.path.isfile(fn): print(f"File {s} does not exist or is not a file.") return try: info = getvinfo(fn) except Exception as e: print(f"Exception {e} ({type(e)}) getting video info for {fn}") return sfn = f"{fn}.timer.ass" fmt = info['format'] sts = info['streams'] vids = None for s in sts: if s['codec_type'] == "video": vids = s break else: print(f"No video stream in {fn}") return dur = math.ceil(float(fmt['duration'])) out = makeout(dur,960,540) out = "\n".join(out) with open(sfn,"wt") as f: print(out,file=f) print("Written",sfn) ofn = f"s_{fn}" if merge: render(fn,sfn,ofn,960,540,["-b:a","128k","-c:v","h264_nvenc","-b:v","1M"]) def makeout(dur,vwidth,vheight): x = int(vwidth*1/3) y = int(vheight*7/8) fsb = int(vheight/7) out = [f"""[Script Info] PlayResY: {vheight} PlayResX: {vwidth} WrapStyle: 1 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, Bold, BorderStyle, Outline, Shadow Style: Timer, Optima, {fsb}, &H00FFFFFF, -1, 1, 2, 2 [Events] Format: Layer, Start, End, Style, Text"""] vh, vm, vs = tohms(dur) leadin = 10 leadout = 5 period = 300 for t in range(dur): tmod = t % period #print(tmod,leadout,leadin,period-leadin) c = "" f = "" h,m,s = tohms(t) t0 = f"{h:02d}:{m:02d}:{s:02d}.00" td = f"{m:02d}:{s:02d}" if dur >= 3600: td = f"{h:02d}:{td}" h,m,s = tohms(t+1) t1 = f"{h:02d}:{m:02d}:{s:02d}.00" if tmod < leadout: tt = 5 - tmod fs = fsb + 10*tt n = int((tmod/leadout)*255.0) print(t,tmod,n) f = f"{{\\fs{fs}}}" c = f"{{\\c{n:02X}FF{n:02X}}}" if tmod > (period-leadin): tt = tmod - (period - leadin) ts = 10 - tt fs = fsb + 5*tt n = int((ts/leadin)*255.0) print(t,tt,tmod,n) f = f"{{\\fs{fs}}}" c = f"{{\\c{n:02X}FF{n:02X}}}" event = f"Dialogue: 0,{t0},{t1},Timer, {{\\pos({x},{y})}}{f}{c}{td}" out.append(event) return out def tohms(x): h, x = divmod(x,3600) m, s = divmod(x,60) return h,m,s if __name__ == "__main__": main() ``` # 4:3 version ```py #!/usr/bin/env python import sys import os import math merge = os.getenv("MERGE","n").lower() in ["y","yes","true","1"] from subprocess import run, DEVNULL import json def getvinfo(fn): fn = str(fn) m = run(["ffprobe","-print_format","json","-show_streams","-show_format",fn],capture_output=True,stdin=DEVNULL) if m.returncode != 0: print(f"ffprobe on {fn} returned {m.returncode}") return None return json.loads(m.stdout.decode()) def render(ifn,sfn,ofn,opts=[]): cmd = ["ffmpeg","-i",ifn,"-vf",f"scale=676:540,pad=width=960:height=540:x=142:y=0,subtitles={sfn}","-aspect",f"960:540"]+opts+[ofn] m = run(cmd,stdin=DEVNULL) rc = m.returncode if rc != 0: print(f"ffmpeg returned",rc) return rc def main(): args = sys.argv[1:] if len(args) == 0: args = ["gm.mp4"] if len(args) == 0: print(f"{sys.argv[0]} [ ...]") exit() for fn in args: proc(fn) def proc(fn): if not os.path.isfile(fn): print(f"File {s} does not exist or is not a file.") return try: info = getvinfo(fn) except Exception as e: print(f"Exception {e} ({type(e)}) getting video info for {fn}") return sfn = f"{fn}.timer.ass" fmt = info['format'] sts = info['streams'] vids = None for s in sts: if s['codec_type'] == "video": vids = s break else: print(f"No video stream in {fn}") return dur = math.ceil(float(fmt['duration'])) out = makeout(dur,960,540) out = "\n".join(out) with open(sfn,"wt") as f: print(out,file=f) print("Written",sfn) ofn = f"s_{fn}" #render(fn,sfn,ofn,960:540,["-b:a","128k","-c:v"," if merge: render(fn,sfn,ofn,["-b:a","128k","-c:v","h264_nvenc","-b:v","1M","-t","300"]) def makeout(dur,vwidth,vheight): x = int(vwidth*1/3) y = int(vheight*7/8) fsb = int(vheight/7) out = [f"""[Script Info] PlayResY: {vheight} PlayResX: {vwidth} WrapStyle: 1 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, Bold, BorderStyle, Outline, Shadow Style: Timer, Optima, {fsb}, &H00FFFFFF, -1, 1, 2, 2 [Events] Format: Layer, Start, End, Style, Text"""] vh, vm, vs = tohms(dur) leadin = 10 leadout = 5 period = 300 for t in range(dur): tmod = t % period #print(tmod,leadout,leadin,period-leadin) c = "" f = "" h,m,s = tohms(t) t0 = f"{h:02d}:{m:02d}:{s:02d}.00" td = f"{m:02d}:{s:02d}" if dur >= 3600: td = f"{h:02d}:{td}" h,m,s = tohms(t+1) t1 = f"{h:02d}:{m:02d}:{s:02d}.00" if tmod < leadout: tt = 5 - tmod fs = fsb + 10*tt n = int((tmod/leadout)*255.0) print(t,tmod,n) f = f"{{\\fs{fs}}}" c = f"{{\\c{n:02X}FF{n:02X}}}" if tmod > (period-leadin): tt = tmod - (period - leadin) ts = 10 - tt fs = fsb + 5*tt n = int((ts/leadin)*255.0) print(t,tt,tmod,n) f = f"{{\\fs{fs}}}" c = f"{{\\c{n:02X}FF{n:02X}}}" event = f"Dialogue: 0,{t0},{t1},Timer, {{\\pos({x},{y})}}{f}{c}{td}" out.append(event) return out def tohms(x): h, x = divmod(x,3600) m, s = divmod(x,60) return h,m,s if __name__ == "__main__": main() ``` # Example Subtitles Files ```plaintext [Script Info] PlayResY: 540 PlayResX: 960 WrapStyle: 1 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, Bold, BorderStyle, Outline, Shadow Style: Timer, Optima, 77, &H00FFFFFF, -1, 1, 2, 2 [Events] Format: Layer, Start, End, Style, Text Dialogue: 0,00:00:00.00,00:00:01.00,Timer, {\pos(320,472)}{\fs127}{\c00FF00}00:00 Dialogue: 0,00:00:01.00,00:00:02.00,Timer, {\pos(320,472)}{\fs117}{\c33FF33}00:01 Dialogue: 0,00:00:02.00,00:00:03.00,Timer, {\pos(320,472)}{\fs107}{\c66FF66}00:02 Dialogue: 0,00:00:03.00,00:00:04.00,Timer, {\pos(320,472)}{\fs97}{\c99FF99}00:03 Dialogue: 0,00:00:04.00,00:00:05.00,Timer, {\pos(320,472)}{\fs87}{\cCCFFCC}00:04 Dialogue: 0,00:00:05.00,00:00:06.00,Timer, {\pos(320,472)}00:05 Dialogue: 0,00:00:06.00,00:00:07.00,Timer, {\pos(320,472)}00:06 Dialogue: 0,00:00:07.00,00:00:08.00,Timer, {\pos(320,472)}00:07 ```