File size: 8,083 Bytes
93f8a11 dc1b84b 93f8a11 769ae14 93f8a11 769ae14 93f8a11 c7a7672 a027de5 c7a7672 93f8a11 4856a1c 93f8a11 769ae14 dc1b84b c7a7672 dc1b84b c7a7672 dc1b84b c7a7672 dc1b84b a027de5 dc1b84b 4a3fec4 dc1b84b 4a3fec4 dc1b84b a027de5 dc1b84b 4a3fec4 a027de5 c7a7672 a027de5 dc1b84b 4a3fec4 dc1b84b a027de5 dc1b84b a027de5 dc1b84b 769ae14 93f8a11 769ae14 a027de5 769ae14 a027de5 93f8a11 769ae14 93f8a11 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """Native CUDA runtimes are created inside the allocated ZeroGPU worker."""
import spaces # Initialize ZeroGPU before importing CUDA-dependent libraries.
import json
import math
import os
import time
from functools import lru_cache
from types import SimpleNamespace
import onnxruntime as ort
import torch
from huggingface_hub import snapshot_download
from transformers import WhisperProcessor
from onnx_runtime import OpenPAOnnx
from asr import transcribe_with_whisperx, prepare_asr_mel
from asr_cache import TranscriptCache
ASR_CACHE = TranscriptCache(max_entries=16, ttl_seconds=600)
def asr_beam_size():
return max(1, min(5, int(os.getenv("OPENPA_ASR_BEAM_SIZE", "5"))))
if "CUDAExecutionProvider" not in ort.get_available_providers():
raise RuntimeError("CUDA ONNX Runtime wheel was overwritten by a CPU-only installation")
MODEL_CACHE = os.getenv("HF_HOME", "/tmp/openpa-hf")
MODEL_REVISION = "13a4f4d027c66bf78c36f58f7cf77b06becb67c0"
ASR_REVISION = "edaa852ec7e145841d8ffdb056a99866b5f0a478"
@lru_cache(maxsize=2)
def prepare_artifacts(reference_free):
"""Download on the CPU process, outside GPU quota usage."""
model = os.getenv("OPENPA_MODEL_DIR") or snapshot_download(
"fuann/openpa-onnx", revision=MODEL_REVISION,
token=os.getenv("OPENPA_HF_TOKEN"), cache_dir=MODEL_CACHE,
allow_patterns=["model.onnx", "model.onnx.data", "*config.json", "tokenizer.json"],
)
asr = None
if reference_free:
asr = snapshot_download(
"Systran/faster-whisper-large-v3", revision=ASR_REVISION,
token=False, cache_dir=MODEL_CACHE,
allow_patterns=["model.bin", "config.json", "tokenizer.json",
"vocabulary.json", "preprocessor_config.json"],
)
return model, asr
@lru_cache(maxsize=1)
def get_processor(model_dir):
return WhisperProcessor.from_pretrained(model_dir, local_files_only=True)
def prepare_request(waveform, rate, text, model_dir, asr_dir):
"""Cache CPU-only processing and imports before allocating a GPU."""
processor = get_processor(model_dir)
features = processor.feature_extractor(
waveform, sampling_rate=rate, return_tensors="np"
).input_features.astype("float32")
if asr_dir:
import whisperx
from whisperx.vads.vad import Vad
return processor, features
def gpu_duration(*args, **kwargs):
return int(os.getenv("OPENPA_GPU_SECONDS", "90"))
@lru_cache(maxsize=1)
def get_cpu_scorer(model_dir):
"""Keep the CPU session alive in the parent; no CUDA resource is retained."""
return OpenPAOnnx(model_dir, threads=int(os.getenv("OPENPA_THREADS", "2")),
provider="CPUExecutionProvider", processor=get_processor(model_dir))
def asr_duration(waveform, asr_dir, mel_features=None):
return 12 + math.ceil(len(waveform) / 16000 / 5)
@spaces.GPU(duration=asr_duration)
def transcribe_gpu(waveform, asr_dir, mel_features=None):
started = time.monotonic()
if not torch.cuda.is_available():
raise RuntimeError("ZeroGPU did not allocate a CUDA device")
options = SimpleNamespace(asr_model=asr_dir, asr_compute_type="float16",
asr_batch_size=1, asr_beam_size=asr_beam_size(), local_files_only=True,
mel_features=mel_features)
transcript, compute_type = transcribe_with_whisperx(waveform, options, torch.device("cuda"))
elapsed = round(time.monotonic() - started, 3)
print("OPENPA_ASR_GPU_SUCCESS " + json.dumps({"seconds": elapsed,
"device": torch.cuda.get_device_name(0), "model": "large-v3", "beam_size": asr_beam_size()}), flush=True)
return transcript, compute_type, elapsed
def run_assessment(waveform, rate, text, model_dir, asr_dir, processor, features, on_stage=None):
notify = on_stage or (lambda stage: None)
if os.getenv("OPENPA_SCORING_DEVICE", "cpu") == "cuda":
notify("transcribe" if text is None else "score")
return infer_gpu(waveform, rate, text, model_dir, asr_dir, processor, features)
started = time.monotonic()
scorer = get_cpu_scorer(model_dir)
loaded = time.monotonic()
reference_free = text is None
asr_seconds = 0
asr_cache_hit = False
if reference_free:
notify("transcribe")
cache_key = ASR_CACHE.key(waveform, (ASR_REVISION, asr_dir, asr_beam_size(), "float16"))
cached = ASR_CACHE.get(cache_key)
if cached is None:
# WhisperX's log-mel transform runs on CPU. Keep it outside GPU quota.
mel_features = prepare_asr_mel(waveform)
text, compute_type, asr_seconds = transcribe_gpu(waveform, asr_dir, mel_features)
ASR_CACHE.put(cache_key, (text, compute_type))
else:
text, compute_type = cached
asr_cache_hit = True
transcribed = time.monotonic()
notify("score")
inputs, transcript, words = scorer.prepare(waveform, rate, text, input_features=features)
result = scorer.format_scores(scorer.predict(inputs), transcript, words)
scored = time.monotonic()
result["mode"] = "reference-free" if reference_free else "reference-text"
if reference_free:
result["asr"] = {"backend": "whisperx", "model": "large-v3",
"compute_type": compute_type, "beam_size": asr_beam_size()}
result["execution"] = {
"providers": scorer.providers, "asr_cache_hit": asr_cache_hit,
"timings": {"session_load": round(loaded - started, 3), "asr": asr_seconds,
"asr_wall": round(transcribed - loaded, 3),
"score": round(scored - transcribed, 3)},
"compute_seconds": round((loaded - started) + asr_seconds + (scored - transcribed), 3),
"wall_seconds": round(scored - started, 3),
}
print("OPENPA_HYBRID_SUCCESS " + json.dumps({"mode": result["mode"], **result["execution"]}), flush=True)
return result
@spaces.GPU(duration=gpu_duration)
def infer_gpu(waveform, rate, text, model_dir, asr_dir, processor, features):
started = time.monotonic()
if not torch.cuda.is_available():
raise RuntimeError("ZeroGPU did not allocate a CUDA device")
ort.preload_dlls()
ready = time.monotonic()
scorer = OpenPAOnnx(model_dir, threads=2, provider="CUDAExecutionProvider",
processor=processor, cuda_options={"cudnn_conv_algo_search": "HEURISTIC"})
loaded = time.monotonic()
reference_free = text is None
if reference_free:
options = SimpleNamespace(asr_model=asr_dir, asr_compute_type="float16",
asr_batch_size=1, asr_beam_size=asr_beam_size(), local_files_only=True)
text, compute_type = transcribe_with_whisperx(waveform, options, torch.device("cuda"))
transcribed = time.monotonic()
inputs, transcript, words = scorer.prepare(waveform, rate, text, input_features=features)
prepared = time.monotonic()
predictions = scorer.predict(inputs)
predicted = time.monotonic()
result = scorer.format_scores(predictions, transcript, words)
result["mode"] = "reference-free" if reference_free else "reference-text"
if reference_free:
result["asr"] = {"backend": "whisperx", "model": "large-v3",
"compute_type": compute_type, "beam_size": asr_beam_size()}
result["execution"] = {
"providers": scorer.providers, "device": torch.cuda.get_device_name(0),
"seconds": round(time.monotonic() - started, 3),
"timings": {"cuda_ready": round(ready - started, 3),
"session_load": round(loaded - ready, 3),
"asr": round(transcribed - loaded, 3),
"token_preparation": round(prepared - transcribed, 3),
"forward": round(predicted - prepared, 3)},
"cuda_options": scorer.session.get_provider_options().get("CUDAExecutionProvider", {}),
}
print("OPENPA_GPU_SUCCESS " + json.dumps({
"mode": result["mode"], **result["execution"]
}), flush=True)
return result
|