AliJabbarpour commited on
Commit
525768d
·
verified ·
1 Parent(s): dfc9371

Delete pipeline/postprocess.py

Browse files
Files changed (1) hide show
  1. pipeline/postprocess.py +0 -276
pipeline/postprocess.py DELETED
@@ -1,276 +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
- mid = len(words) // 2
148
- punct_chars = {'.', ',', '!', '?', ';', ':'}
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', 'i', 'we', 'you', 'he', 'she', 'it', 'they',
154
- }
155
-
156
- def is_weak_split(idx):
157
- """آیا کلمه‌ی درست قبل از این نقطه‌ی شکستن، یه کلمه‌ی کوچیک و ناتموم‌کننده‌ست؟"""
158
- prev_word = words[idx - 1]["word"].strip().strip(".,!?;:").lower() if idx > 0 else ""
159
- return prev_word in weak_endings
160
-
161
- # اولویت اول: یه علامت نگارشی توی قسمت پایانی جمله (بعد از ۷۰٪ طول) پیدا کن -
162
- # یه پایان طبیعیِ دیرتر، بهتر از شکستن مجبوری وسط یه عبارته
163
- late_threshold = len(words) * 0.7
164
- best_punct = -1
165
-
166
- for i in range(len(words) - 2, 0, -1): # از آخر به اول، اولین مورد مناسب رو نگه می‌داریم
167
- word = words[i]["word"].strip()
168
- if word and word[-1] in punct_chars and i >= late_threshold and not is_weak_split(i):
169
- best_punct = i + 1
170
- break
171
-
172
- # اگه علامت پایانی مناسبی نبود، برگرد به روش قبلی: نزدیک‌ترین علامت به وسط جمله
173
- if best_punct == -1:
174
- best_dist = float('inf')
175
- for i in range(1, len(words) - 1):
176
- word = words[i]["word"].strip()
177
- if word and word[-1] in punct_chars and not is_weak_split(i):
178
- dist = abs(i - mid)
179
- if dist < best_dist:
180
- best_dist = dist
181
- best_punct = i + 1
182
-
183
- # اگه punctuation نزدیک وسط هم نبود → نشکن، wrap_text کافیه
184
- if best_punct == -1 or best_dist > len(words) // 3:
185
- return [seg]
186
-
187
- words1 = words[:best_punct]
188
- words2 = words[best_punct:]
189
-
190
- if len(words1) < 3 or len(words2) < 3:
191
- return [seg]
192
-
193
- start2 = words2[0]["start"]
194
- end1 = max(start2 - 0.080, words1[-1]["end"])
195
-
196
- if end1 - words1[0]["start"] < MIN_DISPLAY:
197
- end1 = words1[0]["start"] + MIN_DISPLAY
198
- if end1 >= start2:
199
- end1 = start2 - 0.040
200
-
201
- seg1 = {
202
- "text": " ".join(w["word"].strip() for w in words1),
203
- "start": words1[0]["start"],
204
- "end": end1,
205
- "words": words1,
206
- }
207
- seg2 = {
208
- "text": " ".join(w["word"].strip() for w in words2),
209
- "start": start2,
210
- "end": words2[-1]["end"],
211
- "words": words2,
212
- }
213
-
214
- return split_long_segment(seg1) + split_long_segment(seg2)
215
-
216
-
217
- def wrap_text(text: str) -> str:
218
- """اگه متن بیشتر از WRAP_CHARS بود، به دو خط تقسیم کن.
219
- اگه متن از قبل یه بلوک دیالوگِ دو-گوینده‌ای (با - و خط جدید) بود، دست‌نخورده برگردون."""
220
- if text.startswith("- ") and "\n" in text:
221
- return text
222
-
223
- if len(text) <= WRAP_CHARS:
224
- return text
225
-
226
- words = text.split()
227
- best_break = -1
228
- best_diff = float('inf')
229
- current_len = 0
230
-
231
- for i, word in enumerate(words):
232
- current_len += len(word) + (1 if i > 0 else 0)
233
- diff = abs(current_len - len(text) / 2)
234
- if diff < best_diff:
235
- best_diff = diff
236
- best_break = i + 1
237
-
238
- if best_break <= 0 or best_break >= len(words):
239
- return text
240
-
241
- line1 = " ".join(words[:best_break])
242
- line2 = " ".join(words[best_break:])
243
- return f"{line1}\n{line2}"
244
-
245
-
246
- def build_srt(segments: list) -> str:
247
- lines = []
248
- index = 1
249
- for seg in segments:
250
- text = seg["text"].strip()
251
- if not text:
252
- continue
253
- start_tc = format_timestamp(seg["start"])
254
- end_tc = format_timestamp(seg["end"])
255
- wrapped = wrap_text(text)
256
- lines.append(f"{index}\n{start_tc} --> {end_tc}\n{wrapped}")
257
- index += 1
258
- return "\n\n".join(lines) + "\n"
259
-
260
-
261
- def process(segments: list) -> str:
262
- if not segments:
263
- return ""
264
-
265
- merged = merge_segments(segments)
266
- merged = remove_duplicates(merged)
267
- fixed = fix_overlaps(merged)
268
-
269
- split = []
270
- for seg in fixed:
271
- split.extend(split_long_segment(seg))
272
-
273
- split = fix_overlaps(split)
274
- final = apply_min_display(split)
275
-
276
- return build_srt(final)