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