AliJabbarpour commited on
Commit
93067e1
·
verified ·
1 Parent(s): 927c39a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +37 -69
app.py CHANGED
@@ -9,6 +9,11 @@ import logging
9
  import torch
10
  import torchaudio
11
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
 
 
 
 
 
12
 
13
 
14
  class HealthAndStatusFilter(logging.Filter):
@@ -19,11 +24,6 @@ class HealthAndStatusFilter(logging.Filter):
19
 
20
 
21
  logging.getLogger("uvicorn.access").addFilter(HealthAndStatusFilter())
22
- from fastapi.middleware.cors import CORSMiddleware
23
- from faster_whisper import WhisperModel
24
- from df.enhance import enhance, init_df, load_audio, save_audio
25
- from pyannote.audio import Pipeline as DiarizationPipeline
26
- from pipeline.postprocess import process
27
 
28
  torch.set_num_threads(2) # جلوی رقابت بی‌فایده‌ی رشته‌ها روی CPU محدود رو می‌گیره
29
 
@@ -62,14 +62,14 @@ print("Speaker-diarization model ready!")
62
 
63
  MIN_NOISE_BLOCK_SEC = 1.0
64
  JOB_TTL_SECONDS = 3 * 60 * 60 # کارهای قدیمی بعد از ۳ ساعت پاک میشن (برای فایل‌های سنگین که ممکنه پردازششون طولانی بشه)
65
- STAGE_TIMEOUT_SEC = 1200 # سقف زمانی برای هر مرحله‌ی سنگین (۲۰ دقیقه - فقط محافظ آخر در برابر گیرکردن واقعی، نه محدودیت عادی)
66
 
67
  # ─── نگهداری وضعیت کارها (Jobs) توی حافظه ───────────────────────────────────
68
  jobs = {}
69
  jobs_lock = threading.Lock()
70
 
71
 
72
- def new_job():
73
  job_id = uuid.uuid4().hex
74
  tmp_dir = tempfile.mkdtemp(prefix=f"subflow_{job_id}_")
75
  with jobs_lock:
@@ -83,6 +83,7 @@ def new_job():
83
  "language": None,
84
  "duration": None,
85
  "error": None,
 
86
  }
87
  return job_id
88
 
@@ -113,7 +114,7 @@ def cleanup_expired_jobs():
113
  print(f"[cleanup] removed expired job {jid}")
114
 
115
 
116
- # ─── منطق تشخیص گوینده و هم‌ترازی (بدون تغییر نسبت به نسخه‌ی قبلی) ──────────
117
 
118
  def build_speaker_lookup(diarization):
119
  """
@@ -248,6 +249,7 @@ def realign_word_timestamps(waveform_16k, words, whisper_start, whisper_end, cut
248
  print(f" [align] skipped due to: {type(e).__name__}: {e}")
249
  return words
250
 
 
251
  def run_transcription(audio_path):
252
  """
253
  نکته‌ی مهم: model.transcribe فوراً برنمی‌گرده - یه نتیجه‌ی "تنبل" میده که کار
@@ -269,52 +271,11 @@ def run_transcription(audio_path):
269
  return segments_list, info
270
 
271
 
272
- def probe_speaker_count(audio_path, job_id=None):
273
- """
274
- قبل از اجرای تشخیص گوینده‌ی کامل (که سنگین و کنده)، یه آزمایش سریع انجام می‌ده:
275
- از ۳ نقطه‌ی پخش‌شده در طول فایل (نه فقط اول)، هرکدوم یه تیکه‌ی ۲۰ ثانیه‌ای برمی‌داره
276
- و روی همون تیکه‌های کوتاه تشخیص گوینده اجرا می‌کنه.
277
- اگه هر سه فقط یه گوینده نشون بدن، برمی‌گردونه True (یعنی «به‌احتمال زیاد تک‌گوینده‌ست»)
278
- وگرنه False (یعنی «باید تشخیص کامل روی کل فایل اجرا بشه»).
279
- """
280
- info = torchaudio.info(audio_path)
281
- duration = info.num_frames / info.sample_rate
282
- probe_len = 20.0
283
-
284
- if duration <= probe_len * 3:
285
- # فایل خیلی کوتاهه، آزمایش جداگانه فایده‌ای نداره - مستقیم برو سراغ تشخیص کامل
286
- return False
287
-
288
- fractions = (0.1, 0.5, 0.85)
289
- waveform, sr = torchaudio.load(audio_path)
290
-
291
- for frac in fractions:
292
- center = duration * frac
293
- start_sec = max(0, center - probe_len / 2)
294
- end_sec = min(duration, start_sec + probe_len)
295
- start_sample = int(start_sec * sr)
296
- end_sample = int(end_sec * sr)
297
- clip = waveform[:, start_sample:end_sample]
298
-
299
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
300
- torchaudio.save(tmp.name, clip, sr)
301
- clip_path = tmp.name
302
-
303
- try:
304
- probe_result = diarize_pipeline(clip_path, max_speakers=8)
305
- speakers_found = {speaker for _, _, speaker in probe_result.itertracks(yield_label=True)}
306
- print(f" [speaker-probe] window at {frac:.0%} of file: {len(speakers_found)} speaker(s)")
307
- if len(speakers_found) > 1:
308
- return False
309
- finally:
310
- os.remove(clip_path)
311
-
312
- return True
313
-
314
  class SingleSpeakerResult:
315
  """
316
  یه نتیجه‌ی «قلابی» که همون شکل خروجی diarize_pipeline رو تقلید می‌کنه (متد itertracks داره)
317
  ولی هیچ پردازش واقعی‌ای روی کل فایل انجام نداده - فقط می‌گه «همه‌جا همون یه گوینده‌ست».
 
318
  """
319
  def __init__(self, duration):
320
  self.duration = duration
@@ -327,18 +288,13 @@ class SingleSpeakerResult:
327
  yield FakeTurn(0, self.duration), None, "SPEAKER_00"
328
 
329
 
330
- def run_diarization(audio_path, job_id=None):
331
- if job_id is not None:
332
- update_job(job_id, detail="بررسی سریع تعداد گوینده‌ها...")
333
- single_speaker = probe_speaker_count(audio_path, job_id)
334
-
335
- if single_speaker:
336
- print(" [speaker-probe] looks single-speaker across all 3 windows — skipping full diarization entirely")
337
  info = torchaudio.info(audio_path)
338
  duration = info.num_frames / info.sample_rate
339
  return SingleSpeakerResult(duration)
340
 
341
- print(" [speaker-probe] multiple speakers detected — running full search")
342
  return diarize_pipeline(audio_path, max_speakers=8)
343
 
344
 
@@ -377,9 +333,10 @@ class ProcessingTimeout(Exception):
377
 
378
  def run_blocking_with_timeout(job_id, blocking_fn, *args, status, detail, max_seconds=STAGE_TIMEOUT_SEC):
379
  """
380
- نسخه‌ی هم‌زمان (نه async) - چون این خودش داخل یه thread پس‌زمینه اجرا میشه،
381
- نیازی به هماهنگی با event loop نداره. وضعیت Job رو مستقیم آپدیت می‌کنه
382
- تا polling بتونه ببینتش.
 
383
  """
384
  result_holder = {}
385
  error_holder = {}
@@ -412,6 +369,7 @@ def process_job(job_id):
412
  """کل پایپلاین پردازش - این تابع توی یه thread پس‌زمینه اجرا میشه، نه توی درخواست HTTP."""
413
  job = get_job(job_id)
414
  tmp_dir = job["tmp_dir"]
 
415
  tmp_path = os.path.join(tmp_dir, "input.wav")
416
  enhanced_path = None
417
 
@@ -431,23 +389,27 @@ def process_job(job_id):
431
  enhanced_path = os.path.join(tmp_dir, "enhanced.wav")
432
  save_audio(enhanced_path, enhanced_audio, df_state.sr())
433
  audio_to_transcribe = enhanced_path
434
-
435
  segments_raw_list, info = run_blocking_with_timeout(
436
  job_id, run_transcription, audio_to_transcribe,
437
  status="transcribing", detail="مدل large-v3 در حال شنیدن صداست",
438
  )
439
  update_job(job_id, detail=f"{len(segments_raw_list)} جمله تشخیص داده شد")
440
-
441
  align_waveform, sr = torchaudio.load(audio_to_transcribe)
442
  if sr != 16000:
443
  align_waveform = torchaudio.functional.resample(align_waveform, sr, 16000)
444
  if align_waveform.shape[0] > 1:
445
  align_waveform = align_waveform.mean(dim=0, keepdim=True)
446
 
447
- diarization = run_blocking_with_timeout(
448
- job_id, run_diarization, audio_to_transcribe, job_id,
449
- status="aligning", detail="در حال تشخیص گوینده‌ها",
450
- )
 
 
 
 
451
  update_job(job_id, detail="گوینده‌ها شناسایی شدند")
452
 
453
  segments = run_blocking_with_timeout(
@@ -492,9 +454,15 @@ async def health():
492
 
493
 
494
  @app.post("/upload/start")
495
- async def upload_start():
496
  cleanup_expired_jobs()
497
- job_id = new_job()
 
 
 
 
 
 
498
  return {"job_id": job_id}
499
 
500
 
 
9
  import torch
10
  import torchaudio
11
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from faster_whisper import WhisperModel
14
+ from df.enhance import enhance, init_df, load_audio, save_audio
15
+ from pyannote.audio import Pipeline as DiarizationPipeline
16
+ from pipeline.postprocess import process
17
 
18
 
19
  class HealthAndStatusFilter(logging.Filter):
 
24
 
25
 
26
  logging.getLogger("uvicorn.access").addFilter(HealthAndStatusFilter())
 
 
 
 
 
27
 
28
  torch.set_num_threads(2) # جلوی رقابت بی‌فایده‌ی رشته‌ها روی CPU محدود رو می‌گیره
29
 
 
62
 
63
  MIN_NOISE_BLOCK_SEC = 1.0
64
  JOB_TTL_SECONDS = 3 * 60 * 60 # کارهای قدیمی بعد از ۳ ساعت پاک میشن (برای فایل‌های سنگین که ممکنه پردازششون طولانی بشه)
65
+ STAGE_TIMEOUT_SEC = 1200 # سقف زمانی برای هر مرحله‌ی سنگین (۲۰ دقیقه - فقط محافظ آخر در برابر گیرکردن واقعی)
66
 
67
  # ─── نگهداری وضعیت کارها (Jobs) توی حافظه ───────────────────────────────────
68
  jobs = {}
69
  jobs_lock = threading.Lock()
70
 
71
 
72
+ def new_job(single_speaker_mode=False):
73
  job_id = uuid.uuid4().hex
74
  tmp_dir = tempfile.mkdtemp(prefix=f"subflow_{job_id}_")
75
  with jobs_lock:
 
83
  "language": None,
84
  "duration": None,
85
  "error": None,
86
+ "single_speaker_mode": single_speaker_mode,
87
  }
88
  return job_id
89
 
 
114
  print(f"[cleanup] removed expired job {jid}")
115
 
116
 
117
+ # ─── منطق تشخیص گوینده و هم‌ترازی ────────────────────────────────────────────
118
 
119
  def build_speaker_lookup(diarization):
120
  """
 
249
  print(f" [align] skipped due to: {type(e).__name__}: {e}")
250
  return words
251
 
252
+
253
  def run_transcription(audio_path):
254
  """
255
  نکته‌ی مهم: model.transcribe فوراً برنمی‌گرده - یه نتیجه‌ی "تنبل" میده که کار
 
271
  return segments_list, info
272
 
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  class SingleSpeakerResult:
275
  """
276
  یه نتیجه‌ی «قلابی» که همون شکل خروجی diarize_pipeline رو تقلید می‌کنه (متد itertracks داره)
277
  ولی هیچ پردازش واقعی‌ای روی کل فایل انجام نداده - فقط می‌گه «همه‌جا همون یه گوینده‌ست».
278
+ فقط وقتی استفاده میشه که خودِ کاربر صراحتاً بگه فایل تک‌گوینده‌ست.
279
  """
280
  def __init__(self, duration):
281
  self.duration = duration
 
288
  yield FakeTurn(0, self.duration), None, "SPEAKER_00"
289
 
290
 
291
+ def run_diarization(audio_path, single_speaker_mode=False, job_id=None):
292
+ if single_speaker_mode:
293
+ print(" [diarization] single-speaker mode selected by user — skipping full diarization entirely")
 
 
 
 
294
  info = torchaudio.info(audio_path)
295
  duration = info.num_frames / info.sample_rate
296
  return SingleSpeakerResult(duration)
297
 
 
298
  return diarize_pipeline(audio_path, max_speakers=8)
299
 
300
 
 
333
 
334
  def run_blocking_with_timeout(job_id, blocking_fn, *args, status, detail, max_seconds=STAGE_TIMEOUT_SEC):
335
  """
336
+ یه تابع سنگین و بلاک‌کننده رو توی یه thread جدا اجرا می‌کنه، و تا وقتی تموم بشه،
337
+ هر چند ثانیه یه پیام "هنوز کار می‌کنم" روی همون status می‌فرسته - این هم جلوی
338
+ قطع‌شدن اتصال به‌خاطر سکوت طولانی رو می‌گیره، هم UI رو زنده نگه می‌داره.
339
+ اگه از max_seconds بیشتر طول بکشه، به‌جای قفل ابدی، خطای مشخص میده.
340
  """
341
  result_holder = {}
342
  error_holder = {}
 
369
  """کل پایپلاین پردازش - این تابع توی یه thread پس‌زمینه اجرا میشه، نه توی درخواست HTTP."""
370
  job = get_job(job_id)
371
  tmp_dir = job["tmp_dir"]
372
+ single_speaker_mode = job.get("single_speaker_mode", False)
373
  tmp_path = os.path.join(tmp_dir, "input.wav")
374
  enhanced_path = None
375
 
 
389
  enhanced_path = os.path.join(tmp_dir, "enhanced.wav")
390
  save_audio(enhanced_path, enhanced_audio, df_state.sr())
391
  audio_to_transcribe = enhanced_path
392
+
393
  segments_raw_list, info = run_blocking_with_timeout(
394
  job_id, run_transcription, audio_to_transcribe,
395
  status="transcribing", detail="مدل large-v3 در حال شنیدن صداست",
396
  )
397
  update_job(job_id, detail=f"{len(segments_raw_list)} جمله تشخیص داده شد")
398
+
399
  align_waveform, sr = torchaudio.load(audio_to_transcribe)
400
  if sr != 16000:
401
  align_waveform = torchaudio.functional.resample(align_waveform, sr, 16000)
402
  if align_waveform.shape[0] > 1:
403
  align_waveform = align_waveform.mean(dim=0, keepdim=True)
404
 
405
+ if single_speaker_mode:
406
+ diarization = run_diarization(audio_to_transcribe, single_speaker_mode=True)
407
+ update_job(job_id, status="aligning", detail="تشخیص گوینده رد شد (حالت تک‌گوینده)")
408
+ else:
409
+ diarization = run_blocking_with_timeout(
410
+ job_id, run_diarization, audio_to_transcribe, False, job_id,
411
+ status="aligning", detail="در حال تشخیص گوینده‌ها",
412
+ )
413
  update_job(job_id, detail="گوینده‌ها شناسایی شدند")
414
 
415
  segments = run_blocking_with_timeout(
 
454
 
455
 
456
  @app.post("/upload/start")
457
+ async def upload_start(request: Request):
458
  cleanup_expired_jobs()
459
+ body = {}
460
+ try:
461
+ body = await request.json()
462
+ except Exception:
463
+ pass
464
+ single_speaker_mode = bool(body.get("single_speaker_mode", False))
465
+ job_id = new_job(single_speaker_mode)
466
  return {"job_id": job_id}
467
 
468