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

Upload postprocess.py

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