AliJabbarpour commited on
Commit
d695033
·
verified ·
1 Parent(s): 2d1d6d9

Delete pipeline/postprocess.py

Browse files
Files changed (1) hide show
  1. pipeline/postprocess.py +0 -279
pipeline/postprocess.py DELETED
@@ -1,279 +0,0 @@
1
- # ─── Post-Processing Engine v9 ─────────────────────────────────────────────────
2
-
3
- MIN_DISPLAY = 1.5
4
- MAX_DISPLAY = 7.0
5
- MERGE_MAX_CHARS = 72
6
- MAX_GAP = 1.5
7
- MAX_LINE_CHARS = 60 # threshold برای split به دو subtitle جداگانه
8
- WRAP_CHARS = 42 # استاندارد Netflix برای wrap به دو خط
9
- CPS_LIMIT = 17 # حداکثر کاراکتر قابل‌خواندن در ثانیه - برای تشخیص جمله‌ی «به‌سرعت رد شده»
10
-
11
-
12
- def format_timestamp(seconds: float) -> str:
13
- ms = round((seconds % 1) * 1000)
14
- s = int(seconds) % 60
15
- m = int(seconds // 60) % 60
16
- h = int(seconds // 3600)
17
- return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
18
-
19
-
20
- def merge_segments(segments: list) -> list:
21
- if not segments:
22
- return []
23
-
24
- merged = []
25
- current = None
26
-
27
- for seg in segments:
28
- if current is None:
29
- current = {
30
- "text": seg["text"].strip(),
31
- "start": seg["start"],
32
- "end": seg["end"],
33
- "words": list(seg.get("words", [])),
34
- "speaker": seg.get("speaker"),
35
- }
36
- continue
37
-
38
- gap = seg["start"] - current["end"]
39
- combined_text = current["text"] + " " + seg["text"].strip()
40
- combined_duration = seg["end"] - current["start"]
41
- ends_sentence = current["text"].rstrip().endswith((".", "!", "?"))
42
- current_duration = current["end"] - current["start"]
43
- is_short = current_duration < 1.5
44
- same_speaker = current.get("speaker") is None or current.get("speaker") == seg.get("speaker")
45
- too_fast = (len(current["text"]) / current_duration) > CPS_LIMIT if current_duration > 0 else True
46
- fits_dialogue = len(current["text"]) <= WRAP_CHARS and len(seg["text"].strip()) <= WRAP_CHARS
47
- already_dialogue = current.get("is_dialogue_block", False)
48
-
49
- should_merge = (
50
- not ends_sentence
51
- and combined_duration <= MAX_DISPLAY
52
- and same_speaker
53
- and gap <= MAX_GAP
54
- and (
55
- is_short
56
- or len(combined_text) <= MERGE_MAX_CHARS
57
- )
58
- )
59
-
60
- should_pair_as_dialogue = (
61
- not already_dialogue
62
- and not same_speaker
63
- and too_fast
64
- and gap <= MAX_GAP
65
- and fits_dialogue
66
- )
67
-
68
- if should_merge:
69
- current["text"] = combined_text
70
- current["end"] = seg["end"]
71
- current["words"] += list(seg.get("words", []))
72
- elif should_pair_as_dialogue:
73
- current["text"] = f"- {current['text']}\n- {seg['text'].strip()}"
74
- current["end"] = seg["end"]
75
- current["words"] += list(seg.get("words", []))
76
- current["is_dialogue_block"] = True
77
- else:
78
- merged.append(current)
79
- current = {
80
- "text": seg["text"].strip(),
81
- "start": seg["start"],
82
- "end": seg["end"],
83
- "words": list(seg.get("words", [])),
84
- "speaker": seg.get("speaker"),
85
- }
86
-
87
- if current:
88
- merged.append(current)
89
-
90
- return merged
91
-
92
-
93
- def remove_duplicates(segments: list) -> list:
94
- if not segments:
95
- return []
96
- result = [segments[0]]
97
- for seg in segments[1:]:
98
- if seg["text"].strip() != result[-1]["text"].strip():
99
- result.append(seg)
100
- return result
101
-
102
-
103
- def fix_overlaps(segments: list) -> list:
104
- fixed = []
105
- for seg in segments:
106
- seg = dict(seg)
107
- if fixed and seg["start"] < fixed[-1]["end"]:
108
- seg["start"] = fixed[-1]["end"] + 0.001
109
- if seg["start"] >= seg["end"]:
110
- seg["end"] = seg["start"] + MIN_DISPLAY
111
- fixed.append(seg)
112
- return fixed
113
-
114
-
115
- def apply_min_display(segments: list) -> list:
116
- result = []
117
- for i, seg in enumerate(segments):
118
- seg = dict(seg)
119
- duration = seg["end"] - seg["start"]
120
- if duration < MIN_DISPLAY:
121
- if i + 1 < len(segments):
122
- next_start = segments[i + 1]["start"]
123
- max_end = next_start - 0.080
124
- else:
125
- max_end = seg["end"] + MAX_DISPLAY
126
- new_end = seg["start"] + MIN_DISPLAY
127
- seg["end"] = min(new_end, max(max_end, seg["end"]))
128
- result.append(seg)
129
- return result
130
-
131
-
132
- def split_long_segment(seg: dict) -> list:
133
- """
134
- فقط اگه punctuation نزدیک وسط جمله بود، به دو subtitle جداگانه بشکن.
135
- اگه نقطه مناسبی نبود، نشکن — wrap_text در build_srt دو خطش می‌کنه.
136
- بلوک‌های دیالوگِ دو-گوینده‌ای (is_dialogue_block) هیچ‌وقت شکسته نمی‌شن.
137
- """
138
- if seg.get("is_dialogue_block"):
139
- return [seg]
140
-
141
- words = seg.get("words", [])
142
- text = seg["text"].strip()
143
-
144
- if len(text) <= MAX_LINE_CHARS or len(words) < 4:
145
- return [seg]
146
-
147
- punct_chars = {'.', '!', '?', ',', ';', ':'}
148
- sentence_enders = {'.', '!', '?'}
149
- # کلمه‌های کوچیک و به‌هم‌چسبیده‌ای که نباید تنها و جدا از کلمه‌ی بعدشون ته یه زیرنویس بمونن
150
- weak_endings = {
151
- 'in', 'my', 'your', 'his', 'her', 'our', 'their', 'its', 'the', 'a', 'an',
152
- 'of', 'to', 'at', 'on', 'by', 'for', 'with', 'is', 'are', 'was', 'were',
153
- 'and', 'or', 'but', 'so', 'if', 'that', 'this', 'we', 'you', 'he', 'she', 'they',
154
- }
155
- min_part_chars = 10
156
-
157
- full_text = text
158
- mid_char = len(full_text) / 2
159
-
160
- best_idx = -1
161
- best_score = float('-inf')
162
- char_pos = 0
163
-
164
- for i, w in enumerate(words):
165
- word_stripped = w["word"].strip()
166
- this_word_end_char = char_pos + len(word_stripped)
167
-
168
- if word_stripped and word_stripped[-1] in punct_chars and 0 < i < len(words) - 1:
169
- # چک درست: خودِ کلمه‌ای که این علامت نگارشی روشه، نباید یه کلمه‌ی ضعیف باشه
170
- bare_word = word_stripped.strip(".,!?;:").lower()
171
- part1_chars = this_word_end_char
172
- part2_chars = len(full_text) - this_word_end_char
173
-
174
- if bare_word not in weak_endings and part1_chars >= min_part_chars and part2_chars >= min_part_chars:
175
- dist_from_mid = abs(this_word_end_char - mid_char)
176
- proximity_score = 1.0 - (dist_from_mid / len(full_text))
177
- sentence_end_bonus = 0.5 if word_stripped[-1] in sentence_enders else 0.0
178
- score = proximity_score + sentence_end_bonus
179
-
180
- if score > best_score:
181
- best_score = score
182
- best_idx = i + 1
183
-
184
- char_pos = this_word_end_char + 1
185
-
186
- # هیچ نقطه‌ی شکستن مناسبی پیدا نشد → نشکن، wrap_text کافیه
187
- if best_idx == -1:
188
- return [seg]
189
-
190
- words1 = words[:best_idx]
191
- words2 = words[best_idx:]
192
-
193
- if len(words1) < 2 or len(words2) < 2:
194
- return [seg]
195
-
196
- start2 = words2[0]["start"]
197
- end1 = max(start2 - 0.080, words1[-1]["end"])
198
-
199
- if end1 - words1[0]["start"] < MIN_DISPLAY:
200
- end1 = words1[0]["start"] + MIN_DISPLAY
201
- if end1 >= start2:
202
- end1 = start2 - 0.040
203
-
204
- seg1 = {
205
- "text": " ".join(w["word"].strip() for w in words1),
206
- "start": words1[0]["start"],
207
- "end": end1,
208
- "words": words1,
209
- }
210
- seg2 = {
211
- "text": " ".join(w["word"].strip() for w in words2),
212
- "start": start2,
213
- "end": words2[-1]["end"],
214
- "words": words2,
215
- }
216
-
217
- return split_long_segment(seg1) + split_long_segment(seg2)
218
-
219
-
220
- def wrap_text(text: str) -> str:
221
- """اگه متن بیشتر از WRAP_CHARS بود، به دو خط تقسیم کن.
222
- اگه متن از قبل یه بلوک دیالوگِ دو-گوینده‌ای (با - و خط جدید) بود، دست‌نخورده برگردون."""
223
- if text.startswith("- ") and "\n" in text:
224
- return text
225
-
226
- if len(text) <= WRAP_CHARS:
227
- return text
228
-
229
- words = text.split()
230
- best_break = -1
231
- best_diff = float('inf')
232
- current_len = 0
233
-
234
- for i, word in enumerate(words):
235
- current_len += len(word) + (1 if i > 0 else 0)
236
- diff = abs(current_len - len(text) / 2)
237
- if diff < best_diff:
238
- best_diff = diff
239
- best_break = i + 1
240
-
241
- if best_break <= 0 or best_break >= len(words):
242
- return text
243
-
244
- line1 = " ".join(words[:best_break])
245
- line2 = " ".join(words[best_break:])
246
- return f"{line1}\n{line2}"
247
-
248
-
249
- def build_srt(segments: list) -> str:
250
- lines = []
251
- index = 1
252
- for seg in segments:
253
- text = seg["text"].strip()
254
- if not text:
255
- continue
256
- start_tc = format_timestamp(seg["start"])
257
- end_tc = format_timestamp(seg["end"])
258
- wrapped = wrap_text(text)
259
- lines.append(f"{index}\n{start_tc} --> {end_tc}\n{wrapped}")
260
- index += 1
261
- return "\n\n".join(lines) + "\n"
262
-
263
-
264
- def process(segments: list) -> str:
265
- if not segments:
266
- return ""
267
-
268
- merged = merge_segments(segments)
269
- merged = remove_duplicates(merged)
270
- fixed = fix_overlaps(merged)
271
-
272
- split = []
273
- for seg in fixed:
274
- split.extend(split_long_segment(seg))
275
-
276
- split = fix_overlaps(split)
277
- final = apply_min_display(split)
278
-
279
- return build_srt(final)