Skip to content

Instantly share code, notes, and snippets.

@evorios
Created October 17, 2025 19:32
Show Gist options
  • Select an option

  • Save evorios/a37d64a3738637380a60ae5eb1f9edf7 to your computer and use it in GitHub Desktop.

Select an option

Save evorios/a37d64a3738637380a60ae5eb1f9edf7 to your computer and use it in GitHub Desktop.
Converting subtitles SRT to ASS for karaoke songs with lead-in
import re
import sys
def get_ass_rounded_rectangle(width: int, height: int, radius: int) -> str:
if width == 0 or height == 0 or radius * 2 > width or radius * 2 > height:
return ''
bezier = radius * 0.414
return (
f"m {radius} 0 "
f"l {width - radius} 0 "
f"b {width - bezier} 0 {width} {bezier} {width} {radius} "
f"l {width} {height - radius} "
f"b {width} {height - bezier} {width - bezier} {height} {width - radius} {height} "
f"l {radius} {height} "
f"b {bezier} {height} 0 {height - bezier} 0 {height - radius} "
f"l 0 {radius} "
f"b 0 {bezier} {bezier} 0 {radius} 0"
)
def srt_time_to_ms(srt_time):
h, m, s_ms = srt_time.split(':')
s, ms = s_ms.split(',')
return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000 + int(ms)
def ms_to_ass_time(ms):
total_cent = int(ms // 10)
h = total_cent // 360000
m = (total_cent % 360000) // 6000
s = (total_cent % 6000) // 100
c = total_cent % 100
return f"{h}:{m:02d}:{s:02d}.{c:02d}"
def main(srt_path, ass_path, lead_in_ms=1500):
with open(srt_path, 'r', encoding='utf-8') as f:
content = f.read()
blocks = re.split(r'\n\s*\n', content.strip())
lines = []
for block in blocks:
parts = block.strip().split('\n')
if len(parts) < 3:
continue
try:
time_line = parts[1]
text = ' '.join(parts[2:]).strip()
if not text:
continue
start_str, end_str = time_line.split(' --> ')
start_ms = srt_time_to_ms(start_str)
end_ms = srt_time_to_ms(end_str)
lines.append({
'start_ms': start_ms,
'end_ms': end_ms,
'text': text
})
except Exception:
continue
with open(ass_path, 'w', encoding='utf-8') as f:
f.write("""[Script Info]
Title: Karaoke with Inline Lead-in Indicator
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.601
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00AAAAAA,&H00FFFF00,&H00000000,&H64000000,0,0,0,0,100,100,0,0,1,1.5,0,2,20,20,20,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
""")
for line in lines:
start_ms = line['start_ms']
end_ms = line['end_ms']
text = line['text']
duration_ms = end_ms - start_ms
if duration_ms <= 0 or not text.strip():
continue
words = text.split()
if not words:
continue
# Time to display the entire line - including lead-in
display_start = start_ms - lead_in_ms
display_end = end_ms
# --- Lead-in: rectangle as the first "segment" ---
rect_path = get_ass_rounded_rectangle(50, 16, 5)
lead_kf = max(1, lead_in_ms // 10)
lead_part = f"{{\\kf{lead_kf}}}{{\\p1}}{rect_path}{{\\p0}} " if rect_path else ""
# --- Words with \kf ---
word_duration_ms = duration_ms / len(words)
word_kf = max(1, int(word_duration_ms // 10))
word_parts = [f"{{\\kf{word_kf}}}{word}" for word in words]
words_text = " ".join(word_parts)
# Merge all together
full_text = lead_part + words_text
start_ass = ms_to_ass_time(display_start)
end_ass = ms_to_ass_time(display_end)
f.write(f"Dialogue: 0,{start_ass},{end_ass},Default,,0,0,0,,{full_text}\n")
print(f"✅ Done! The rectangle is inline, words with spaces. File: {ass_path}")
if __name__ == "__main__":
if len(sys.argv) not in (3, 4):
print("Usage: python karaoke_inline_rect.py input.srt output.ass [lead_in_ms]")
sys.exit(1)
srt_file = sys.argv[1]
ass_file = sys.argv[2]
lead = int(sys.argv[3]) if len(sys.argv) == 4 else 1500
main(srt_file, ass_file, lead)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment