Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ docker-compose.override.yaml
.mypy_cache/
.venv/
.vscode/
.idea/
.builds/
.cursor/
openarc_bench.db
Expand Down
36 changes: 34 additions & 2 deletions src/engine/ov_genai/whisper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
import gc
import io
import logging
import os
import subprocess
import tempfile
from typing import Any, AsyncIterator, Dict, Union

import librosa
import numpy as np
from openvino_genai import WhisperPipeline
from soundfile import LibsndfileError

from src.server.model_registry import ModelRegistry
from src.server.schemas.registration import ModelLoadConfig
Expand All @@ -22,6 +26,30 @@ def __init__(self, load_config: ModelLoadConfig):
self.load_config = load_config
pass

def decode_with_ffmpeg(self, audio_bytes: bytes) -> np.ndarray:
src = tempfile.NamedTemporaryFile(delete=False)
try:
src.write(audio_bytes)
src.flush()
src.close()
proc = subprocess.run(
[
"ffmpeg",
"-loglevel", "error",
"-i", src.name, # Read the uploaded audio from the temporary file.
"-ac", "1", # Convert the audio to mono.
"-ar", "16000", # Resample to 16 kHz.
"-f", "f32le", # Emit raw 32-bit little-endian float PCM.
"pipe:1", # Write the decoded audio to std out.
],
capture_output=True,
)
if proc.returncode != 0:
raise ValueError(f"ffmpeg could not decode audio: {proc.stderr.decode(errors='replace')[:500]}")
return np.frombuffer(proc.stdout, dtype=np.float32)
finally:
os.unlink(src.name) # always delete the temp file

def prepare_audio(self, gen_config: OVGenAI_WhisperGenConfig) -> list[float]:
"""
Prepare audio inputs from base64 string for the Whisper pipeline.
Expand All @@ -30,8 +58,12 @@ def prepare_audio(self, gen_config: OVGenAI_WhisperGenConfig) -> list[float]:
audio_bytes = base64.b64decode(gen_config.audio_base64)

audio_buffer = io.BytesIO(audio_bytes)

audio, sr = librosa.load(audio_buffer, sr=16000, mono=True)

try:
audio, sr = librosa.load(audio_buffer, sr=16000, mono=True)
except LibsndfileError as exc:
logger.info(f"librosa decode failed ({exc}), retrying with ffmpeg")
audio = self.decode_with_ffmpeg(audio_bytes)

return audio.astype(np.float32).tolist()

Expand Down
22 changes: 21 additions & 1 deletion tests/unit/test_ov_genai_whisper_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ def test_prepare_audio_calls_librosa(monkeypatch: pytest.MonkeyPatch, load_confi
assert audio_list == audio_array.tolist()


def test_prepare_audio_falls_back_to_ffmpeg(monkeypatch: pytest.MonkeyPatch, load_config: ModelLoadConfig) -> None:
whisper = OVGenAI_Whisper(load_config)
audio_array = np.array([0.1, -0.2], dtype=np.float32)

load_mock = MagicMock(side_effect=whisper_module.LibsndfileError(1))
monkeypatch.setattr(whisper_module.librosa, "load", load_mock)
proc_mock = MagicMock(returncode=0, stdout=audio_array.tobytes())
run_mock = MagicMock(return_value=proc_mock)
monkeypatch.setattr(whisper_module.subprocess, "run", run_mock)

audio_list = whisper.prepare_audio(OVGenAI_WhisperGenConfig(audio_base64=_sample_audio_base64()))

load_mock.assert_called_once()
run_mock.assert_called_once()
command = run_mock.call_args.args[0]
assert command[:4] == ["ffmpeg", "-loglevel", "error", "-i"]
assert command[5:] == ["-ac", "1", "-ar", "16000", "-f", "f32le", "pipe:1"]
assert run_mock.call_args.kwargs["capture_output"] is True
assert audio_list == audio_array.tolist()


def test_collect_metrics_formats_values(load_config: ModelLoadConfig) -> None:
whisper = OVGenAI_Whisper(load_config)
metrics = whisper.collect_metrics(DummyPerfMetrics())
Expand Down Expand Up @@ -151,4 +172,3 @@ def test_unload_model_resets_state(monkeypatch: pytest.MonkeyPatch, load_config:
assert whisper.whisper_model is None
registry.register_unload.assert_called_once_with("model-name")
gc_mock.assert_called_once()