"""Tests for the faster-qwen3-tts backend setup module (backends/faster.py).""" import json import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch from backends import faster as make_voices class FindWavFilesTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.folder = Path(self._tmp.name) def tearDown(self): self._tmp.cleanup() def _touch(self, name): path = self.folder / name path.write_bytes(b"x") return path def test_finds_only_wavs_case_insensitive(self): self._touch("b.wav") self._touch("a.WAV") self._touch("notes.txt") (self.folder / "sub").mkdir() (self.folder / "sub" / "c.wav").write_bytes(b"x") names = [path.name for path in make_voices.find_wav_files(self.folder)] self.assertEqual(names, ["a.WAV", "b.wav"]) def test_sorted_alphabetically_case_insensitive(self): for name in ("Zed.wav", "alpha.wav", "Beta.wav"): self._touch(name) names = [path.name for path in make_voices.find_wav_files(self.folder)] self.assertEqual(names, ["alpha.wav", "Beta.wav", "Zed.wav"]) def test_empty_directory_returns_empty_list(self): self.assertEqual(make_voices.find_wav_files(self.folder), []) class BuildVoicesTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.folder = Path(self._tmp.name) self.narrator = self.folder / "narrator.wav" self.narrator.write_bytes(b"x") self.other = self.folder / "other.wav" self.other.write_bytes(b"x") def tearDown(self): self._tmp.cleanup() def test_voices_named_after_basenames_with_absolute_paths(self): transcripts = {str(self.narrator): "First transcript.", str(self.other): "Second transcript."} with patch.object(make_voices, "transcribe_reference_audio", side_effect=lambda path, model_name="base": transcripts[path]): voices = make_voices.build_voices([self.narrator, self.other], "English", "base") self.assertEqual(list(voices), ["narrator", "other"]) self.assertEqual(voices["narrator"]["ref_text"], "First transcript.") self.assertEqual(voices["narrator"]["language"], "English") self.assertTrue(Path(voices["narrator"]["ref_audio"]).is_absolute()) self.assertEqual(Path(voices["narrator"]["ref_audio"]), self.narrator.resolve()) def test_failed_transcription_keeps_entry_with_empty_text(self): with patch.object(make_voices, "transcribe_reference_audio", return_value=None): voices = make_voices.build_voices([self.narrator], "English", "base") self.assertEqual(voices["narrator"]["ref_text"], "") def test_whisper_model_name_is_passed_through(self): with patch.object(make_voices, "transcribe_reference_audio", return_value="text") as mock_transcribe: make_voices.build_voices([self.narrator], "English", "large-v3") self.assertEqual(mock_transcribe.call_args.kwargs["model_name"], "large-v3") class MainTests(unittest.TestCase): """The flag-only (non-TUI) path through main(), end to end.""" def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.folder = Path(self._tmp.name) (self.folder / "narrator.wav").write_bytes(b"x") (self.folder / "alpha.wav").write_bytes(b"x") self.output = self.folder / "voices.json" # Avoid touching the real converter/config.py and pip/git. patcher = patch.object(make_voices.common, "update_config_value", return_value=False) patcher.start() self.addCleanup(patcher.stop) patcher = patch.object(make_voices, "_interactive", return_value=False) patcher.start() self.addCleanup(patcher.stop) def tearDown(self): self._tmp.cleanup() def _run(self, argv): with patch.object(sys, "argv", ["backends/faster.py"] + argv), \ patch.object(make_voices, "transcribe_reference_audio", return_value="hello"): return make_voices.main() def test_writes_json_with_alphabetical_voice_order(self): exit_code = self._run([str(self.folder), "--output", str(self.output), "--skip-install", "--skip-clone"]) self.assertEqual(exit_code, 0) data = json.loads(self.output.read_text(encoding="utf-8")) self.assertEqual(list(data), ["alpha", "narrator"]) self.assertEqual(data["alpha"]["ref_text"], "hello") self.assertEqual(data["alpha"]["language"], "English") def test_custom_output_path(self): custom = Path(self._tmp.name) / "custom.json" exit_code = self._run([str(self.folder), "--output", str(custom), "--skip-install", "--skip-clone"]) self.assertEqual(exit_code, 0) self.assertTrue(custom.exists()) self.assertFalse(self.output.exists()) def test_invalid_language_errors_before_work(self): with patch.object(make_voices, "transcribe_reference_audio") as mock_transcribe: with self.assertRaises(SystemExit) as ctx: self._run([str(self.folder), "--output", str(self.output), "--language", "klingon", "--skip-install", "--skip-clone"]) self.assertEqual(ctx.exception.code, 2) mock_transcribe.assert_not_called() def test_missing_input_dir_errors(self): with self.assertRaises(SystemExit) as ctx: self._run([str(self.folder / "nope"), "--output", str(self.output), "--skip-install", "--skip-clone"]) self.assertEqual(ctx.exception.code, 2) def test_no_wav_files_returns_error(self): empty = Path(tempfile.mkdtemp()) try: exit_code = self._run([str(empty), "--output", str(empty / "voices.json"), "--skip-install", "--skip-clone"]) self.assertEqual(exit_code, 1) finally: empty.rmdir() def test_existing_output_declined_keeps_file(self): self.output.write_text('{"old": true}', encoding="utf-8") with patch.object(make_voices, "transcribe_reference_audio") as mock_transcribe: exit_code = self._run([str(self.folder), "--output", str(self.output), "--skip-install", "--skip-clone"]) self.assertEqual(exit_code, 1) mock_transcribe.assert_not_called() self.assertEqual(json.loads(self.output.read_text(encoding="utf-8")), {"old": True}) def test_force_overwrites_without_prompt(self): self.output.write_text('{"old": true}', encoding="utf-8") exit_code = self._run([str(self.folder), "--output", str(self.output), "--force", "--skip-install", "--skip-clone"]) self.assertEqual(exit_code, 0) data = json.loads(self.output.read_text(encoding="utf-8")) self.assertEqual(list(data), ["alpha", "narrator"]) if __name__ == "__main__": unittest.main()