"""
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"