Isaacyn commited on
Commit
d9f1475
Β·
1 Parent(s): 31f497d

feat: parallel ONNX+Groq inference

Browse files
Files changed (1) hide show
  1. asr.py +92 -76
asr.py CHANGED
@@ -6,6 +6,7 @@ import time
6
  import wave
7
  import threading
8
  import logging
 
9
 
10
  from groq import Groq
11
  from optimum.onnxruntime import ORTModelForSpeechSeq2Seq
@@ -15,11 +16,10 @@ logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
  SAMPLE_RATE = 16000
18
- GROQ_TIMEOUT = 3.0 # dari 5.0 β€” skip Groq kalau lambat
19
  VAD_THRESHOLD = 0.3
20
- BUFFER_DURATION = 2.0 # dari 2.5 β€” buffer lebih pendek
21
  BUFFER_SAMPLES = int(SAMPLE_RATE * BUFFER_DURATION)
22
- CONFIDENCE_THRESHOLD = -0.5
23
 
24
  LOCAL_MODEL_DIR = "/app/models/whisper"
25
 
@@ -105,9 +105,14 @@ class ASRPipeline:
105
  self.is_active = True
106
  self.lock = threading.Lock()
107
 
108
- logger.info("ASRPipeline (Whisper ONNX + Groq hybrid) siap βœ“")
 
 
 
109
 
110
  def stop(self):
 
 
111
  self.rolling_buffer = np.array([], dtype=np.float32)
112
  self.last_text = ""
113
  self.is_active = True
@@ -157,7 +162,8 @@ class ASRPipeline:
157
  return False
158
  return True
159
 
160
- def whisper_transcribe(self, audio: np.ndarray) -> tuple[str, float]:
 
161
  try:
162
  audio = self.preprocess(audio)
163
  inputs = self.processor(
@@ -168,73 +174,90 @@ class ASRPipeline:
168
  generated = self.onnx_model.generate(
169
  **inputs,
170
  forced_decoder_ids=self.forced_decoder_ids,
171
- return_dict_in_generate=True,
172
- output_scores=True,
173
  )
174
  text = self.processor.batch_decode(
175
- generated.sequences, skip_special_tokens=True
176
  )[0].strip()
177
-
178
- if generated.scores:
179
- log_probs = []
180
- for score, token_id in zip(
181
- generated.scores,
182
- generated.sequences[0][len(self.forced_decoder_ids) + 1:],
183
- ):
184
- lp = torch.nn.functional.log_softmax(score[0], dim=-1)
185
- log_probs.append(lp[token_id].item())
186
- avg_logprob = float(np.mean(log_probs)) if log_probs else -1.0
187
- else:
188
- avg_logprob = -1.0
189
-
190
- logger.info(f"[ONNX] '{text}' | logprob={avg_logprob:.3f}")
191
- return text, avg_logprob
192
-
193
  except Exception as e:
194
  logger.error(f"Whisper ONNX error: {e}")
195
- return "", -99.0
196
 
197
  def groq_transcribe(self, audio: np.ndarray) -> str:
 
198
  try:
199
  audio = self.preprocess(audio)
200
  wav_bytes = audio_to_wav_bytes(audio)
201
- result = [""]
202
- success = [False]
203
- start = time.time()
204
-
205
- def call_groq():
206
- try:
207
- client = _get_groq_client()
208
- transcription = client.audio.transcriptions.create(
209
- file=("audio.wav", wav_bytes),
210
- model="whisper-large-v3-turbo",
211
- language="id",
212
- response_format="text",
213
- prompt="",
214
- )
215
- result[0] = (
216
- transcription.strip()
217
- if isinstance(transcription, str)
218
- else str(transcription).strip()
219
- )
220
- success[0] = True
221
- except Exception as e:
222
- logger.error(f"Groq error: {e}")
223
-
224
- t = threading.Thread(target=call_groq)
225
- t.start()
226
- t.join(timeout=GROQ_TIMEOUT)
227
-
228
- elapsed = time.time() - start
229
- if success[0] and result[0]:
230
- logger.info(f"[Groq {elapsed:.2f}s] >> {result[0]}")
231
- return result[0]
232
- logger.warning(f"[Groq {elapsed:.2f}s] timeout/empty")
233
- return ""
234
  except Exception as e:
235
- logger.error(f"Groq exception: {e}")
236
  return ""
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  def transcribe_chunk(self, audio_chunk: np.ndarray) -> str | None:
239
  # Empty array = signal pause dari extension
240
  if len(audio_chunk) == 0:
@@ -261,25 +284,18 @@ class ASRPipeline:
261
  return None
262
 
263
  audio_to_process = self.rolling_buffer.copy()
264
- # Overlap 33% β€” lebih cepat dari 50%
265
  self.rolling_buffer = self.rolling_buffer[BUFFER_SAMPLES // 3:]
266
 
267
- # Hybrid logic
268
- text, avg_logprob = self.whisper_transcribe(audio_to_process)
269
-
270
- if not text or avg_logprob < CONFIDENCE_THRESHOLD:
271
- source = "groq"
272
- text = self.groq_transcribe(audio_to_process)
273
- else:
274
- source = "onnx"
275
 
276
- logger.info(f"[{source}] final: '{text}'")
277
 
278
- if not text:
279
- return None
280
- if not self.is_valid_text(text):
281
- logger.info(f"Filtered: {text}")
282
- return None
283
 
284
- self.last_text = text
285
- return text.strip()
 
6
  import wave
7
  import threading
8
  import logging
9
+ import concurrent.futures
10
 
11
  from groq import Groq
12
  from optimum.onnxruntime import ORTModelForSpeechSeq2Seq
 
16
  logger = logging.getLogger(__name__)
17
 
18
  SAMPLE_RATE = 16000
19
+ GROQ_TIMEOUT = 3.0
20
  VAD_THRESHOLD = 0.3
21
+ BUFFER_DURATION = 2.0
22
  BUFFER_SAMPLES = int(SAMPLE_RATE * BUFFER_DURATION)
 
23
 
24
  LOCAL_MODEL_DIR = "/app/models/whisper"
25
 
 
105
  self.is_active = True
106
  self.lock = threading.Lock()
107
 
108
+ # Thread pool untuk parallel inference
109
+ self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)
110
+
111
+ logger.info("ASRPipeline (Whisper ONNX + Groq parallel) siap βœ“")
112
 
113
  def stop(self):
114
+ self.is_active = False
115
+ time.sleep(0.1)
116
  self.rolling_buffer = np.array([], dtype=np.float32)
117
  self.last_text = ""
118
  self.is_active = True
 
162
  return False
163
  return True
164
 
165
+ def whisper_transcribe(self, audio: np.ndarray) -> str:
166
+ """ONNX inference β€” returns text atau empty string."""
167
  try:
168
  audio = self.preprocess(audio)
169
  inputs = self.processor(
 
174
  generated = self.onnx_model.generate(
175
  **inputs,
176
  forced_decoder_ids=self.forced_decoder_ids,
 
 
177
  )
178
  text = self.processor.batch_decode(
179
+ generated, skip_special_tokens=True
180
  )[0].strip()
181
+ logger.info(f"[ONNX] '{text}'")
182
+ return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  except Exception as e:
184
  logger.error(f"Whisper ONNX error: {e}")
185
+ return ""
186
 
187
  def groq_transcribe(self, audio: np.ndarray) -> str:
188
+ """Groq inference β€” returns text atau empty string."""
189
  try:
190
  audio = self.preprocess(audio)
191
  wav_bytes = audio_to_wav_bytes(audio)
192
+ client = _get_groq_client()
193
+ transcription = client.audio.transcriptions.create(
194
+ file=("audio.wav", wav_bytes),
195
+ model="whisper-large-v3-turbo",
196
+ language="id",
197
+ response_format="text",
198
+ prompt="",
199
+ )
200
+ text = (
201
+ transcription.strip()
202
+ if isinstance(transcription, str)
203
+ else str(transcription).strip()
204
+ )
205
+ logger.info(f"[Groq] '{text}'")
206
+ return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  except Exception as e:
208
+ logger.error(f"Groq error: {e}")
209
  return ""
210
 
211
+ def parallel_transcribe(self, audio: np.ndarray) -> tuple[str, str]:
212
+ """
213
+ Jalankan ONNX dan Groq secara paralel.
214
+ Return (winner_text, winner_source) β€” siapapun yang selesai duluan.
215
+ """
216
+ result_text = ["", ""]
217
+ result_done = [False, False]
218
+ result_lock = threading.Lock()
219
+ winner = [None, None] # [text, source]
220
+ event = threading.Event()
221
+
222
+ def run_onnx():
223
+ t = self.whisper_transcribe(audio)
224
+ with result_lock:
225
+ result_text[0] = t
226
+ result_done[0] = True
227
+ if winner[0] is None and t:
228
+ winner[0] = t
229
+ winner[1] = "onnx"
230
+ event.set()
231
+
232
+ def run_groq():
233
+ t = self.groq_transcribe(audio)
234
+ with result_lock:
235
+ result_text[1] = t
236
+ result_done[1] = True
237
+ if winner[0] is None and t:
238
+ winner[0] = t
239
+ winner[1] = "groq"
240
+ event.set()
241
+
242
+ t1 = threading.Thread(target=run_onnx, daemon=True)
243
+ t2 = threading.Thread(target=run_groq, daemon=True)
244
+ t1.start()
245
+ t2.start()
246
+
247
+ # Tunggu siapapun yang selesai duluan, max GROQ_TIMEOUT + 1s
248
+ event.wait(timeout=GROQ_TIMEOUT + 1.0)
249
+
250
+ # Kalau event tidak di-set (keduanya gagal/timeout)
251
+ if winner[0] is None:
252
+ # Tunggu sebentar lagi untuk ONNX
253
+ t1.join(timeout=2.0)
254
+ with result_lock:
255
+ if result_text[0]:
256
+ winner[0] = result_text[0]
257
+ winner[1] = "onnx_late"
258
+
259
+ return winner[0] or "", winner[1] or "none"
260
+
261
  def transcribe_chunk(self, audio_chunk: np.ndarray) -> str | None:
262
  # Empty array = signal pause dari extension
263
  if len(audio_chunk) == 0:
 
284
  return None
285
 
286
  audio_to_process = self.rolling_buffer.copy()
 
287
  self.rolling_buffer = self.rolling_buffer[BUFFER_SAMPLES // 3:]
288
 
289
+ # Parallel inference β€” di luar lock biar tidak blocking
290
+ text, source = self.parallel_transcribe(audio_to_process)
 
 
 
 
 
 
291
 
292
+ logger.info(f"[{source}] final: '{text}'")
293
 
294
+ if not text:
295
+ return None
296
+ if not self.is_valid_text(text):
297
+ logger.info(f"Filtered: {text}")
298
+ return None
299
 
300
+ self.last_text = text
301
+ return text.strip()