Kai72828 commited on
Commit
8edf447
Β·
verified Β·
1 Parent(s): d5f4e8b

Upload 11 files

Browse files
Files changed (1) hide show
  1. subtitle_parser.py +46 -87
subtitle_parser.py CHANGED
@@ -1,12 +1,6 @@
1
  """
2
- subtitle_parser.py β€” Subtitle Parsing & Writing (Step 3)
3
  Handles .srt / .vtt / .ass via pysubs2.
4
-
5
- ASS-specific handling:
6
- - Inline override tags {\\pos(...)\\be5} are preserved on replace
7
- - Style-based skip (EDR/romaji) β€” lines returned as "" β†’ kept original
8
- - Style: Default, Alt, Note, EDE β†’ translated
9
- - Style: EDR (romaji) β†’ skipped (returned as-is)
10
  """
11
 
12
  import copy
@@ -20,17 +14,32 @@ import pysubs2
20
 
21
  logger = logging.getLogger(__name__)
22
 
23
- # ── Workspace ──────────────────────────────────────────────────────────────
24
  WORKSPACE_DIR = os.path.join(tempfile.gettempdir(), "subsync_workspace")
25
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
26
 
27
- # ── Styles to skip (do not send to AI, keep original text) ────────────────
28
- # EDR = Japanese romaji in fansub ASS files β€” should never be translated
29
- SKIP_STYLES = {"EDR", "Romaji", "JP", "Karaoke", "KAR"}
 
 
30
 
31
- # ── Format map ─────────────────────────────────────────────────────────────
32
  _EXT_MAP = {".srt": "srt", ".vtt": "vtt", ".ass": "ass", ".ssa": "ass"}
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  def _detect_format(filename: str) -> str:
36
  ext = os.path.splitext(filename.lower())[1]
@@ -42,10 +51,14 @@ def _detect_format(filename: str) -> str:
42
  return fmt
43
 
44
 
45
- # ── Parse ──────────────────────────────────────────────────────────────────
 
 
 
 
46
 
47
  def parse_subtitle(file_bytes: bytes, filename: str):
48
- """Parse subtitle bytes β†’ (SSAFile, fmt_string). Raises ValueError on failure."""
49
  fmt = _detect_format(filename)
50
 
51
  text = None
@@ -78,47 +91,27 @@ def parse_subtitle(file_bytes: bytes, filename: str):
78
  os.unlink(tmp)
79
  return subs, fmt
80
  except Exception as e2:
81
- raise ValueError(f"Parse မရပါ:\nAttempt 1: {e1}\nAttempt 2: {e2}") from e2
82
-
83
-
84
- # ── Tag stripping ──────────────────────────────────────────────────────────
85
- _ASS_TAG_RE = re.compile(r"\{[^}]*\}") # {\\an8} {\\pos(x,y)} etc.
86
- _HTML_TAG_RE = re.compile(r"<[^>]+>") # <i> <b> etc.
87
- # Leading tag block: one or more {...} at the start of the string
88
- _LEADING_TAGS_RE = re.compile(r"^(\{[^}]*\})+")
89
-
90
-
91
- def _strip_tags(text: str) -> str:
92
- text = _ASS_TAG_RE.sub("", text)
93
- text = _HTML_TAG_RE.sub("", text)
94
- return text.strip()
95
-
96
 
97
- # ── Plain line extraction ──────────────────────────────────────────────────
98
 
99
  def get_plain_lines(ssafile) -> list:
100
  """
101
  Return list[str] same length as ssafile.events.
102
-
103
- Translate decisions per event:
104
- βœ… Dialogue events with translatable style β†’ return clean text
105
- βœ— Comment events (editor notes, not shown to viewer) β†’ return ""
106
- βœ— Style in SKIP_STYLES (EDR/Romaji/JP/Karaoke) β†’ return ""
107
- Empty string β†’ replace_lines keeps original event.text unchanged.
108
-
109
- ASS inline tags {\\an8}{\\pos(...)} stripped before returning.
110
- \\N / \\n normalised to Python newline.
111
  """
112
  lines = []
113
  for event in ssafile.events:
114
- # Skip Comment lines (editor notes β€” not displayed to viewer)
115
- # pysubs2 exposes both Dialogue and Comment in events list
116
- is_comment = getattr(event, "is_comment", False)
117
- if is_comment:
118
  lines.append("")
119
  continue
120
 
121
- # Skip non-English / romaji / karaoke style tracks (exact + pattern match)
122
  if is_skip_style(event.style):
123
  lines.append("")
124
  continue
@@ -129,24 +122,11 @@ def get_plain_lines(ssafile) -> list:
129
  return lines
130
 
131
 
132
- # ── Replace lines β€” tag-safe ───────────────────────────────────────────────
133
-
134
  def replace_lines(ssafile, new_lines: list):
135
  """
136
- Return new SSAFile with translated text injected index-for-index.
137
-
138
- TIMESTAMP SAFETY:
139
- - event.start / event.end never touched (deep-copy preserves them)
140
- - Only event.text is changed
141
-
142
- ASS TAG PRESERVATION:
143
- - Leading override tags {\\pos(...)\\be5} etc. are extracted from the
144
- original event.text and re-prepended to the translated text.
145
- - This keeps positioning, blur, fade effects intact after translation.
146
-
147
- SKIP STYLES:
148
- - If translated line is "" (empty) β†’ original event.text kept unchanged.
149
- This handles EDR (romaji) and any other skip-style events.
150
  """
151
  n_events = len(ssafile.events)
152
  n_lines = len(new_lines)
@@ -157,44 +137,26 @@ def replace_lines(ssafile, new_lines: list):
157
  f"!= subtitle events ({n_events})"
158
  )
159
 
160
- result = copy.deepcopy(ssafile)
161
- kept_count = 0
162
- tagged_count = 0
163
 
164
  for i, event in enumerate(result.events):
165
  translated = str(new_lines[i]).strip()
166
 
167
- # ── Empty translation β†’ keep original (skip-style or parse fail) ──
168
  if not translated:
169
- kept_count += 1
170
- continue # event.text unchanged
171
 
172
- # ── Preserve leading inline override tags ──────────────────────────
173
- # e.g. {\\pos(960,540)\\fad(200,0)} Hello β†’ {\\pos(960,540)\\fad(200,0)}α€™α€„α€Ία€Ήα€‚α€œα€¬α€•α€«
174
- original_text = ssafile.events[i].text # from original (pre-deepcopy)
175
  leading_m = _LEADING_TAGS_RE.match(original_text)
176
  tag_prefix = leading_m.group(0) if leading_m else ""
177
 
178
- if tag_prefix:
179
- tagged_count += 1
180
 
181
- # Convert Python newlines back to ASS \\N
182
- body = translated.replace("\n", "\\N")
183
- event.text = tag_prefix + body
184
-
185
- if kept_count:
186
- logger.info("replace_lines: %d/%d events kept original (skip/empty)",
187
- kept_count, n_events)
188
- if tagged_count:
189
- logger.info("replace_lines: %d events had leading tags preserved",
190
- tagged_count)
191
  return result
192
 
193
 
194
- # ── Write ──────────────────────────────────────────────────────────────────
195
-
196
  def write_subtitle(ssafile, fmt: str) -> bytes:
197
- """Serialise SSAFile to UTF-8 bytes in original format."""
198
  try:
199
  return ssafile.to_string(fmt).encode("utf-8")
200
  except Exception as exc:
@@ -202,13 +164,10 @@ def write_subtitle(ssafile, fmt: str) -> bytes:
202
 
203
 
204
  def output_filename(original_name: str) -> str:
205
- """'movie.en.ass' β†’ 'movie.en.my.ass'"""
206
  base, ext = os.path.splitext(original_name)
207
  return f"{base}.my{ext}"
208
 
209
 
210
- # ── Workspace management ───────────────────────────────────────────────────
211
-
212
  def clean_workspace() -> None:
213
  try:
214
  if os.path.exists(WORKSPACE_DIR):
 
1
  """
2
+ subtitle_parser.py β€” Subtitle Parsing & Writing
3
  Handles .srt / .vtt / .ass via pysubs2.
 
 
 
 
 
 
4
  """
5
 
6
  import copy
 
14
 
15
  logger = logging.getLogger(__name__)
16
 
 
17
  WORKSPACE_DIR = os.path.join(tempfile.gettempdir(), "subsync_workspace")
18
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
19
 
20
+ # ASS styles that should NOT be translated (romaji, karaoke, etc.)
21
+ SKIP_STYLES = {
22
+ "EDR", "Romaji", "JP", "Karaoke", "KAR",
23
+ "Signs", "Lyrics_JP", "Romaji2", "OP_Romaji", "ED_Romaji",
24
+ }
25
 
 
26
  _EXT_MAP = {".srt": "srt", ".vtt": "vtt", ".ass": "ass", ".ssa": "ass"}
27
 
28
+ _ASS_TAG_RE = re.compile(r"\{[^}]*\}")
29
+ _HTML_TAG_RE = re.compile(r"<[^>]+>")
30
+ _LEADING_TAGS_RE = re.compile(r"^(\{[^}]*\})+")
31
+
32
+
33
+ def is_skip_style(style_name: str) -> bool:
34
+ """Return True if this ASS style should be skipped (not translated)."""
35
+ if not style_name:
36
+ return False
37
+ s = style_name.strip()
38
+ if s in SKIP_STYLES:
39
+ return True
40
+ sl = s.lower()
41
+ return any(k in sl for k in ["romaji", "karaoke", "kar_", "_kar", "furigana"])
42
+
43
 
44
  def _detect_format(filename: str) -> str:
45
  ext = os.path.splitext(filename.lower())[1]
 
51
  return fmt
52
 
53
 
54
+ def _strip_tags(text: str) -> str:
55
+ text = _ASS_TAG_RE.sub("", text)
56
+ text = _HTML_TAG_RE.sub("", text)
57
+ return text.strip()
58
+
59
 
60
  def parse_subtitle(file_bytes: bytes, filename: str):
61
+ """Parse subtitle bytes β†’ (SSAFile, fmt_string)."""
62
  fmt = _detect_format(filename)
63
 
64
  text = None
 
91
  os.unlink(tmp)
92
  return subs, fmt
93
  except Exception as e2:
94
+ raise ValueError(
95
+ f"Parse မရပါ:\nAttempt 1: {e1}\nAttempt 2: {e2}"
96
+ ) from e2
 
 
 
 
 
 
 
 
 
 
 
 
97
 
 
98
 
99
  def get_plain_lines(ssafile) -> list:
100
  """
101
  Return list[str] same length as ssafile.events.
102
+ - Comment events β†’ "" (keep original, not displayed)
103
+ - Skip styles β†’ "" (EDR/romaji/karaoke etc.)
104
+ - Normal events β†’ clean text (tags stripped)
105
+ Empty string β†’ replace_lines keeps original text unchanged.
 
 
 
 
 
106
  """
107
  lines = []
108
  for event in ssafile.events:
109
+ # Skip ASS Comment lines (editor notes, not shown to viewer)
110
+ if getattr(event, "is_comment", False):
 
 
111
  lines.append("")
112
  continue
113
 
114
+ # Skip romaji / karaoke / non-English style tracks
115
  if is_skip_style(event.style):
116
  lines.append("")
117
  continue
 
122
  return lines
123
 
124
 
 
 
125
  def replace_lines(ssafile, new_lines: list):
126
  """
127
+ Return new SSAFile with translated text. Timestamps never touched.
128
+ Leading ASS override tags {\\pos(...)\\be5} preserved from original.
129
+ Empty translation β†’ keep original event.text.
 
 
 
 
 
 
 
 
 
 
 
130
  """
131
  n_events = len(ssafile.events)
132
  n_lines = len(new_lines)
 
137
  f"!= subtitle events ({n_events})"
138
  )
139
 
140
+ result = copy.deepcopy(ssafile)
 
 
141
 
142
  for i, event in enumerate(result.events):
143
  translated = str(new_lines[i]).strip()
144
 
145
+ # Empty β†’ keep original (skip-style or fallback)
146
  if not translated:
147
+ continue
 
148
 
149
+ # Re-prepend leading inline tags from original
150
+ original_text = ssafile.events[i].text
 
151
  leading_m = _LEADING_TAGS_RE.match(original_text)
152
  tag_prefix = leading_m.group(0) if leading_m else ""
153
 
154
+ event.text = tag_prefix + translated.replace("\n", "\\N")
 
155
 
 
 
 
 
 
 
 
 
 
 
156
  return result
157
 
158
 
 
 
159
  def write_subtitle(ssafile, fmt: str) -> bytes:
 
160
  try:
161
  return ssafile.to_string(fmt).encode("utf-8")
162
  except Exception as exc:
 
164
 
165
 
166
  def output_filename(original_name: str) -> str:
 
167
  base, ext = os.path.splitext(original_name)
168
  return f"{base}.my{ext}"
169
 
170
 
 
 
171
  def clean_workspace() -> None:
172
  try:
173
  if os.path.exists(WORKSPACE_DIR):