AliJabbarpour commited on
Commit
b02233b
·
verified ·
1 Parent(s): a0023f0

Update pipeline/postprocess.py

Browse files
Files changed (1) hide show
  1. pipeline/postprocess.py +99 -107
pipeline/postprocess.py CHANGED
@@ -1,4 +1,4 @@
1
- # ─── Post-Processing Engine v2 ─────────────────────────────────────────────────
2
 
3
  def format_timestamp(seconds: float) -> str:
4
  ms = round((seconds % 1) * 1000)
@@ -8,147 +8,134 @@ def format_timestamp(seconds: float) -> str:
8
  return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
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:
17
- continue
18
- duration = words[-1]["end"] - words[0]["start"]
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:
28
- return 20.0
29
- else:
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)
77
- best_gap_idx = -1
78
- best_gap = 0.0
79
- for i in range(1, len(words)):
80
- gap = words[i]["start"] - words[i - 1]["end"]
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):
91
- word_clean = words[i]["word"].strip().lower().strip(",.!?")
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]
103
 
104
- duration = words[-1]["end"] - words[0]["start"]
105
- if duration <= 0:
106
  return [seg]
107
 
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
 
127
  seg1 = {
128
- "words": words_part1,
129
- "text": " ".join(w["word"].strip() for w in words_part1),
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),
136
  "start": start2,
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"])
149
  text = seg["text"].strip()
150
  if not text:
151
  continue
 
 
152
  lines.append(f"{index}\n{start_tc} --> {end_tc}\n{text}")
153
  index += 1
154
  return "\n\n".join(lines) + "\n"
@@ -158,13 +145,18 @@ 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)
 
1
+ # ─── Post-Processing Engine v3 ─────────────────────────────────────────────────
2
 
3
  def format_timestamp(seconds: float) -> str:
4
  ms = round((seconds % 1) * 1000)
 
8
  return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
9
 
10
 
11
+ def merge_segments(segments: list) -> list:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  """
13
+ ادغام segment‌های کوتاه و تک‌کلمه‌ای به جملات کامل.
14
+ هر جمله حداکثر ۸۰ کاراکتر و حداکثر ۷ ثانیه.
 
15
  """
16
  if not segments:
17
  return []
18
 
19
+ MAX_CHARS = 80
20
+ MAX_DURATION = 7.0
21
+ MAX_GAP = 1.5 # اگه gap بین دو segment بیشتر از این بود، جدا بمون
22
+
23
  merged = []
24
+ current = None
 
25
 
26
+ for seg in segments:
27
+ if current is None:
28
+ current = {
29
+ "text": seg["text"].strip(),
30
+ "start": seg["start"],
31
+ "end": seg["end"],
32
+ "words": seg.get("words", []),
33
+ }
34
+ continue
35
 
 
36
  gap = seg["start"] - current["end"]
37
+ combined_text = current["text"] + " " + seg["text"].strip()
38
+ combined_duration = seg["end"] - current["start"]
39
+ ends_sentence = current["text"].rstrip().endswith((".", "!", "?"))
40
+
41
+ should_merge = (
42
+ len(combined_text.replace(" ", "")) <= MAX_CHARS
43
+ and combined_duration <= MAX_DURATION
44
+ and gap <= MAX_GAP
45
+ and not ends_sentence
46
+ )
47
+
48
+ if should_merge:
49
+ current["text"] = combined_text
50
  current["end"] = seg["end"]
51
+ current["words"] = current["words"] + seg.get("words", [])
52
  else:
53
  merged.append(current)
54
+ current = {
55
+ "text": seg["text"].strip(),
56
+ "start": seg["start"],
57
+ "end": seg["end"],
58
+ "words": seg.get("words", []),
59
+ }
60
 
61
+ if current:
62
+ merged.append(current)
63
 
64
+ return merged
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
 
67
+ def fix_overlaps(segments: list) -> list:
68
+ """
69
+ رفع timestamp‌های overlapping.
70
+ هیچ subtitle نباید قبل از شروع subtitle قبلی تموم بشه.
71
+ """
72
+ fixed = []
73
+ for i, seg in enumerate(segments):
74
+ if fixed and seg["start"] < fixed[-1]["end"]:
75
+ # overlap داریم — start رو تنظیم کن
76
+ seg = dict(seg)
77
+ seg["start"] = fixed[-1]["end"] + 0.001
78
+ if seg["start"] >= seg["end"]:
79
+ seg["end"] = seg["start"] + 0.5
80
+ fixed.append(seg)
81
+ return fixed
82
+
83
+
84
+ def split_long_segment(seg: dict, max_chars: int = 80) -> list:
85
+ """
86
+ شکستن segment‌های خیلی طولانی به دو بخش.
87
+ """
88
  words = seg.get("words", [])
89
+ text = seg["text"].strip()
 
90
 
91
+ if len(text.replace(" ", "")) <= max_chars or len(words) < 4:
 
92
  return [seg]
93
 
94
+ mid = len(words) // 2
 
95
 
96
+ # پیدا کردن بهترین نقطه split
97
+ best_idx = mid
98
+ best_gap = -1
99
+ for i in range(1, len(words)):
100
+ gap = words[i]["start"] - words[i-1]["end"]
101
+ if gap > best_gap:
102
+ best_gap = gap
103
+ best_idx = i
104
 
105
+ words1 = words[:best_idx]
106
+ words2 = words[best_idx:]
 
107
 
108
+ if not words1 or not words2:
109
  return [seg]
110
 
111
+ start2 = words2[0]["start"]
112
+ end1 = max(start2 - 0.08, words1[-1]["end"])
 
 
 
113
 
114
  seg1 = {
115
+ "text": " ".join(w["word"].strip() for w in words1),
116
+ "start": words1[0]["start"],
 
117
  "end": end1,
118
+ "words": words1,
119
  }
120
  seg2 = {
121
+ "text": " ".join(w["word"].strip() for w in words2),
 
122
  "start": start2,
123
+ "end": words2[-1]["end"],
124
+ "words": words2,
125
  }
126
 
127
+ return split_long_segment(seg1, max_chars) + split_long_segment(seg2, max_chars)
128
 
129
 
130
  def build_srt(segments: list) -> str:
131
  lines = []
132
  index = 1
133
  for seg in segments:
 
 
134
  text = seg["text"].strip()
135
  if not text:
136
  continue
137
+ start_tc = format_timestamp(seg["start"])
138
+ end_tc = format_timestamp(seg["end"])
139
  lines.append(f"{index}\n{start_tc} --> {end_tc}\n{text}")
140
  index += 1
141
  return "\n\n".join(lines) + "\n"
 
145
  if not segments:
146
  return ""
147
 
148
+ # مرحله ۱ — ادغام segment‌های کوتاه
 
 
149
  merged = merge_segments(segments)
150
 
151
+ # مرحله ۲ — رفع overlap
152
+ fixed = fix_overlaps(merged)
153
+
154
+ # مرحله ۳ — شکستن segment‌های خیلی طولانی
155
+ final = []
156
+ for seg in fixed:
157
+ final.extend(split_long_segment(seg))
158
+
159
+ # مرحله ۴ — رفع overlap بعد از split
160
+ final = fix_overlaps(final)
161
 
162
+ return build_srt(final)