""" subtitle_format_rules.py — Subtitle Format Rules Reference This module serves as the single source of truth for how each subtitle format is structured and which parts should be translated vs preserved vs skipped. Used by: - subtitle_parser.py for parsing/writing decisions - translation.py for system prompt context - app.py for user-facing format info Formats covered: SRT, ASS/SSA, VTT """ # ───────────────────────────────────────────────────────────────────────────── # ASS / SSA (.ass / .ssa) # ───────────────────────────────────────────────────────────────────────────── ASS_STRUCTURE = """ ASS file has 4 sections: [Script Info] → metadata (resolution, title, script type) [Aegisub Project Garbage] → editor state (video path, scroll pos) [V4+ Styles] → font/color/style definitions [Events] → the actual subtitle events Inside [Events]: Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Comment: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,Editor note here Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,Subtitle text here """ # Line types in [Events] ASS_LINE_TYPES = { "Dialogue": "TRANSLATE — displayed to viewer", "Comment": "SKIP — editor notes, NOT displayed to viewer", "Format": "SKIP — column header, not a subtitle", } # Style names that indicate non-translatable tracks ASS_SKIP_STYLES = { "EDR", # Japanese romaji (English Dialogue Romaji) "Romaji", # Japanese romanisation "JP", # Japanese original "Karaoke", # Karaoke timing track "KAR", # Karaoke abbreviation "Signs", # On-screen signs (often handled separately) } # Inline override tags — strip before AI, restore prefix after # Format: {one or more tag sequences at START of text} # e.g. {\\pos(960,540)\\fad(200,0)\\be5}Text here # Rule: extract leading {...} block, strip, translate, re-prepend ASS_INLINE_TAG_RULES = """ LEADING TAGS (restore after translation): {\\pos(x,y)} — screen position {\\fad(in,out)} — fade in/out timing {\\be5} — blur edges {\\an8} — alignment (8=top-center) {\\fnFontName} — font name override {\\fs60} — font size override {\\c&HRRGGBB&} — color (leading position) MID-TEXT TAGS (keep in text, don't translate): word{\\c&H0000FF&}word — inline color change mid-sentence These are rare; if present, pass to AI with instruction to preserve {...} LINE BREAK: \\N — hard line break (ASS standard) \\n — soft line break Both stripped for AI, \\N restored from Python \\n """ # ───────────────────────────────────────────────────────────────────────────── # SRT (.srt) # ───────────────────────────────────────────────────────────────────────────── SRT_STRUCTURE = """ SRT file structure (repeating blocks separated by blank lines): 1 ← sequential index (integer) 00:00:01,000 --> 00:00:03,000 ← timestamp (comma for ms, not period) Subtitle text line 1 ← text (translate this) Optional second line ← text continues until blank line 2 00:00:04,000 --> 00:00:06,000 Italic text """ SRT_LINE_TYPES = { "index": "SKIP — sequential integer only", "timestamp": "SKIP — contains '-->'", "text": "TRANSLATE — all other non-blank lines in a block", "blank": "SKIP — block separator", } SRT_TAG_RULES = """ HTML-STYLE TAGS (strip before AI): text — italic text — bold text — underline text — color (common in older SRTs) text — WebVTT class (rare in SRT) Rule: Strip all tags. SRT doesn't require tags restored (pysubs2 handles styling via the format layer). Only the text content needs translating. """ SRT_GOTCHAS = [ "Timestamp uses comma for milliseconds: 00:00:01,000 (NOT 00:00:01.000)", "Encoding often Windows-1252 (CP1252) for older files — handle with fallback", "UTF-8 BOM (\\ufeff) may be present — strip it", "Some files omit blank line between last block and EOF — handle gracefully", "Index numbers may restart or be non-sequential — pysubs2 handles this", "SDH (Subtitles for Deaf/Hard of Hearing): [MUSIC] [APPLAUSE] — translate as-is", ] # ───────────────────────────────────────────────────────────────────────────── # WebVTT (.vtt) # ───────────────────────────────────────────────────────────────────────────── VTT_STRUCTURE = """ WebVTT file structure: WEBVTT ← mandatory header (first line) ← optional description after WEBVTT NOTE This is a comment ← NOTE block — skip entirely ← blank line ends NOTE block STYLE ← CSS styling block — skip ::cue { color: white; } ← blank line ends STYLE block 00:00:01.000 --> 00:00:03.000 align:middle ← timestamp + optional settings Subtitle text ← translate cue-id-optional ← optional cue identifier (text before timestamp) 00:00:04.000 --> 00:00:06.000 Text here ← translate (strip tag) """ VTT_LINE_TYPES = { "WEBVTT": "SKIP — mandatory file header", "NOTE": "SKIP — comment block", "STYLE": "SKIP — CSS block", "REGION": "SKIP — region definition block", "timestamp": "SKIP — contains '-->'", "cue_id": "SKIP — line before timestamp (no '-->')", "text": "TRANSLATE", "blank": "SKIP — separator", } VTT_TAG_RULES = """ VOICE SPANS (strip tag, translate content): text → translate "text" SHOUTING → translate "SHOUTING" TIMESTAMP TAGS (strip entirely): <00:00:01.500> → remove CLASS SPANS (strip tag, translate content): text → translate "text" RUBY (Japanese furigana — strip entirely): 漢字かんじ → pass through or remove ALIGNMENT / POSITION in timestamp line (preserve): --> 00:00:03.000 align:start line:90% → preserve settings after timestamp """ VTT_GOTCHAS = [ "Milliseconds use PERIOD: 00:00:01.000 (NOT comma like SRT)", "Hours are optional: 00:01.000 is valid", "Cue settings after '-->' must be preserved (alignment, position, size)", "WEBVTT header line must remain as first line exactly", "NOTE blocks can span multiple lines until blank line", "pysubs2 handles VTT but may lose some cue settings — test output", ] # ───────────────────────────────────────────────────────────────────────────── # Common translation rules (all formats) # ───────────────────────────────────────────────────────────────────────────── COMMON_TRANSLATION_RULES = """ WHAT TO TRANSLATE: ✅ Dialogue / spoken text ✅ On-screen signs (if in a translatable style) ✅ [SOUND EFFECTS] in SDH subtitles — translate description ✅ Song lyrics in a translatable style (EDE, Default, etc.) WHAT TO SKIP (return original unchanged): ✗ Timestamps — NEVER send to AI ✗ Index numbers (SRT) ✗ File headers (WEBVTT, [Script Info], etc.) ✗ Style/format definitions ✗ Comment/Note lines ✗ Romaji tracks (EDR, JP style) ✗ Karaoke timing tracks EMPTY LINE HANDLING: If translated result is "" → keep ORIGINAL event text (safety fallback) This prevents blank subtitles from appearing on screen TAG HANDLING (pysubs2 approach): 1. get_plain_lines() → strip all tags → send clean text to AI 2. AI returns clean Myanmar text 3. replace_lines() → re-prepend leading {tag} block from original 4. event.start / event.end → NEVER touched """ # ───────────────────────────────────────────────────────────────────────────── # Runtime helpers used by subtitle_parser.py # ───────────────────────────────────────────────────────────────────────────── # Master skip-styles set (union of all known non-translatable style names) ALL_SKIP_STYLES = ASS_SKIP_STYLES | { "Signs", "Lyrics_JP", "Romaji2", "OP_Romaji", "ED_Romaji", } def is_skip_style(style_name: str) -> bool: """Return True if this ASS style should be skipped (not translated).""" if not style_name: return False s = style_name.strip() # Exact match if s in ALL_SKIP_STYLES: return True # Pattern match for common romaji/karaoke naming conventions sl = s.lower() return any(k in sl for k in ["romaji", "karaoke", "kar_", "_kar", "furigana"]) def format_display_info(fmt: str) -> str: """Return a human-readable description of what will be translated.""" if fmt == "srt": return "SRT: text lines only (timestamps and index numbers skipped)" if fmt in ("ass", "ssa"): return ( "ASS: Dialogue lines only " "(Comment lines, Style definitions, EDR/Romaji tracks skipped)" ) if fmt == "vtt": return "VTT: cue text only (NOTE blocks, STYLE blocks, timestamps skipped)" return f"{fmt.upper()}: subtitle text lines translated"