Files
arc53--docsgpt/tests/stt/test_faster_whisper.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

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