test_daemon.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """Tests for the daemon's worker processing and transcription backlog warning."""
  2. import logging
  3. import numpy as np
  4. import pytest
  5. from stt_daemon import events
  6. from stt_daemon import transcriber as transcriber_mod
  7. from stt_daemon.config import Config
  8. from stt_daemon.daemon import Daemon
  9. from stt_daemon.segmenter import Utterance
  10. from stt_daemon.transcriber import TranscriptResult
  11. def _daemon(threshold=2, queue_size=10):
  12. cfg = Config()
  13. cfg.daemon.backlog_warn_at = threshold
  14. cfg.daemon.queue_size = queue_size
  15. return Daemon(cfg)
  16. def test_queue_size_comes_from_config():
  17. daemon = _daemon(queue_size=7)
  18. assert daemon.segment_queue.maxsize == 7
  19. def test_backlog_warning_is_edge_triggered(caplog):
  20. daemon = _daemon(threshold=2)
  21. with caplog.at_level(logging.WARNING, logger="stt_daemon"):
  22. daemon.segment_queue.put("a")
  23. daemon._check_backlog()
  24. assert "backlog" not in caplog.text.lower()
  25. daemon.segment_queue.put("b")
  26. daemon._check_backlog()
  27. assert caplog.text.lower().count("backlog") == 1
  28. daemon._check_backlog()
  29. assert caplog.text.lower().count("backlog") == 1
  30. daemon.segment_queue.get()
  31. daemon.segment_queue.get()
  32. daemon._check_backlog()
  33. assert daemon._backlog_warned is False
  34. daemon.segment_queue.put("c")
  35. daemon.segment_queue.put("d")
  36. daemon._check_backlog()
  37. assert caplog.text.lower().count("backlog") == 2
  38. class _FakeWhisperModel:
  39. """Constructor-only fake; keeps faster-whisper from loading a real model."""
  40. def __init__(self, *args, **kwargs):
  41. self.init_kwargs = kwargs
  42. class _FakeCppModel:
  43. """Constructor-only fake; keeps pywhispercpp from loading a real model."""
  44. def __init__(self, *args, **kwargs):
  45. self.init_kwargs = kwargs
  46. def test_build_transcriber_defaults_to_faster_whisper(monkeypatch):
  47. monkeypatch.setattr(transcriber_mod, "WhisperModel", _FakeWhisperModel)
  48. assert isinstance(_daemon()._build_transcriber(), transcriber_mod.Transcriber)
  49. def test_build_transcriber_honors_pywhispercpp_backend(monkeypatch):
  50. cpp_model = pytest.importorskip("pywhispercpp.model")
  51. monkeypatch.setattr(cpp_model, "Model", _FakeCppModel)
  52. from stt_daemon.pywhispercpp_transcriber import PyWhisperCppTranscriber
  53. cfg = Config()
  54. cfg.whisper.backend = "pywhispercpp"
  55. assert isinstance(Daemon(cfg)._build_transcriber(), PyWhisperCppTranscriber)
  56. # --- worker processing ------------------------------------------------------
  57. class _FakeTranscriber:
  58. """Returns a canned result without loading a real model."""
  59. def __init__(self, result):
  60. self.result = result
  61. def transcribe(self, audio):
  62. return self.result
  63. def close(self):
  64. pass
  65. class _RaisingSink:
  66. """Sink that fails on emit, simulating a non-JSON-serializable record."""
  67. def emit(self, *args, **kwargs):
  68. raise TypeError("Object of type float32 is not JSON serializable")
  69. def close(self):
  70. pass
  71. def _utterance(seg_id=1):
  72. return Utterance(
  73. audio=np.zeros(1600, dtype=np.float32),
  74. duration_s=1.0,
  75. started_at=0.0,
  76. rms=0.05,
  77. reason="pause",
  78. seg_id=seg_id,
  79. )
  80. def _transcript_result():
  81. return TranscriptResult(language="en", language_probability=0.9, text="hello world")
  82. def _daemon_with_raising_sink():
  83. daemon = _daemon()
  84. daemon.transcriber = _FakeTranscriber(_transcript_result())
  85. daemon.sink = _RaisingSink()
  86. published = []
  87. daemon._publish = lambda type_, **fields: published.append((type_, fields))
  88. return daemon, published
  89. def test_sink_failure_still_publishes_transcript(caplog):
  90. daemon, published = _daemon_with_raising_sink()
  91. with caplog.at_level(logging.ERROR, logger="stt_daemon"):
  92. daemon._process(_utterance())
  93. transcripts = [fields for type_, fields in published if type_ == events.TRANSCRIPT]
  94. assert len(transcripts) == 1
  95. assert transcripts[0]["text"] == "hello world"
  96. assert transcripts[0]["lang_prob"] == 0.9
  97. assert transcripts[0]["audio"] is None
  98. assert "Output error" in caplog.text
  99. def test_sink_failure_reports_segment_done_as_emitted():
  100. daemon, published = _daemon_with_raising_sink()
  101. daemon._process(_utterance())
  102. done = [fields for type_, fields in published if type_ == events.SEGMENT_DONE]
  103. assert len(done) == 1
  104. assert done[0]["emitted"] is True