test_config.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. """Tests for configuration loading."""
  2. import tempfile
  3. from pathlib import Path
  4. from stt_daemon.config import load_config
  5. def test_load_config_defaults():
  6. """Test that load_config returns defaults when no file exists."""
  7. with tempfile.TemporaryDirectory() as tmpdir:
  8. # Change to temp dir and load (no config.toml exists)
  9. import os
  10. old_cwd = os.getcwd()
  11. try:
  12. os.chdir(tmpdir)
  13. cfg = load_config()
  14. assert cfg.audio.backend == "parec"
  15. assert cfg.audio.device == "VirtualMicSink.monitor"
  16. assert cfg.audio.sample_rate == 16000
  17. assert cfg.vad.aggressiveness == 2
  18. assert cfg.whisper.model == "small"
  19. assert cfg.whisper.language is None
  20. assert cfg.output.stdout is True
  21. assert cfg.output.jsonl is False
  22. finally:
  23. os.chdir(old_cwd)
  24. def test_load_config_from_file():
  25. """Test loading config from a TOML file."""
  26. config_toml = """
  27. [audio]
  28. backend = "sounddevice"
  29. device = "hw:0"
  30. sample_rate = 44100
  31. [vad]
  32. aggressiveness = 3
  33. pre_roll_ms = 500
  34. [whisper]
  35. model = "base"
  36. device = "cuda"
  37. language = "es"
  38. [output]
  39. stdout = false
  40. jsonl = true
  41. jsonl_dir = "logs"
  42. """
  43. with tempfile.TemporaryDirectory() as tmpdir:
  44. config_path = Path(tmpdir) / "config.toml"
  45. config_path.write_text(config_toml)
  46. cfg = load_config(str(config_path))
  47. assert cfg.audio.backend == "sounddevice"
  48. assert cfg.audio.device == "hw:0"
  49. assert cfg.audio.sample_rate == 44100
  50. assert cfg.vad.aggressiveness == 3
  51. assert cfg.vad.pre_roll_ms == 500
  52. assert cfg.whisper.model == "base"
  53. assert cfg.whisper.device == "cuda"
  54. assert cfg.whisper.language == "es"
  55. assert cfg.output.stdout is False
  56. assert cfg.output.jsonl is True
  57. assert cfg.output.jsonl_dir == "logs"
  58. def test_language_normalization_auto():
  59. """Test that language 'auto' is normalized to None."""
  60. config_toml = """
  61. [whisper]
  62. language = "auto"
  63. """
  64. with tempfile.TemporaryDirectory() as tmpdir:
  65. config_path = Path(tmpdir) / "config.toml"
  66. config_path.write_text(config_toml)
  67. cfg = load_config(str(config_path))
  68. assert cfg.whisper.language is None
  69. def test_language_normalization_empty_string():
  70. """Test that language '' is normalized to None."""
  71. config_toml = """
  72. [whisper]
  73. language = ""
  74. """
  75. with tempfile.TemporaryDirectory() as tmpdir:
  76. config_path = Path(tmpdir) / "config.toml"
  77. config_path.write_text(config_toml)
  78. cfg = load_config(str(config_path))
  79. assert cfg.whisper.language is None
  80. def test_language_normalization_none():
  81. """Test that language 'none' is normalized to None."""
  82. config_toml = """
  83. [whisper]
  84. language = "none"
  85. """
  86. with tempfile.TemporaryDirectory() as tmpdir:
  87. config_path = Path(tmpdir) / "config.toml"
  88. config_path.write_text(config_toml)
  89. cfg = load_config(str(config_path))
  90. assert cfg.whisper.language is None
  91. def test_language_explicit_en():
  92. """Test that explicit language codes are preserved."""
  93. config_toml = """
  94. [whisper]
  95. language = "en"
  96. """
  97. with tempfile.TemporaryDirectory() as tmpdir:
  98. config_path = Path(tmpdir) / "config.toml"
  99. config_path.write_text(config_toml)
  100. cfg = load_config(str(config_path))
  101. assert cfg.whisper.language == "en"
  102. def test_partial_config():
  103. """Test that partial configs merge with defaults."""
  104. config_toml = """
  105. [audio]
  106. device = "custom_device"
  107. """
  108. with tempfile.TemporaryDirectory() as tmpdir:
  109. config_path = Path(tmpdir) / "config.toml"
  110. config_path.write_text(config_toml)
  111. cfg = load_config(str(config_path))
  112. # Overridden
  113. assert cfg.audio.device == "custom_device"
  114. # Defaults
  115. assert cfg.audio.backend == "parec"
  116. assert cfg.audio.sample_rate == 16000
  117. assert cfg.vad.aggressiveness == 2
  118. def test_chunking_and_confidence_fields():
  119. """New chunking/gate fields load from TOML."""
  120. config_toml = """
  121. [vad]
  122. chunk_pause_ms = 500
  123. soft_max_s = 8.0
  124. rms_floor = 0.01
  125. trailing_trim_ms = 150
  126. [whisper]
  127. no_speech_prob_max = 0.7
  128. avg_logprob_min = -0.8
  129. """
  130. with tempfile.TemporaryDirectory() as tmpdir:
  131. config_path = Path(tmpdir) / "config.toml"
  132. config_path.write_text(config_toml)
  133. cfg = load_config(str(config_path))
  134. assert cfg.vad.chunk_pause_ms == 500
  135. assert cfg.vad.soft_max_s == 8.0
  136. assert cfg.vad.rms_floor == 0.01
  137. assert cfg.vad.trailing_trim_ms == 150
  138. assert cfg.whisper.no_speech_prob_max == 0.7
  139. assert cfg.whisper.avg_logprob_min == -0.8
  140. def test_chunking_defaults():
  141. """New fields have sane defaults."""
  142. with tempfile.TemporaryDirectory() as tmpdir:
  143. import os
  144. old_cwd = os.getcwd()
  145. try:
  146. os.chdir(tmpdir)
  147. cfg = load_config()
  148. finally:
  149. os.chdir(old_cwd)
  150. assert cfg.vad.chunk_pause_ms == 350
  151. assert cfg.vad.soft_max_s == 10.0
  152. assert cfg.vad.rms_floor == 0.008
  153. assert cfg.vad.trailing_trim_ms == 200
  154. assert cfg.whisper.no_speech_prob_max == 0.6
  155. assert cfg.whisper.avg_logprob_min == -1.0
  156. def test_output_defaults_disable_audio_saving():
  157. with tempfile.TemporaryDirectory() as tmpdir:
  158. import os
  159. old_cwd = os.getcwd()
  160. try:
  161. os.chdir(tmpdir)
  162. cfg = load_config()
  163. finally:
  164. os.chdir(old_cwd)
  165. assert cfg.output.save_audio is False
  166. assert cfg.output.audio_dir == "audio"
  167. def test_output_save_audio_fields():
  168. config_toml = """
  169. [output]
  170. save_audio = true
  171. audio_dir = "clips"
  172. """
  173. with tempfile.TemporaryDirectory() as tmpdir:
  174. config_path = Path(tmpdir) / "config.toml"
  175. config_path.write_text(config_toml)
  176. cfg = load_config(str(config_path))
  177. assert cfg.output.save_audio is True
  178. assert cfg.output.audio_dir == "clips"
  179. def test_daemon_defaults():
  180. with tempfile.TemporaryDirectory() as tmpdir:
  181. import os
  182. old_cwd = os.getcwd()
  183. try:
  184. os.chdir(tmpdir)
  185. cfg = load_config()
  186. finally:
  187. os.chdir(old_cwd)
  188. assert cfg.daemon.queue_size == 50
  189. assert cfg.daemon.backlog_warn_at == 5
  190. def test_daemon_config_fields():
  191. config_toml = """
  192. [daemon]
  193. queue_size = 20
  194. backlog_warn_at = 3
  195. """
  196. with tempfile.TemporaryDirectory() as tmpdir:
  197. config_path = Path(tmpdir) / "config.toml"
  198. config_path.write_text(config_toml)
  199. cfg = load_config(str(config_path))
  200. assert cfg.daemon.queue_size == 20
  201. assert cfg.daemon.backlog_warn_at == 3
  202. def test_unknown_sections_warn_and_are_ignored(caplog):
  203. """A typo'd section like [assitant] must warn, not silently use defaults."""
  204. config_toml = """
  205. [assitant]
  206. require_viewer = false
  207. """
  208. with tempfile.TemporaryDirectory() as tmpdir:
  209. config_path = Path(tmpdir) / "config.toml"
  210. config_path.write_text(config_toml)
  211. with caplog.at_level("WARNING", logger="stt_daemon.config"):
  212. cfg = load_config(str(config_path))
  213. assert "assitant" in caplog.text
  214. assert cfg.assistant.require_viewer is True
  215. def test_cpu_threads_default_is_auto():
  216. """cpu_threads defaults to 0 (CTranslate2's automatic choice)."""
  217. with tempfile.TemporaryDirectory() as tmpdir:
  218. import os
  219. old_cwd = os.getcwd()
  220. try:
  221. os.chdir(tmpdir)
  222. cfg = load_config()
  223. finally:
  224. os.chdir(old_cwd)
  225. assert cfg.whisper.cpu_threads == 0
  226. def test_cpu_threads_config_field():
  227. config_toml = """
  228. [whisper]
  229. cpu_threads = 16
  230. """
  231. with tempfile.TemporaryDirectory() as tmpdir:
  232. config_path = Path(tmpdir) / "config.toml"
  233. config_path.write_text(config_toml)
  234. cfg = load_config(str(config_path))
  235. assert cfg.whisper.cpu_threads == 16
  236. def test_language_strategy_defaults_to_static():
  237. """language_strategy defaults to 'static'."""
  238. with tempfile.TemporaryDirectory() as tmpdir:
  239. import os
  240. old_cwd = os.getcwd()
  241. try:
  242. os.chdir(tmpdir)
  243. cfg = load_config()
  244. finally:
  245. os.chdir(old_cwd)
  246. assert cfg.whisper.language_strategy == "static"
  247. def test_language_strategy_sticky_is_parsed():
  248. """language_strategy 'sticky' is parsed correctly."""
  249. config_toml = """
  250. [whisper]
  251. language_strategy = "sticky"
  252. """
  253. with tempfile.TemporaryDirectory() as tmpdir:
  254. config_path = Path(tmpdir) / "config.toml"
  255. config_path.write_text(config_toml)
  256. cfg = load_config(str(config_path))
  257. assert cfg.whisper.language_strategy == "sticky"
  258. def test_unknown_language_strategy_falls_back_to_static(caplog):
  259. """An unknown language_strategy value falls back to 'static' with a warning."""
  260. config_toml = """
  261. [whisper]
  262. language_strategy = "bogus"
  263. """
  264. with tempfile.TemporaryDirectory() as tmpdir:
  265. config_path = Path(tmpdir) / "config.toml"
  266. config_path.write_text(config_toml)
  267. with caplog.at_level("WARNING", logger="stt_daemon.config"):
  268. cfg = load_config(str(config_path))
  269. assert cfg.whisper.language_strategy == "static"
  270. assert "Unknown language_strategy" in caplog.text
  271. assert "bogus" in caplog.text
  272. def test_sticky_redetect_logprob_defaults_to_minus_0_8():
  273. """sticky_redetect_logprob defaults to -0.8."""
  274. with tempfile.TemporaryDirectory() as tmpdir:
  275. import os
  276. old_cwd = os.getcwd()
  277. try:
  278. os.chdir(tmpdir)
  279. cfg = load_config()
  280. finally:
  281. os.chdir(old_cwd)
  282. assert cfg.whisper.sticky_redetect_logprob == -0.8
  283. def test_sticky_redetect_logprob_is_parsed():
  284. """sticky_redetect_logprob is parsed from TOML."""
  285. config_toml = """
  286. [whisper]
  287. sticky_redetect_logprob = -0.5
  288. """
  289. with tempfile.TemporaryDirectory() as tmpdir:
  290. config_path = Path(tmpdir) / "config.toml"
  291. config_path.write_text(config_toml)
  292. cfg = load_config(str(config_path))
  293. assert cfg.whisper.sticky_redetect_logprob == -0.5