Created
October 17, 2025 19:32
-
-
Save evorios/a37d64a3738637380a60ae5eb1f9edf7 to your computer and use it in GitHub Desktop.
Converting subtitles SRT to ASS for karaoke songs with lead-in
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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