fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
235 lines
6.7 KiB
Python
235 lines
6.7 KiB
Python
"""Tests for application/stt/faster_whisper_stt.py"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from application.stt.faster_whisper_stt import FasterWhisperSTT
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestFasterWhisperSTTInit:
|
|
|
|
def test_init_defaults(self):
|
|
stt = FasterWhisperSTT()
|
|
assert stt.model_size == "base"
|
|
assert stt.device == "auto"
|
|
assert stt.compute_type == "int8"
|
|
assert stt._model is None
|
|
|
|
def test_init_custom_params(self):
|
|
stt = FasterWhisperSTT(
|
|
model_size="large-v2",
|
|
device="cuda",
|
|
compute_type="float16",
|
|
)
|
|
assert stt.model_size == "large-v2"
|
|
assert stt.device == "cuda"
|
|
assert stt.compute_type == "float16"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestFasterWhisperSTTGetModel:
|
|
|
|
def test_get_model_lazy_init(self):
|
|
stt = FasterWhisperSTT()
|
|
|
|
mock_whisper_model = MagicMock()
|
|
mock_module = MagicMock()
|
|
mock_module.WhisperModel.return_value = mock_whisper_model
|
|
|
|
with patch.dict("sys.modules", {"faster_whisper": mock_module}):
|
|
model = stt._get_model()
|
|
|
|
assert model is mock_whisper_model
|
|
mock_module.WhisperModel.assert_called_once_with(
|
|
"base",
|
|
device="auto",
|
|
compute_type="int8",
|
|
)
|
|
|
|
def test_get_model_caches(self):
|
|
stt = FasterWhisperSTT()
|
|
|
|
mock_whisper_model = MagicMock()
|
|
mock_module = MagicMock()
|
|
mock_module.WhisperModel.return_value = mock_whisper_model
|
|
|
|
with patch.dict("sys.modules", {"faster_whisper": mock_module}):
|
|
model1 = stt._get_model()
|
|
model2 = stt._get_model()
|
|
|
|
assert model1 is model2
|
|
assert mock_module.WhisperModel.call_count == 1
|
|
|
|
def test_get_model_raises_import_error(self):
|
|
stt = FasterWhisperSTT()
|
|
|
|
with patch.dict("sys.modules", {"faster_whisper": None}):
|
|
with pytest.raises(ImportError, match="faster-whisper is required"):
|
|
stt._get_model()
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestFasterWhisperSTTTranscribe:
|
|
|
|
def _make_stt_with_mock_model(self):
|
|
stt = FasterWhisperSTT()
|
|
mock_model = MagicMock()
|
|
stt._model = mock_model
|
|
return stt, mock_model
|
|
|
|
def test_transcribe_basic(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
seg1 = MagicMock()
|
|
seg1.text = " Hello world "
|
|
seg1.start = 0.0
|
|
seg1.end = 1.5
|
|
|
|
seg2 = MagicMock()
|
|
seg2.text = " How are you "
|
|
seg2.start = 1.5
|
|
seg2.end = 3.0
|
|
|
|
info = MagicMock()
|
|
info.language = "en"
|
|
info.duration = 3.0
|
|
|
|
mock_model.transcribe.return_value = (iter([seg1, seg2]), info)
|
|
|
|
result = stt.transcribe(Path("/tmp/audio.wav"))
|
|
|
|
assert result["text"] == "Hello world How are you"
|
|
assert result["language"] == "en"
|
|
assert result["duration_s"] == 3.0
|
|
assert result["segments"] == [] # timestamps=False by default
|
|
assert result["provider"] == "faster_whisper"
|
|
|
|
mock_model.transcribe.assert_called_once_with(
|
|
"/tmp/audio.wav",
|
|
language=None,
|
|
word_timestamps=False,
|
|
)
|
|
|
|
def test_transcribe_with_language(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
info = MagicMock()
|
|
info.language = "fr"
|
|
info.duration = 1.0
|
|
|
|
mock_model.transcribe.return_value = (iter([]), info)
|
|
|
|
result = stt.transcribe(Path("/tmp/audio.wav"), language="fr")
|
|
|
|
mock_model.transcribe.assert_called_once_with(
|
|
"/tmp/audio.wav",
|
|
language="fr",
|
|
word_timestamps=False,
|
|
)
|
|
assert result["language"] == "fr"
|
|
|
|
def test_transcribe_with_timestamps(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
seg = MagicMock()
|
|
seg.text = " Hello "
|
|
seg.start = 0.0
|
|
seg.end = 1.0
|
|
|
|
info = MagicMock()
|
|
info.language = "en"
|
|
info.duration = 1.0
|
|
|
|
mock_model.transcribe.return_value = (iter([seg]), info)
|
|
|
|
result = stt.transcribe(Path("/tmp/audio.wav"), timestamps=True)
|
|
|
|
assert len(result["segments"]) == 1
|
|
assert result["segments"][0]["start"] == 0.0
|
|
assert result["segments"][0]["end"] == 1.0
|
|
assert result["segments"][0]["text"] == "Hello"
|
|
|
|
mock_model.transcribe.assert_called_once_with(
|
|
"/tmp/audio.wav",
|
|
language=None,
|
|
word_timestamps=True,
|
|
)
|
|
|
|
def test_transcribe_empty_segments(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
info = MagicMock()
|
|
info.language = "en"
|
|
info.duration = 0.0
|
|
|
|
mock_model.transcribe.return_value = (iter([]), info)
|
|
|
|
result = stt.transcribe(Path("/tmp/audio.wav"))
|
|
|
|
assert result["text"] == ""
|
|
assert result["segments"] == []
|
|
|
|
def test_transcribe_segment_with_empty_text(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
seg = MagicMock()
|
|
seg.text = " "
|
|
seg.start = 0.0
|
|
seg.end = 0.5
|
|
|
|
info = MagicMock()
|
|
info.language = "en"
|
|
info.duration = 0.5
|
|
|
|
mock_model.transcribe.return_value = (iter([seg]), info)
|
|
|
|
result = stt.transcribe(Path("/tmp/audio.wav"))
|
|
|
|
# Empty text stripped should not be included in text_parts
|
|
assert result["text"] == ""
|
|
|
|
def test_transcribe_diarize_is_ignored(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
info = MagicMock()
|
|
info.language = "en"
|
|
info.duration = 1.0
|
|
|
|
mock_model.transcribe.return_value = (iter([]), info)
|
|
|
|
# diarize param should be accepted but ignored
|
|
result = stt.transcribe(
|
|
Path("/tmp/audio.wav"),
|
|
diarize=True,
|
|
)
|
|
|
|
assert result["provider"] == "faster_whisper"
|
|
|
|
def test_transcribe_missing_attrs_use_none(self):
|
|
stt, mock_model = self._make_stt_with_mock_model()
|
|
|
|
seg = MagicMock(spec=[]) # No attributes
|
|
seg.text = "" # Override to avoid AttributeError on text
|
|
|
|
# Create a segment that uses getattr fallbacks
|
|
class MinimalSegment:
|
|
pass
|
|
|
|
minimal = MinimalSegment()
|
|
|
|
info_cls = type("Info", (), {})()
|
|
|
|
mock_model.transcribe.return_value = (iter([minimal]), info_cls)
|
|
|
|
result = stt.transcribe(Path("/tmp/audio.wav"), timestamps=True)
|
|
|
|
assert result["language"] is None
|
|
assert result["duration_s"] is None
|
|
# Segment should have None for start/end
|
|
assert len(result["segments"]) == 1
|
|
assert result["segments"][0]["start"] is None
|
|
assert result["segments"][0]["end"] is None
|