AliJabbarpour commited on
Commit
f99d1a8
·
verified ·
1 Parent(s): 95ff138

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -291
app.py DELETED
@@ -1,291 +0,0 @@
1
- import os
2
- import tempfile
3
- import torch
4
- import torchaudio
5
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect
6
- from fastapi.middleware.cors import CORSMiddleware
7
- from faster_whisper import WhisperModel
8
- from df.enhance import enhance, init_df, load_audio, save_audio
9
- from pyannote.audio import Pipeline as DiarizationPipeline
10
- from pipeline.postprocess import process
11
-
12
- app = FastAPI()
13
-
14
- app.add_middleware(
15
- CORSMiddleware,
16
- allow_origins=["*"],
17
- allow_methods=["*"],
18
- allow_headers=["*"],
19
- )
20
-
21
- print("Loading Whisper model...", flush=True)
22
- model = WhisperModel("large-v3", device="cpu", compute_type="int8")
23
- print("Whisper model ready!")
24
-
25
- print("Loading DeepFilterNet model...", flush=True)
26
- df_model, df_state, _ = init_df()
27
- print("DeepFilterNet model ready!")
28
-
29
- USE_DEEPFILTERNET = False # غیرفعال - شواهد نشون داد کمکی نمی‌کنه و بعضی صداها رو خراب می‌کنه
30
-
31
- print("Loading word-alignment model...", flush=True)
32
- align_bundle = torchaudio.pipelines.MMS_FA
33
- align_model = align_bundle.get_model().to("cpu")
34
- align_labels = align_bundle.get_labels()
35
- align_dictionary = {c: i for i, c in enumerate(align_labels)}
36
- print("Word-alignment model ready!")
37
-
38
- print("Loading speaker-diarization model...", flush=True)
39
- HF_TOKEN = os.environ.get("HF_TOKEN")
40
- diarize_pipeline = DiarizationPipeline.from_pretrained(
41
- "pyannote/speaker-diarization-3.1", use_auth_token=HF_TOKEN
42
- )
43
- print("Speaker-diarization model ready!")
44
-
45
- MIN_NOISE_BLOCK_SEC = 1.0 # حداقل طول یه بلوک بی‌گوینده تا «نویز» حساب بشه
46
-
47
-
48
- def get_speaker_at(diarization, time_sec):
49
- for turn, _, speaker in diarization.itertracks(yield_label=True):
50
- if turn.start <= time_sec <= turn.end:
51
- return speaker
52
- return None
53
-
54
-
55
- def find_leading_noise_cutoff(diarization, whisper_start, search_back_sec=4.0):
56
- """
57
- برمی‌گرده به عقب از شروعِ حدسیِ Whisper، دنبال یه بلوک نویز/بی‌گوینده‌ی
58
- طولانی (حداقل MIN_NOISE_BLOCK_SEC) که دقیقاً قبل از این جمله باشه.
59
- نوسان‌های خیلی کوتاه (کمتر از STABLE_SPEAKER_SEC) که یه گوینده برای یه
60
- لحظه‌ی گذرا ظاهر میشه رو نادیده می‌گیره - فقط با دیدنِ یه گوینده‌ی
61
- "پایدار" واقعاً متوقف میشه.
62
- اگه پیدا کرد، زمان پایانِ اون بلوک نویز رو برمی‌گردونه (یعنی «از اینجا به بعد
63
- اعتماد کن»)؛ اگه پیدا نکرد، None برمی‌گردونه (یعنی چیزی رو نبُر).
64
- """
65
- STABLE_SPEAKER_SEC = 0.2
66
- window_start = max(0, whisper_start - search_back_sec)
67
- step = 0.05
68
- t = whisper_start
69
- noise_end = None
70
- noise_start = None
71
- speaker_run_start = None
72
-
73
- while t > window_start:
74
- speaker = get_speaker_at(diarization, t)
75
-
76
- if speaker is None:
77
- speaker_run_start = None
78
- if noise_end is None:
79
- noise_end = t
80
- noise_start = t
81
- else:
82
- if speaker_run_start is None:
83
- speaker_run_start = t
84
- run_len = speaker_run_start - t
85
- if run_len >= STABLE_SPEAKER_SEC:
86
- break
87
- # نوسان کوتاه‌مدت - نادیده بگیر و بخشی از بلوک نویز حساب کن
88
- if noise_end is None:
89
- noise_end = t
90
- noise_start = t
91
-
92
- t -= step
93
-
94
- if noise_start is not None and noise_end is not None:
95
- block_len = noise_end - noise_start
96
- if block_len >= MIN_NOISE_BLOCK_SEC:
97
- return noise_end
98
- return None
99
-
100
-
101
- def realign_word_timestamps(waveform_16k, words, whisper_start, whisper_end, cutoff_start=None):
102
- """
103
- برای یه جمله، به‌جای زمان‌بندی حدسی خودِ Whisper، با یه مدل تخصصی جدا
104
- (wav2vec2 از طریق torchaudio) دقیقاً چک می‌کنه هر کلمه از کجای صدا گفته شده.
105
- اگه cutoff_start داده بشه (یعنی یه بلوک نویز قبل از جمله شناسایی شده)،
106
- جستجو از همون نقطه شروع میشه، نه از قبل‌ترش.
107
- اگه به هر دلیلی هم‌ترازی جواب نداد، همون زمان‌بندی قبلی Whisper دست‌نخورده می‌مونه.
108
- """
109
- try:
110
- margin = 0.5
111
- raw_start = whisper_start - margin
112
- effective_start = max(raw_start, cutoff_start) if cutoff_start is not None else raw_start
113
-
114
- start_sample = max(0, int(effective_start * 16000))
115
- end_sample = int((whisper_end + margin) * 16000)
116
- clip = waveform_16k[:, start_sample:end_sample]
117
-
118
- clean_words = []
119
- for w in words:
120
- token = "".join(ch for ch in w["word"].lower() if ch in align_dictionary)
121
- if token:
122
- clean_words.append((w, token))
123
-
124
- if not clean_words:
125
- return words
126
-
127
- transcript = "".join(tok for _, tok in clean_words)
128
- tokens = [align_dictionary[c] for c in transcript]
129
-
130
- with torch.inference_mode():
131
- emission, _ = align_model(clip)
132
-
133
- targets = torch.tensor([tokens], dtype=torch.int32)
134
- input_lengths = torch.tensor([emission.shape[1]])
135
- target_lengths = torch.tensor([len(tokens)])
136
-
137
- aligned_tokens, scores = torchaudio.functional.forced_align(
138
- emission, targets, input_lengths, target_lengths, blank=0
139
- )
140
- spans = torchaudio.functional.merge_tokens(aligned_tokens[0], scores[0])
141
-
142
- ratio = clip.shape[1] / emission.shape[1] / 16000
143
- new_words = []
144
- span_idx = 0
145
- for w, tok in clean_words:
146
- n_chars = len(tok)
147
- matched = [s for s in spans[span_idx:] if s.token != 0][:n_chars]
148
- if len(matched) < n_chars:
149
- new_words.append(w)
150
- continue
151
- w_start = matched[0].start * ratio + effective_start
152
- w_end = matched[-1].end * ratio + effective_start
153
- new_words.append({"word": w["word"], "start": round(max(0, w_start), 3), "end": round(max(0, w_end), 3)})
154
- span_idx += n_chars
155
-
156
- return new_words if len(new_words) == len(words) else words
157
- except Exception as e:
158
- print(f" [align] skipped due to: {type(e).__name__}: {e}")
159
- return words
160
-
161
-
162
- @app.get("/health")
163
- async def health():
164
- return {"status": "ok", "message": "SubFlow backend is running"}
165
-
166
-
167
- @app.websocket("/transcribe")
168
- async def transcribe(websocket: WebSocket):
169
- await websocket.accept()
170
- tmp_path = None
171
- enhanced_path = None
172
-
173
- try:
174
- print("Step 1: waiting for bytes")
175
- audio_bytes = await websocket.receive_bytes()
176
- print(f"Step 2: received {len(audio_bytes)} bytes")
177
- await websocket.send_json({"status": "receiving"})
178
-
179
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
180
- tmp.write(audio_bytes)
181
- tmp_path = tmp.name
182
- print(f"Step 3: saved to {tmp_path}")
183
-
184
- audio_to_transcribe = tmp_path
185
-
186
- if USE_DEEPFILTERNET:
187
- await websocket.send_json({"status": "cleaning"})
188
- print("Step 3.5: cleaning audio with DeepFilterNet")
189
-
190
- audio_data, _ = load_audio(tmp_path, sr=df_state.sr())
191
- enhanced_audio = enhance(df_model, df_state, audio_data, atten_lim_db=20)
192
- enhanced_path = tmp_path.replace(".wav", "_enhanced.wav")
193
- save_audio(enhanced_path, enhanced_audio, df_state.sr())
194
- audio_to_transcribe = enhanced_path
195
- print(f"Step 3.6: cleaned audio saved to {enhanced_path}")
196
- else:
197
- print("Step 3.5: skipping DeepFilterNet (disabled for A/B test)")
198
-
199
- await websocket.send_json({"status": "transcribing"})
200
- print("Step 4: starting transcription")
201
-
202
- segments_raw, info = model.transcribe(
203
- audio_to_transcribe,
204
- language="en",
205
- word_timestamps=True,
206
- vad_filter=True,
207
- vad_parameters=dict(
208
- min_silence_duration_ms=300,
209
- speech_pad_ms=200,
210
- threshold=0.3,
211
- ),
212
- )
213
-
214
- print("Step 5: transcription done")
215
-
216
- await websocket.send_json({"status": "aligning"})
217
- print("Step 5.5: loading audio + running speaker diarization")
218
-
219
- align_waveform, sr = torchaudio.load(audio_to_transcribe)
220
- if sr != 16000:
221
- align_waveform = torchaudio.functional.resample(align_waveform, sr, 16000)
222
- if align_waveform.shape[0] > 1:
223
- align_waveform = align_waveform.mean(dim=0, keepdim=True)
224
-
225
- diarization = diarize_pipeline(audio_to_transcribe)
226
- print("Step 5.6: diarization done, now aligning words per segment")
227
-
228
- segments = []
229
- for seg in segments_raw:
230
- words = list(seg.words)
231
- if not words:
232
- continue
233
- word_dicts = [{"word": w.word, "start": w.start, "end": w.end} for w in words]
234
-
235
- cutoff = find_leading_noise_cutoff(diarization, word_dicts[0]["start"])
236
- if cutoff is not None:
237
- print(f" [noise-trim] '{seg.text.strip()[:40]}...' cutoff moved from {word_dicts[0]['start']:.2f}s to {cutoff:.2f}s")
238
-
239
- realigned = realign_word_timestamps(
240
- align_waveform, word_dicts, word_dicts[0]["start"], word_dicts[-1]["end"], cutoff_start=cutoff
241
- )
242
-
243
- mid_time = (realigned[0]["start"] + realigned[-1]["end"]) / 2
244
- speaker = get_speaker_at(diarization, mid_time)
245
-
246
- segments.append({
247
- "text": seg.text.strip(),
248
- "start": realigned[0]["start"],
249
- "end": realigned[-1]["end"],
250
- "words": realigned,
251
- "speaker": speaker,
252
- })
253
-
254
- print(f"Step 6: got {len(segments)} segments")
255
- for i, seg in enumerate(segments):
256
- print(f" seg[{i}]: '{seg['text']}' | speaker={seg['speaker']} | dur={seg['end']-seg['start']:.2f}s")
257
-
258
- await websocket.send_json({"status": "processing"})
259
-
260
- srt_content = process(segments)
261
- print(f"Step 7: SRT built")
262
-
263
- await websocket.send_json({
264
- "status": "done",
265
- "srt": srt_content,
266
- "language": info.language,
267
- "duration": info.duration,
268
- })
269
- print("Step 8: done sent!")
270
- import asyncio
271
- await asyncio.sleep(1)
272
-
273
- except WebSocketDisconnect:
274
- print("WebSocket disconnected by client")
275
- except Exception as e:
276
- print(f"ERROR: {type(e).__name__}: {e}")
277
- import traceback
278
- traceback.print_exc()
279
- try:
280
- await websocket.send_json({"status": "error", "message": str(e)})
281
- except Exception:
282
- pass
283
- finally:
284
- if tmp_path and os.path.exists(tmp_path):
285
- os.remove(tmp_path)
286
- if enhanced_path and os.path.exists(enhanced_path):
287
- os.remove(enhanced_path)
288
- try:
289
- await websocket.close()
290
- except Exception:
291
- pass