Trynitzan commited on
Commit
21a2142
·
verified ·
1 Parent(s): 88c9c9d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -64
app.py CHANGED
@@ -8,76 +8,79 @@ from fastapi.responses import JSONResponse
8
  from pyannote.audio import Pipeline
9
  import torch
10
  from pathlib import Path
11
- import librosa
12
  from datetime import datetime
13
 
14
- # קריאת טוקן מ-Secrets
15
  HF_TOKEN = os.getenv("HF_TOKEN")
16
  if not HF_TOKEN:
17
- raise RuntimeError("❌ HF_TOKEN env var is required. Add it in Settings → Variables.")
18
 
19
  # טעינת המודל
20
  device = "cuda" if torch.cuda.is_available() else "cpu"
21
  print(f"🚀 Loading model on {device}...")
22
 
23
- pipeline = Pipeline.from_pretrained(
24
- "ivrit-ai/pyannote-speaker-diarization-3.1",
25
- token=HF_TOKEN, # ✅ שונה מ-use_auth_token ל-token
26
- )
27
- pipeline.to(device)
28
- print("✅ Model loaded successfully!")
 
 
 
 
29
 
30
  app = FastAPI(
31
  title="Hebrew Speaker Diarization API",
32
- description="API for Hebrew audio speaker diarization using pyannote.audio",
33
  version="1.0.0"
34
  )
35
 
36
- # 🔒 הגבלות
37
- MAX_FILE_SIZE_MB = 50 # מקסימום 50MB
38
- MAX_DURATION_MINUTES = 15 # מקסימום 15 דקות
39
- MAX_CONCURRENT_REQUESTS = 2 # מקסימום 2 בקשות במקביל
40
 
41
- # תור בקשות
42
  processing_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
43
  active_requests = 0
44
 
45
 
46
  def ensure_wav_16k_mono(in_path: str) -> str:
47
- """ממיר קובץ אודיו ל-WAV 16kHz mono"""
48
  out_path = str(Path(in_path).with_suffix(".wav"))
49
  cmd = [
50
  "ffmpeg", "-y", "-i", in_path,
51
  "-ac", "1", "-ar", "16000",
 
52
  out_path
53
  ]
54
  result = subprocess.run(cmd, capture_output=True, text=True)
55
  if result.returncode != 0:
56
- raise RuntimeError(f"ffmpeg failed: {result.stderr}")
57
  return out_path
58
 
59
 
60
- def check_audio_duration(file_path: str) -> float:
61
- """בדיקת אורך הקובץ"""
62
  try:
63
- # חישוב אורך לפי גודל קובץ
64
- file_size = os.path.getsize(file_path)
65
- # אומדן גס: 1 דקה אודיו ≈ 1-2 MB (תלוי בקידוד)
66
- estimated_duration = file_size / (1024 * 1024) * 0.5 # דקות
67
- return estimated_duration
68
  except Exception:
69
  return 0
70
 
71
 
72
  @app.get("/")
73
  def root():
74
- """נקודת כניסה ראשית - מידע על השירות"""
75
  global active_requests
76
  return {
77
  "service": "Hebrew Speaker Diarization API",
78
  "status": "running",
79
  "device": device,
80
  "model": "ivrit-ai/pyannote-speaker-diarization-3.1",
 
81
  "limitations": {
82
  "max_file_size_mb": MAX_FILE_SIZE_MB,
83
  "max_duration_minutes": MAX_DURATION_MINUTES,
@@ -88,22 +91,23 @@ def root():
88
  "available_slots": MAX_CONCURRENT_REQUESTS - active_requests
89
  },
90
  "endpoints": {
91
- "health": "/health",
92
- "diarize": "/diarize (POST with audio file)"
 
93
  }
94
  }
95
 
96
 
97
  @app.get("/health")
98
  def health():
99
- """בדיקת בריאות השרת"""
100
  global active_requests
101
  return {
102
  "status": "healthy",
103
  "device": device,
104
- "model": "ivrit-ai/pyannote-speaker-diarization-3.1",
105
  "active_requests": active_requests,
106
- "queue_available": active_requests < MAX_CONCURRENT_REQUESTS
107
  }
108
 
109
 
@@ -112,15 +116,14 @@ async def diarize(file: UploadFile = File(...)):
112
  """
113
  זיהוי דוברים בקובץ אודיו
114
 
115
- Parameters:
116
- - file: קובץ אודיו (MP3, WAV, M4A, וכו')
117
 
118
  Returns:
119
- - JSON עם רשימת מקטעים לכל דובר
120
  """
121
  global active_requests
122
 
123
- file_size_mb = 0
124
  tmp_path = None
125
  wav_path = None
126
 
@@ -129,27 +132,27 @@ async def diarize(file: UploadFile = File(...)):
129
  content = await file.read()
130
  file_size_mb = len(content) / (1024 * 1024)
131
 
132
- # בדיקת גודל קובץ
133
  if file_size_mb > MAX_FILE_SIZE_MB:
134
  raise HTTPException(
135
  status_code=400,
136
- detail=f" קובץ גדול מדי: {file_size_mb:.1f}MB. מקסימום: {MAX_FILE_SIZE_MB}MB"
137
  )
138
 
139
  # בדיקת תור
140
  if active_requests >= MAX_CONCURRENT_REQUESTS:
141
  raise HTTPException(
142
  status_code=503,
143
- detail=f" השרת עמוס ({active_requests}/{MAX_CONCURRENT_REQUESTS} בקשות פעילות). נסה שוב בעוד דקה."
144
  )
145
 
146
- # נעילת slot
147
  async with processing_semaphore:
148
  active_requests += 1
149
 
150
  try:
151
  # שמירה זמנית
152
- suffix = Path(file.filename).suffix or ".bin"
153
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
154
  tmp.write(content)
155
  tmp_path = tmp.name
@@ -157,53 +160,59 @@ async def diarize(file: UploadFile = File(...)):
157
  # המרה ל-WAV
158
  wav_path = ensure_wav_16k_mono(tmp_path)
159
 
160
- # בדיקת אורך
161
- duration = check_audio_duration(wav_path)
162
  if duration > MAX_DURATION_MINUTES:
163
  raise HTTPException(
164
  status_code=400,
165
- detail=f" קובץ ארוך מדי: ~{duration:.1f} דקות. מקסימום: {MAX_DURATION_MINUTES} דקות"
166
  )
167
 
168
- # עיבוד בפועל
169
- print(f"🎤 Processing: {file.filename} ({file_size_mb:.1f}MB, ~{duration:.1f}min)")
170
  start_time = datetime.now()
171
 
172
  annotation = pipeline(wav_path)
173
 
174
  processing_time = (datetime.now() - start_time).total_seconds()
175
- print(f"✅ Completed in {processing_time:.1f}s")
176
 
177
- # איחוד מקטעים רציפים של אותו דובר
178
  segments = []
179
- last = None
180
 
181
  for segment, _, speaker in annotation.itertracks(yield_label=True):
182
  start = round(float(segment.start), 3)
183
  end = round(float(segment.end), 3)
184
- spk = str(speaker)
185
 
186
  # איחוד מקטעים צמודים של אותו דובר
187
- if last and last["speaker"] == spk and abs(start - last["end"]) < 0.05:
188
- last["end"] = end
 
 
189
  else:
190
- if last:
191
- segments.append(last)
192
- last = {"speaker": spk, "start": start, "end": end}
 
 
 
 
193
 
194
- if last:
195
- segments.append(last)
196
 
197
- # סינון מקטעים קצרים מדי (פחות מ-0.2 שניות)
198
- cleaned = [s for s in segments if s["end"] - s["start"] >= 0.2]
199
 
200
  return JSONResponse({
201
  "success": True,
202
- "file": file.filename,
203
  "file_size_mb": round(file_size_mb, 2),
204
  "processing_time_seconds": round(processing_time, 1),
205
- "segments": cleaned,
206
- "total_speakers": len(set(s["speaker"] for s in cleaned))
207
  })
208
 
209
  finally:
@@ -211,12 +220,11 @@ async def diarize(file: UploadFile = File(...)):
211
 
212
  except HTTPException:
213
  raise
214
- except subprocess.CalledProcessError as e:
215
- raise HTTPException(status_code=400, detail=f"❌ שגיאת המרה: {str(e)}")
216
  except Exception as e:
217
- raise HTTPException(status_code=500, detail=f"❌ שגיאת עיבוד: {str(e)}")
 
218
  finally:
219
- # ניקוי קבצים זמניים
220
  for path in [tmp_path, wav_path]:
221
  if path and os.path.exists(path):
222
  try:
 
8
  from pyannote.audio import Pipeline
9
  import torch
10
  from pathlib import Path
 
11
  from datetime import datetime
12
 
13
+ # קריאת טוכן מ-Secrets
14
  HF_TOKEN = os.getenv("HF_TOKEN")
15
  if not HF_TOKEN:
16
+ raise RuntimeError("❌ HF_TOKEN environment variable is required")
17
 
18
  # טעינת המודל
19
  device = "cuda" if torch.cuda.is_available() else "cpu"
20
  print(f"🚀 Loading model on {device}...")
21
 
22
+ try:
23
+ pipeline = Pipeline.from_pretrained(
24
+ "ivrit-ai/pyannote-speaker-diarization-3.1",
25
+ use_auth_token=HF_TOKEN,
26
+ )
27
+ pipeline.to(device)
28
+ print("✅ Model loaded successfully!")
29
+ except Exception as e:
30
+ print(f"❌ Failed to load model: {e}")
31
+ raise
32
 
33
  app = FastAPI(
34
  title="Hebrew Speaker Diarization API",
35
+ description="API for speaker diarization in Hebrew audio",
36
  version="1.0.0"
37
  )
38
 
39
+ # הגבלות
40
+ MAX_FILE_SIZE_MB = 50
41
+ MAX_DURATION_MINUTES = 15
42
+ MAX_CONCURRENT_REQUESTS = 2
43
 
44
+ # ניהול תור
45
  processing_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
46
  active_requests = 0
47
 
48
 
49
  def ensure_wav_16k_mono(in_path: str) -> str:
50
+ """ממיר אודיו ל-WAV 16kHz mono"""
51
  out_path = str(Path(in_path).with_suffix(".wav"))
52
  cmd = [
53
  "ffmpeg", "-y", "-i", in_path,
54
  "-ac", "1", "-ar", "16000",
55
+ "-loglevel", "error",
56
  out_path
57
  ]
58
  result = subprocess.run(cmd, capture_output=True, text=True)
59
  if result.returncode != 0:
60
+ raise RuntimeError(f"FFmpeg conversion failed: {result.stderr}")
61
  return out_path
62
 
63
 
64
+ def estimate_duration(file_path: str) -> float:
65
+ """אומדן אורך קובץ בדקות"""
66
  try:
67
+ file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
68
+ # אומדן: ~2MB לדקה (ממוצע)
69
+ return file_size_mb / 2.0
 
 
70
  except Exception:
71
  return 0
72
 
73
 
74
  @app.get("/")
75
  def root():
76
+ """מידע על ה-API"""
77
  global active_requests
78
  return {
79
  "service": "Hebrew Speaker Diarization API",
80
  "status": "running",
81
  "device": device,
82
  "model": "ivrit-ai/pyannote-speaker-diarization-3.1",
83
+ "version": "1.0.0",
84
  "limitations": {
85
  "max_file_size_mb": MAX_FILE_SIZE_MB,
86
  "max_duration_minutes": MAX_DURATION_MINUTES,
 
91
  "available_slots": MAX_CONCURRENT_REQUESTS - active_requests
92
  },
93
  "endpoints": {
94
+ "GET /": "This page",
95
+ "GET /health": "Health check",
96
+ "POST /diarize": "Upload audio file for diarization"
97
  }
98
  }
99
 
100
 
101
  @app.get("/health")
102
  def health():
103
+ """בדיקת בריאות"""
104
  global active_requests
105
  return {
106
  "status": "healthy",
107
  "device": device,
108
+ "model_loaded": True,
109
  "active_requests": active_requests,
110
+ "slots_available": MAX_CONCURRENT_REQUESTS - active_requests
111
  }
112
 
113
 
 
116
  """
117
  זיהוי דוברים בקובץ אודיו
118
 
119
+ Args:
120
+ file: קובץ אודיו (MP3, WAV, M4A, וכו')
121
 
122
  Returns:
123
+ JSON: רשימת מקטעים עם זיהוי דוברים
124
  """
125
  global active_requests
126
 
 
127
  tmp_path = None
128
  wav_path = None
129
 
 
132
  content = await file.read()
133
  file_size_mb = len(content) / (1024 * 1024)
134
 
135
+ # בדיקת גודל
136
  if file_size_mb > MAX_FILE_SIZE_MB:
137
  raise HTTPException(
138
  status_code=400,
139
+ detail=f"File too large: {file_size_mb:.1f}MB (max: {MAX_FILE_SIZE_MB}MB)"
140
  )
141
 
142
  # בדיקת תור
143
  if active_requests >= MAX_CONCURRENT_REQUESTS:
144
  raise HTTPException(
145
  status_code=503,
146
+ detail=f"Server busy ({active_requests}/{MAX_CONCURRENT_REQUESTS} active). Try again later."
147
  )
148
 
149
+ # עיבוד
150
  async with processing_semaphore:
151
  active_requests += 1
152
 
153
  try:
154
  # שמירה זמנית
155
+ suffix = Path(file.filename).suffix or ".tmp"
156
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
157
  tmp.write(content)
158
  tmp_path = tmp.name
 
160
  # המרה ל-WAV
161
  wav_path = ensure_wav_16k_mono(tmp_path)
162
 
163
+ # אומדן אורך
164
+ duration = estimate_duration(wav_path)
165
  if duration > MAX_DURATION_MINUTES:
166
  raise HTTPException(
167
  status_code=400,
168
+ detail=f"File too long: ~{duration:.1f} min (max: {MAX_DURATION_MINUTES} min)"
169
  )
170
 
171
+ # זיהוי דוברים
172
+ print(f"🎤 Processing: {file.filename} ({file_size_mb:.1f}MB)")
173
  start_time = datetime.now()
174
 
175
  annotation = pipeline(wav_path)
176
 
177
  processing_time = (datetime.now() - start_time).total_seconds()
178
+ print(f"✅ Done in {processing_time:.1f}s")
179
 
180
+ # בניית תוצאות
181
  segments = []
182
+ last_segment = None
183
 
184
  for segment, _, speaker in annotation.itertracks(yield_label=True):
185
  start = round(float(segment.start), 3)
186
  end = round(float(segment.end), 3)
187
+ speaker_id = str(speaker)
188
 
189
  # איחוד מקטעים צמודים של אותו דובר
190
+ if (last_segment and
191
+ last_segment["speaker"] == speaker_id and
192
+ abs(start - last_segment["end"]) < 0.1):
193
+ last_segment["end"] = end
194
  else:
195
+ if last_segment:
196
+ segments.append(last_segment)
197
+ last_segment = {
198
+ "speaker": speaker_id,
199
+ "start": start,
200
+ "end": end
201
+ }
202
 
203
+ if last_segment:
204
+ segments.append(last_segment)
205
 
206
+ # סינון מקטעים קצרים
207
+ segments = [s for s in segments if s["end"] - s["start"] >= 0.2]
208
 
209
  return JSONResponse({
210
  "success": True,
211
+ "filename": file.filename,
212
  "file_size_mb": round(file_size_mb, 2),
213
  "processing_time_seconds": round(processing_time, 1),
214
+ "total_speakers": len(set(s["speaker"] for s in segments)),
215
+ "segments": segments
216
  })
217
 
218
  finally:
 
220
 
221
  except HTTPException:
222
  raise
 
 
223
  except Exception as e:
224
+ print(f"❌ Error: {str(e)}")
225
+ raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
226
  finally:
227
+ # ניקוי
228
  for path in [tmp_path, wav_path]:
229
  if path and os.path.exists(path):
230
  try: