mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
* test(nvidia_riva): audit #41021 Adds an owned Riva ASR gRPC server and provider integration cells for the riva_offline request-body ban, the offline and streaming paths, the per-deployment caller opt-in, and the 413 and 408 error mappings * test(nvidia_riva): check word timing requests and audio samples in the Riva double --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
143 lines
5.1 KiB
Python
143 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import threading
|
|
import wave
|
|
from array import array
|
|
from collections.abc import Iterable, Iterator
|
|
from concurrent import futures
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from queue import SimpleQueue
|
|
from typing import Final
|
|
|
|
import grpc
|
|
from riva.client.proto import riva_asr_pb2, riva_asr_pb2_grpc
|
|
|
|
SAMPLE_RATE_HZ: Final = 16000
|
|
OFFLINE_ONLY_REFUSAL: Final = (
|
|
"Error: Unavailable model requested given these parameters: language_code=en-US; sample_rate=16000; type=online;"
|
|
)
|
|
_STALL_LIMIT_SECONDS: Final = 120
|
|
|
|
|
|
class Rpc(Enum):
|
|
RECOGNIZE = "Recognize"
|
|
STREAMING = "StreamingRecognize"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Word:
|
|
word: str
|
|
start_ms: int
|
|
end_ms: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RivaCall:
|
|
rpc: Rpc
|
|
audio: bytes
|
|
cancelled: bool
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RivaServer:
|
|
address: str
|
|
calls: SimpleQueue[RivaCall]
|
|
|
|
def drain(self) -> tuple[RivaCall, ...]:
|
|
return tuple(self.calls.get_nowait() for _ in range(self.calls.qsize()))
|
|
|
|
|
|
def _clip_samples(seconds: float) -> array[int]:
|
|
return array("h", (((index * 37) % 2001 - 1000) * 16 for index in range(int(seconds * SAMPLE_RATE_HZ))))
|
|
|
|
|
|
def pcm_wav(seconds: float) -> bytes:
|
|
buffer: Final = io.BytesIO()
|
|
with wave.open(buffer, "wb") as writer:
|
|
writer.setnchannels(1)
|
|
writer.setsampwidth(2)
|
|
writer.setframerate(SAMPLE_RATE_HZ)
|
|
writer.writeframes(_clip_samples(seconds).tobytes())
|
|
return buffer.getvalue()
|
|
|
|
|
|
def pcm_bytes(seconds: float) -> int:
|
|
return int(seconds * SAMPLE_RATE_HZ) * 2
|
|
|
|
|
|
def carries_clip(audio: bytes, seconds: float) -> bool:
|
|
sent: Final = _clip_samples(seconds)
|
|
if len(audio) != len(sent) * 2:
|
|
return False
|
|
received: Final = array("h", audio)
|
|
return all(abs(left - right) <= 1 for left, right in zip(sent, received, strict=True))
|
|
|
|
|
|
class _Asr(riva_asr_pb2_grpc.RivaSpeechRecognitionServicer):
|
|
def __init__(self, words: tuple[Word, ...], offline_only: bool, stall: bool, calls: SimpleQueue[RivaCall]) -> None:
|
|
self._words: Final = words
|
|
self._offline_only: Final = offline_only
|
|
self._stall: Final = stall
|
|
self._calls: Final = calls
|
|
|
|
def _alternative(self, config: riva_asr_pb2.RecognitionConfig) -> riva_asr_pb2.SpeechRecognitionAlternative:
|
|
timed: Final = self._words if config.enable_word_time_offsets else ()
|
|
return riva_asr_pb2.SpeechRecognitionAlternative(
|
|
transcript=" ".join(word.word for word in self._words),
|
|
confidence=0.9,
|
|
words=[
|
|
riva_asr_pb2.WordInfo(word=word.word, start_time=word.start_ms, end_time=word.end_ms) for word in timed
|
|
],
|
|
)
|
|
|
|
def Recognize(
|
|
self, request: riva_asr_pb2.RecognizeRequest, context: grpc.ServicerContext
|
|
) -> riva_asr_pb2.RecognizeResponse:
|
|
if self._stall:
|
|
released: Final = threading.Event()
|
|
context.add_callback(released.set)
|
|
released.wait(timeout=_STALL_LIMIT_SECONDS)
|
|
self._calls.put(RivaCall(Rpc.RECOGNIZE, request.audio, cancelled=not context.is_active()))
|
|
return riva_asr_pb2.RecognizeResponse()
|
|
self._calls.put(RivaCall(Rpc.RECOGNIZE, request.audio, cancelled=False))
|
|
return riva_asr_pb2.RecognizeResponse(
|
|
results=[riva_asr_pb2.SpeechRecognitionResult(alternatives=[self._alternative(request.config)])]
|
|
)
|
|
|
|
def StreamingRecognize(
|
|
self, request_iterator: Iterable[riva_asr_pb2.StreamingRecognizeRequest], context: grpc.ServicerContext
|
|
) -> Iterator[riva_asr_pb2.StreamingRecognizeResponse]:
|
|
if self._offline_only:
|
|
self._calls.put(RivaCall(Rpc.STREAMING, b"", cancelled=False))
|
|
context.abort(grpc.StatusCode.INVALID_ARGUMENT, OFFLINE_ONLY_REFUSAL)
|
|
requests: Final = tuple(request_iterator)
|
|
config: Final = requests[0].streaming_config.config if requests else riva_asr_pb2.RecognitionConfig()
|
|
self._calls.put(
|
|
RivaCall(Rpc.STREAMING, b"".join(request.audio_content for request in requests), cancelled=False)
|
|
)
|
|
yield riva_asr_pb2.StreamingRecognizeResponse(
|
|
results=[riva_asr_pb2.StreamingRecognitionResult(alternatives=[self._alternative(config)], is_final=True)]
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def riva_server(
|
|
words: tuple[Word, ...],
|
|
*,
|
|
offline_only: bool = False,
|
|
stall: bool = False,
|
|
max_receive_bytes: int | None = None,
|
|
) -> Iterator[RivaServer]:
|
|
calls: Final[SimpleQueue[RivaCall]] = SimpleQueue()
|
|
options: Final = () if max_receive_bytes is None else (("grpc.max_receive_message_length", max_receive_bytes),)
|
|
server: Final = grpc.server(futures.ThreadPoolExecutor(max_workers=8), options=options)
|
|
riva_asr_pb2_grpc.add_RivaSpeechRecognitionServicer_to_server(_Asr(words, offline_only, stall, calls), server)
|
|
port: Final = server.add_insecure_port("127.0.0.1:0")
|
|
server.start()
|
|
try:
|
|
yield RivaServer(f"127.0.0.1:{port}", calls)
|
|
finally:
|
|
server.stop(grace=None).wait()
|