Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """
- convert_srt_to_ass.py
- Converts an .srt subtitle to .ass with:
- - Spoken dialogue (normal lines) at bottom
- - On-screen captions (lines enclosed in parentheses) above the dialogue area
- - Overlapping lines stacked vertically (not drawn on top of each other)
- Usage:
- python convert_srt_to_ass.py input.srt [output.ass]
- """
- import re
- import sys
- def srt_time_to_seconds(t):
- """Convert SRT time 'HH:MM:SS,ms' to seconds (float)."""
- h, m, s_ms = t.split(':')
- s, ms = s_ms.split(',')
- return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
- def seconds_to_ass_time(seconds):
- """Convert seconds (float) to ASS time format h:mm:ss.cc (centiseconds)."""
- total_cs = int(round(seconds * 100))
- cs = total_cs % 100
- total_seconds = total_cs // 100
- s = total_seconds % 60
- total_minutes = total_seconds // 60
- m = total_minutes % 60
- h = total_minutes // 60
- return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
- def parse_srt(path):
- """Parse an SRT file into a list of (start_seconds, end_seconds, text)."""
- with open(path, 'r', encoding='utf-8-sig') as f:
- raw = f.read()
- raw = raw.replace('\r\n', '\n').replace('\r', '\n')
- blocks = re.split(r'\n\s*\n', raw.strip())
- subs = []
- time_re = re.compile(r'(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})')
- for block in blocks:
- lines = block.strip().split('\n')
- if len(lines) < 2:
- continue
- for i, ln in enumerate(lines):
- if '-->' in ln:
- time_line = ln
- text_lines = lines[i+1:]
- break
- else:
- continue
- m = time_re.search(time_line)
- if not m:
- continue
- start_s = srt_time_to_seconds(m.group(1))
- end_s = srt_time_to_seconds(m.group(2))
- text = '\n'.join(text_lines).strip()
- subs.append((start_s, end_s, text))
- return subs
- def is_caption(text):
- """Return True if the entire text is enclosed in parentheses."""
- t = text.strip()
- return bool(re.match(r'^\(.*\)$', t, re.DOTALL))
- def assign_tracks(items):
- """Assign each (start, end, text) to a track index so overlapping items stack vertically."""
- tracks = []
- assigned = []
- for start, end, text in sorted(items, key=lambda x: x[0]):
- for idx, last_end in enumerate(tracks):
- if start >= last_end - 1e-6:
- tracks[idx] = end
- assigned.append(((start, end, text), idx))
- break
- else:
- tracks.append(end)
- assigned.append(((start, end, text), len(tracks)-1))
- return assigned, len(tracks)
- def generate_ass(subs, out_path, playresx=1920, playresy=1080):
- voice = []
- caption = []
- for s, e, t in subs:
- if is_caption(t):
- caption.append((s, e, t))
- else:
- voice.append((s, e, t))
- voice_assigned, voice_track_count = assign_tracks(voice)
- caption_assigned, caption_track_count = assign_tracks(caption)
- # Base positions
- base_voice_margin = 40
- line_height = 56
- base_caption_margin = base_voice_margin + (voice_track_count * line_height) + 10
- header = [
- "[Script Info]",
- "Title: Converted from SRT",
- "ScriptType: v4.00+",
- "Collisions: Normal",
- f"PlayResX: {playresx}",
- f"PlayResY: {playresy}",
- "WrapStyle: 2",
- "ScaledBorderAndShadow: yes",
- "",
- "[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",
- # Font sizes increased to 55 for both
- "Style: Voice, Arial, 55, &H00FFFFFF, &H000000FF, &H00000000, &H00000000, "
- "0, 0, 0, 0, 100, 100, 0, 0, 1, 3, 0, 2, 10, 10, 40, 1",
- "Style: Caption, Arial, 55, &H00FFFF99, &H000000FF, &H00000000, &H00000000, "
- "0, 0, 0, 0, 100, 100, 0, 0, 1, 3, 0, 2, 10, 10, 200, 1",
- "",
- "[Events]",
- "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
- ]
- events = []
- # Voice subtitles
- for (start, end, text), track in voice_assigned:
- margin_v = base_voice_margin + track * line_height
- text_esc = text.replace('\r', '').replace('\n', r'\N')
- events.append(
- f"Dialogue: 0,{seconds_to_ass_time(start)},{seconds_to_ass_time(end)},Voice,,0,0,{margin_v},,{text_esc}"
- )
- # Captions just above voice area
- for (start, end, text), track in caption_assigned:
- margin_v = base_caption_margin + track * line_height
- text_esc = text.replace('\r', '').replace('\n', r'\N')
- events.append(
- f"Dialogue: 0,{seconds_to_ass_time(start)},{seconds_to_ass_time(end)},Caption,,0,0,{margin_v},,{text_esc}"
- )
- with open(out_path, 'w', encoding='utf-8-sig') as f:
- f.write('\n'.join(header) + '\n' + '\n'.join(events) + '\n')
- print(f"✅ Converted: {out_path}")
- print(f" - voice tracks: {voice_track_count}, caption tracks: {caption_track_count}")
- def main():
- if len(sys.argv) < 2:
- print("Usage: python convert_srt_to_ass.py input.srt [output.ass]")
- sys.exit(1)
- input_srt = sys.argv[1]
- output_ass = sys.argv[2] if len(sys.argv) > 2 else re.sub(r'\.srt$', '.ass', input_srt, flags=re.I)
- subs = parse_srt(input_srt)
- if not subs:
- print("No subtitles found or failed to parse the SRT.")
- sys.exit(2)
- generate_ass(subs, output_ass)
- if __name__ == '__main__':
- main()
Advertisement