AliJabbarpour commited on
Commit
0d3ffcc
·
verified ·
1 Parent(s): 14a3f64

Delete pipeline/postprocess.py

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