Spaces:
Sleeping
Sleeping
Isaacyn commited on
Commit Β·
d9f1475
1
Parent(s): 31f497d
feat: parallel ONNX+Groq inference
Browse files
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
|
| 19 |
VAD_THRESHOLD = 0.3
|
| 20 |
-
BUFFER_DURATION = 2.0
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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) ->
|
|
|
|
| 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
|
| 176 |
)[0].strip()
|
| 177 |
-
|
| 178 |
-
|
| 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 ""
|
| 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 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 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
|
| 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 |
-
|
| 268 |
-
|
| 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 |
-
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
|
| 284 |
-
|
| 285 |
-
|
|
|
|
| 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()
|