AliJabbarpour commited on
Commit
ea34464
·
verified ·
1 Parent(s): f04be86

Update pipeline/postprocess.py

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