test_evaluate.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """Hardware/network-free tests for stt_daemon.evaluate."""
  2. import wave
  3. import numpy as np
  4. from stt_daemon.evaluate import EvalResult, accuracy, evaluate, format_report
  5. from stt_daemon.speakers import SpeakerStore
  6. _SAMPLE_RATE = 16000
  7. ALICE = np.array([1.0, 0.0, 0.0], dtype=np.float32)
  8. BOB = np.array([0.0, 1.0, 0.0], dtype=np.float32)
  9. # Cosine 0.5 to ALICE, 0.0 to BOB -> uncertain with top-1 alice (default thresholds).
  10. ALICE_UNCERTAIN = np.array([0.5, 0.0, 0.8660254], dtype=np.float32)
  11. UNKNOWN_VEC = np.array([0.0, 0.0, 1.0], dtype=np.float32)
  12. MARKERS = {
  13. 1001: ALICE,
  14. 1002: BOB,
  15. 1003: ALICE_UNCERTAIN,
  16. 1004: UNKNOWN_VEC,
  17. }
  18. class FakeEmbedder:
  19. """Return a controlled unit vector keyed by the first int16 sample."""
  20. def __init__(self, mapping=None):
  21. self.mapping = dict(MARKERS if mapping is None else mapping)
  22. def embed(self, audio):
  23. marker = int(round(float(np.asarray(audio).reshape(-1)[0]) * 32768.0))
  24. return np.array(self.mapping[marker], dtype=np.float32)
  25. def _write_wav(path, marker, nframes=1600):
  26. path.parent.mkdir(parents=True, exist_ok=True)
  27. frames = np.zeros(nframes, dtype=np.int16)
  28. frames[0] = np.int16(marker)
  29. with wave.open(str(path), "wb") as wav:
  30. wav.setnchannels(1)
  31. wav.setsampwidth(2)
  32. wav.setframerate(_SAMPLE_RATE)
  33. wav.writeframes(frames.tobytes())
  34. def _write_wav_rate(path, marker, framerate):
  35. path.parent.mkdir(parents=True, exist_ok=True)
  36. frames = np.full(1600, marker, dtype=np.int16)
  37. with wave.open(str(path), "wb") as wav:
  38. wav.setnchannels(1)
  39. wav.setsampwidth(2)
  40. wav.setframerate(framerate)
  41. wav.writeframes(frames.tobytes())
  42. def _write_stereo_wav(path):
  43. path.parent.mkdir(parents=True, exist_ok=True)
  44. frames = np.zeros((1600, 2), dtype=np.int16)
  45. with wave.open(str(path), "wb") as wav:
  46. wav.setnchannels(2)
  47. wav.setsampwidth(2)
  48. wav.setframerate(_SAMPLE_RATE)
  49. wav.writeframes(frames.tobytes())
  50. def _make_store(root):
  51. store = SpeakerStore(root)
  52. store.enroll("alice", [ALICE])
  53. store.enroll("bob", [BOB])
  54. return store
  55. def test_all_correct_accuracy_one(tmp_path):
  56. store = _make_store(tmp_path / "store")
  57. labeled = tmp_path / "labeled"
  58. _write_wav(labeled / "alice" / "a.wav", 1001)
  59. _write_wav(labeled / "bob" / "b.wav", 1002)
  60. result = evaluate(store, FakeEmbedder(), labeled)
  61. assert result.total == 2
  62. assert result.correct == 2
  63. assert result.wrong == 0
  64. assert result.unknown == 0
  65. assert accuracy(result) == 1.0
  66. assert result.per_speaker == {"alice": (1, 1), "bob": (1, 1)}
  67. assert result.confusions == {}
  68. assert len(result.margins) == 2
  69. assert result.labeled == (("matched", "alice/a.wav"), ("matched", "bob/b.wav"))
  70. def test_wrong_match_populates_confusions(tmp_path):
  71. store = _make_store(tmp_path / "store")
  72. labeled = tmp_path / "labeled"
  73. _write_wav(labeled / "alice" / "a.wav", 1002) # bob's voice under alice/
  74. result = evaluate(store, FakeEmbedder(), labeled)
  75. assert result.total == 1
  76. assert result.correct == 0
  77. assert result.wrong == 1
  78. assert result.confusions == {("alice", "bob"): 1}
  79. assert result.per_speaker == {"alice": (1, 0)}
  80. assert accuracy(result) == 0.0
  81. def test_uncertain_top1_correct(tmp_path):
  82. store = _make_store(tmp_path / "store")
  83. labeled = tmp_path / "labeled"
  84. _write_wav(labeled / "alice" / "u.wav", 1003)
  85. result = evaluate(store, FakeEmbedder(), labeled)
  86. assert result.total == 1
  87. assert result.uncertain_correct == 1
  88. assert result.uncertain_wrong == 0
  89. assert result.correct == 0 and result.wrong == 0 and result.unknown == 0
  90. assert result.labeled[0][0] == "uncertain"
  91. assert len(result.margins) == 1
  92. def test_uncertain_top1_wrong_and_unknown(tmp_path):
  93. store = _make_store(tmp_path / "store")
  94. labeled = tmp_path / "labeled"
  95. # Uncertain vector closest to alice but filed under bob -> uncertain_wrong.
  96. _write_wav(labeled / "bob" / "u.wav", 1003)
  97. _write_wav(labeled / "alice" / "x.wav", 1004)
  98. result = evaluate(store, FakeEmbedder(), labeled)
  99. assert result.uncertain_wrong == 1
  100. assert result.unknown == 1
  101. assert result.total == 2
  102. def test_unreadable_and_bad_format_skipped(tmp_path):
  103. store = _make_store(tmp_path / "store")
  104. labeled = tmp_path / "labeled"
  105. _write_wav(labeled / "alice" / "good.wav", 1001)
  106. (labeled / "alice" / "bad.wav").parent.mkdir(parents=True, exist_ok=True)
  107. (labeled / "alice" / "bad.wav").write_bytes(b"not a wav at all")
  108. _write_wav_rate(labeled / "alice" / "rate8k.wav", 1001, 8000)
  109. _write_stereo_wav(labeled / "alice" / "stereo.wav")
  110. result = evaluate(store, FakeEmbedder(), labeled)
  111. assert result.total == 1
  112. assert result.correct == 1
  113. assert result.labeled == (("matched", "alice/good.wav"),)
  114. def test_unknown_speaker_directory_still_evaluated(tmp_path):
  115. store = _make_store(tmp_path / "store")
  116. labeled = tmp_path / "labeled"
  117. _write_wav(labeled / "carol" / "c.wav", 1001) # matches alice, true name carol
  118. result = evaluate(store, FakeEmbedder(), labeled)
  119. assert result.total == 1
  120. assert result.wrong == 1
  121. assert result.confusions == {("carol", "alice"): 1}
  122. assert result.per_speaker == {"carol": (1, 0)}
  123. def test_empty_and_missing_directory(tmp_path):
  124. store = _make_store(tmp_path / "store")
  125. empty = tmp_path / "empty"
  126. empty.mkdir()
  127. result = evaluate(store, FakeEmbedder(), empty)
  128. assert result.total == 0
  129. assert accuracy(result) == 0.0
  130. assert result.margins == ()
  131. assert result.labeled == ()
  132. missing = tmp_path / "does-not-exist"
  133. result2 = evaluate(store, FakeEmbedder(), missing)
  134. assert result2.total == 0
  135. assert accuracy(result2) == 0.0
  136. def test_format_report_contents_and_order(tmp_path):
  137. result = EvalResult(
  138. total=3,
  139. correct=1,
  140. wrong=1,
  141. uncertain_correct=1,
  142. uncertain_wrong=0,
  143. unknown=0,
  144. per_speaker={"bob": (2, 1), "alice": (1, 0)},
  145. confusions={("alice", "bob"): 1},
  146. margins=(0.5, 1.0),
  147. labeled=(("matched", "alice/a.wav"),),
  148. )
  149. report = format_report(result)
  150. assert "total: 3" in report
  151. assert "accuracy: 0.333" in report
  152. assert "correct: 1" in report
  153. assert "wrong: 1" in report
  154. assert "uncertain (top-1 correct/total): 1/1" in report
  155. assert "unknown: 0" in report
  156. assert "alice | 1 | 0 | 0.000" in report
  157. assert "bob | 2 | 1 | 0.500" in report
  158. assert report.index("alice |") < report.index("bob |")
  159. assert "alice -> bob (1)" in report
  160. assert "count: 2" in report
  161. assert "mean: 0.7500" in report
  162. assert "p10:" in report
  163. # Empty margins guard.
  164. empty_report = format_report(
  165. EvalResult(0, 0, 0, 0, 0, 0, {}, {}, (), ()),
  166. )
  167. assert "count: 0" in empty_report
  168. def test_margins_nonempty_and_store_untouched(tmp_path):
  169. store = _make_store(tmp_path / "store")
  170. before_speakers = store.speakers
  171. before_vectors = np.array(store._vectors, copy=True)
  172. labeled = tmp_path / "labeled"
  173. _write_wav(labeled / "alice" / "a.wav", 1001)
  174. result = evaluate(store, FakeEmbedder(), labeled)
  175. assert len(result.margins) == 1
  176. assert result.margins[0] > 0.0
  177. assert store.speakers == before_speakers
  178. assert np.array_equal(np.asarray(store._vectors), before_vectors)