AliJabbarpour commited on
Commit
358af18
·
verified ·
1 Parent(s): 79ef2f0

Create pipeline/postprocess.py

Browse files
Files changed (1) hide show
  1. pipeline/postprocess.py +167 -0
pipeline/postprocess.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─── Post-Processing Engine ────────────────────────────────────────────────────
2
+ # این لایه خروجی خام Whisper رو به SRT حرفه‌ای تبدیل می‌کنه
3
+
4
+ def format_timestamp(seconds: float) -> str:
5
+ """تبدیل ثانیه به فرمت SRT: HH:MM:SS,mmm"""
6
+ ms = round((seconds % 1) * 1000)
7
+ s = int(seconds) % 60
8
+ m = int(seconds // 60) % 60
9
+ h = int(seconds // 3600)
10
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
11
+
12
+
13
+ def detect_cps_threshold(segments: list) -> float:
14
+ """
15
+ تشخیص سرعت گفتار از روی کل ویدیو و برگرداندن CPS threshold مناسب.
16
+ slow/documentary → 17 | normal → 20 | fast/vlog → 25
17
+ """
18
+ total_chars = 0
19
+ total_duration = 0.0
20
+
21
+ for seg in segments:
22
+ words = seg.get("words", [])
23
+ if not words:
24
+ continue
25
+ duration = words[-1]["end"] - words[0]["start"]
26
+ if duration > 0:
27
+ total_chars += len(seg["text"].replace(" ", ""))
28
+ total_duration += duration
29
+
30
+ if total_duration == 0:
31
+ return 20.0
32
+
33
+ avg_cps = total_chars / total_duration
34
+
35
+ if avg_cps < 12:
36
+ return 17.0
37
+ elif avg_cps <= 18:
38
+ return 20.0
39
+ else:
40
+ return 25.0
41
+
42
+
43
+ def find_split_point(words: list) -> int:
44
+ """
45
+ پیدا کردن بهترین نقطه split در لیست کلمات.
46
+ اولویت ۱: مکث طبیعی (gap > 150ms)
47
+ اولویت ۲: مرز معنایی
48
+ اولویت ۳: نزدیک‌ترین نقطه به وسط
49
+ """
50
+ mid = len(words) // 2
51
+
52
+ # اولویت ۱ — مکث طبیعی (gap > 150ms)
53
+ best_gap_idx = -1
54
+ best_gap = 0.0
55
+ for i in range(1, len(words)):
56
+ gap = words[i]["start"] - words[i - 1]["end"]
57
+ if gap > 0.150 and gap > best_gap:
58
+ best_gap = gap
59
+ best_gap_idx = i
60
+
61
+ if best_gap_idx != -1:
62
+ return best_gap_idx
63
+
64
+ # اولویت ۲ — مرز معنایی (حروف ربط انگلیسی)
65
+ conjunctions = {"and", "but", "or", "so", "because", "that", "which", "when", "while", "if"}
66
+ for i in range(mid - 2, mid + 3):
67
+ if 1 <= i < len(words):
68
+ word_clean = words[i]["word"].strip().lower().strip(",.!?")
69
+ if word_clean in conjunctions:
70
+ return i
71
+
72
+ # اولویت ۳ — نزدیک‌ترین نقطه به وسط
73
+ return mid
74
+
75
+
76
+ def split_segment(seg: dict, cps_threshold: float) -> list:
77
+ """
78
+ بررسی اینکه آیا segment نیاز به split داره.
79
+ اگه CPS بالاتر از threshold بود → split.
80
+ """
81
+ words = seg.get("words", [])
82
+ if not words:
83
+ return [seg]
84
+
85
+ duration = words[-1]["end"] - words[0]["start"]
86
+ if duration <= 0:
87
+ return [seg]
88
+
89
+ char_count = len(seg["text"].replace(" ", ""))
90
+ cps = char_count / duration
91
+
92
+ # اگه CPS پایین‌تر از threshold بود → نیازی به split نیست
93
+ if cps <= cps_threshold:
94
+ return [seg]
95
+
96
+ # split لازمه — پیدا کردن نقطه split
97
+ split_idx = find_split_point(words)
98
+
99
+ words_part1 = words[:split_idx]
100
+ words_part2 = words[split_idx:]
101
+
102
+ if not words_part1 or not words_part2:
103
+ return [seg]
104
+
105
+ # ─── محاسبه timestamp (ترتیب مهمه!) ───────────────────────────────────────
106
+ # ۱. اول start[subtitle دوم] رو از word timestamp می‌گیریم
107
+ start2 = words_part2[0]["start"]
108
+ # ۲. بعد end[subtitle اول] رو مشتق می‌کنیم
109
+ end1 = max(start2 - 0.080, words_part1[-1]["end"])
110
+
111
+ # safety: حداقل 800ms نمایش
112
+ if end1 - words_part1[0]["start"] < 0.800:
113
+ end1 = words_part1[0]["start"] + 0.800
114
+
115
+ seg1 = {
116
+ "words": words_part1,
117
+ "text": " ".join(w["word"].strip() for w in words_part1),
118
+ "start": words_part1[0]["start"],
119
+ "end": end1,
120
+ }
121
+
122
+ seg2 = {
123
+ "words": words_part2,
124
+ "text": " ".join(w["word"].strip() for w in words_part2),
125
+ "start": start2,
126
+ "end": words_part2[-1]["end"],
127
+ }
128
+
129
+ # recursive split اگه هنوز طولانیه
130
+ return split_segment(seg1, cps_threshold) + split_segment(seg2, cps_threshold)
131
+
132
+
133
+ def build_srt(segments: list) -> str:
134
+ """ساخت فایل SRT نهایی از لیست segment‌ها"""
135
+ lines = []
136
+ index = 1
137
+
138
+ for seg in segments:
139
+ start_tc = format_timestamp(seg["start"])
140
+ end_tc = format_timestamp(seg["end"])
141
+ text = seg["text"].strip()
142
+ if not text:
143
+ continue
144
+ lines.append(f"{index}\n{start_tc} --> {end_tc}\n{text}")
145
+ index += 1
146
+
147
+ return "\n\n".join(lines) + "\n"
148
+
149
+
150
+ def process(segments: list) -> str:
151
+ """
152
+ تابع اصلی — خروجی Whisper ر�� می‌گیره و SRT برمی‌گردونه.
153
+ segments: لیستی از dict با کلیدهای text, words, start, end
154
+ """
155
+ if not segments:
156
+ return ""
157
+
158
+ # تشخیص CPS threshold بر اساس سرعت کل ویدیو
159
+ cps_threshold = detect_cps_threshold(segments)
160
+
161
+ # split کردن segment‌های طولانی
162
+ final_segments = []
163
+ for seg in segments:
164
+ final_segments.extend(split_segment(seg, cps_threshold))
165
+
166
+ # ساخت SRT نهایی
167
+ return build_srt(final_segments)