1577-2 / backend /asr.py
MickMick102's picture
feat: Enhance audio processing and transcription features
377f697
Raw
History Blame
6.32 kB
"""Speech-to-text utilities with graceful fallbacks and dynamic stage switching."""
from __future__ import annotations
import io
import os
import wave
from threading import Lock
from typing import Any, Literal, Optional
import numpy as np
try:
from openai import OpenAI
except ModuleNotFoundError:
OpenAI = None # type: ignore[assignment]
from backend.utils import device
import nemo.collections.asr as nemo_asr
try:
import torch
from transformers import pipeline
except ModuleNotFoundError: # PyTorch or transformers not available on Python 3.13 wheels
torch = None # type: ignore
pipeline = None # type: ignore
try:
from google.cloud import speech
except ModuleNotFoundError:
speech = None # type: ignore
_ASR_PIPELINE = None
_ASR_STAGE: Literal["typhoon", "gpt"] = "typhoon"
_ASR_STAGE_LOCK: Lock = Lock()
def _huggingface_device() -> int | str | None:
if device == "cuda":
return 0
if device == "mps":
return "mps"
return "cpu"
def _initialize_typhoon_pipeline():
if torch is None or pipeline is None:
return None
print(f"Using device: {device}")
print("Initializing Typhoon ASR pipeline...")
asr_model = nemo_asr.models.ASRModel.from_pretrained(
model_name="scb10x/typhoon-asr-realtime",
map_location=device,
)
print("Typhoon ASR pipeline initialized.")
return asr_model
def _initialize_gpt_client() -> Optional[Any]:
if OpenAI is None:
print("openai package not available; GPT ASR unavailable.")
return None
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
print("OPENAI_API_KEY not found; GPT ASR unavailable.")
return None
try:
return OpenAI(api_key=api_key)
except Exception as exc:
print(f"Failed to initialise GPT ASR client: {exc}")
return None
_GPT_ASR_MODEL = os.getenv("GPT_ASR_MODEL", "gpt-4o-mini-transcribe")
_GPT_CLIENT = _initialize_gpt_client()
def set_asr_stage(stage: str) -> None:
"""Update the active ASR stage."""
normalized_stage = stage.lower()
if normalized_stage not in {"typhoon", "gpt"}:
raise ValueError(f"Unsupported ASR stage '{stage}'")
global _ASR_STAGE
with _ASR_STAGE_LOCK:
if _ASR_STAGE != normalized_stage:
print(f"Switching ASR stage to: {normalized_stage}")
_ASR_STAGE = normalized_stage # type: ignore[assignment]
def get_asr_stage() -> Literal["typhoon", "gpt"]:
"""Return the current ASR stage."""
with _ASR_STAGE_LOCK:
return _ASR_STAGE
def _transcribe_with_pipeline(audio_array: np.ndarray) -> str:
output = _ASR_PIPELINE(audio_array) # type: ignore[operator]
if isinstance(output, dict):
text = output.get("text", "")
else:
text = str(output)
return text.replace("ทางลัด", "ทางรัฐ")
def _transcribe_with_typhoon(audio_array: np.ndarray) -> str:
if _ASR_TYPHOON is None:
raise RuntimeError("Typhoon ASR is unavailable")
result = _ASR_TYPHOON.transcribe(audio=audio_array)
if isinstance(result, list):
transcription = " ".join(result)
else:
transcription = str(result)
return transcription.strip()
def _transcribe_with_google(audio_array: np.ndarray) -> str:
if speech is None:
raise RuntimeError("google-cloud-speech is not available")
int16_audio = (audio_array * 32767.0).astype(np.int16)
audio_bytes = int16_audio.tobytes()
client = speech.SpeechClient()
audio_config = speech.RecognitionConfig(
encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
sample_rate_hertz=16000,
language_code="th-TH",
alternative_language_codes=["en-US"],
model="telephony",
)
audio_data = speech.RecognitionAudio(content=audio_bytes)
response = client.recognize(config=audio_config, audio=audio_data)
transcription = " ".join(
result.alternatives[0].transcript for result in response.results
)
return transcription
def _transcribe_with_gpt(audio_array: np.ndarray) -> str:
if _GPT_CLIENT is None:
raise RuntimeError("GPT ASR client is unavailable")
normalized = np.asarray(audio_array, dtype=np.float32)
if normalized.ndim > 1:
normalized = normalized.squeeze()
normalized = np.clip(normalized, -1.0, 1.0)
int16_audio = (normalized * 32767.0).astype(np.int16)
buffer = io.BytesIO()
with wave.open(buffer, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(16000)
wav_file.writeframes(int16_audio.tobytes())
buffer.seek(0)
response = _GPT_CLIENT.audio.transcriptions.create(
model=_GPT_ASR_MODEL,
file=("audio.wav", buffer.read(), "audio/wav"),
)
text = getattr(response, "text", "")
if not text and isinstance(response, dict):
text = response.get("text", "")
return text.strip()
_ASR_TYPHOON = _initialize_typhoon_pipeline()
def transcribe_typhoon(path: str) -> str:
text = _ASR_TYPHOON.transcribe(path)
if text[0].text:
return text[0].text
else :
print(text)
return ""
def transcribe_audio(audio_array: np.ndarray) -> str:
"""Transcribe user audio with the best available backend based on the current stage."""
if audio_array is None or not np.any(audio_array):
return ""
stage = get_asr_stage()
if stage == "gpt":
try:
transcription = _transcribe_with_gpt(audio_array)
if transcription:
return transcription.replace("ทางลัด", "ทางรัฐ")
except Exception as exc:
print(f"GPT ASR failed: {exc}; falling back to Typhoon.")
if _ASR_TYPHOON is not None:
try:
transcription = _transcribe_with_typhoon(audio_array)
if transcription:
return transcription.replace("ทางลัด", "ทางรัฐ")
except Exception as exc:
print(f"Typhoon ASR pipeline failed: {exc}")
try:
return _transcribe_with_google(audio_array).replace("ทางลัด", "ทางรัฐ")
except Exception as exc:
print(f"ASR fallback failed: {exc}")
return ""