Dup Ver Goto 📝

Make ASS Subtitles Example 1

To
298 lines, 888 words, 7825 chars Page 'MakeTimerSubtitesASS_1' does not exist.

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

#!/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]} <fn> [<fn> ...]")
    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

#!/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]} <fn> [<fn> ...]")
    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

[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