"""Per-model speaker embeddings (.1.7b.pt / .0.6b.pt) travel with their voice.""" import json import os import tempfile import unittest from pathlib import Path os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="voice-embedding-tests-")) from core.voice import _move_voice_package, _remove_voice_package, _voice_embedding_paths, _voice_package_paths class VoiceEmbeddingPackageTests(unittest.TestCase): def setUp(self): self.root = Path(tempfile.mkdtemp(prefix="voice-pkg-")) self.active = self.root / "active_voices" self.active.mkdir() def make(self, stem="DE_Banker", extra=()): for name in (f"{stem}.wav", f"{stem}.reference.txt", f"{stem}.1.7b.pt", f"{stem}.0.6b.pt", *extra): (self.active / name).write_bytes(b"x") (self.active / f"{stem}.meta.json").write_text(json.dumps({})) return self.active / f"{stem}.wav" def test_only_this_voices_embeddings_are_part_of_the_package(self): wav = self.make(extra=("DE_Banker_2.1.7b.pt", "DE_Banker.notes.pt", "DE_Banker.1.7b.pt.bak", "Other.1.7b.pt")) names = sorted(p.name for p in _voice_embedding_paths(wav)) self.assertEqual(names, ["DE_Banker.0.6b.pt", "DE_Banker.1.7b.pt"]) self.assertTrue({"DE_Banker.0.6b.pt", "DE_Banker.1.7b.pt"} <= {p.name for p in _voice_package_paths(wav)}) def test_hiding_a_voice_moves_its_embeddings_and_leaves_no_orphans(self): wav = self.make() hidden = self.root / "hidden_voices" moved = _move_voice_package(wav, hidden) self.assertEqual(moved.parent, hidden) self.assertTrue((hidden / "DE_Banker.1.7b.pt").exists() and (hidden / "DE_Banker.0.6b.pt").exists()) self.assertEqual([p.name for p in self.active.iterdir() if p.suffix == ".pt"], []) def test_removing_a_voice_removes_its_embeddings(self): wav = self.make() _remove_voice_package(wav) self.assertEqual(list(self.active.iterdir()), []) def test_names_with_regex_characters_are_matched_literally(self): wav = self.make("A+B (1)", extra=("AxB (1).1.7b.pt",)) self.assertEqual(sorted(p.name for p in _voice_embedding_paths(wav)), ["A+B (1).0.6b.pt", "A+B (1).1.7b.pt"]) class RenameKeepsEmbeddingsTests(unittest.TestCase): def test_rename_renames_embeddings_next_to_the_audio(self): import asyncio from unittest.mock import AsyncMock, Mock, patch from routes import library root = Path(tempfile.mkdtemp(prefix="voice-rename-")) active = root / "active_voices" active.mkdir() for name in ("Old.wav", "Old.reference.txt", "Old.meta.json", "Old.1.7b.pt", "Old.0.6b.pt"): (active / name).write_text("{}" if name.endswith(".json") else "x") request = Mock() request.json = AsyncMock(return_value={"old_id": "Old", "new_id": "New"}) with patch.object(library, "_load_settings", return_value={"voices_scan_dir": str(root)}), \ patch.object(library, "upsert_voice_in_index"), patch.object(library, "remove_voice_from_index"): asyncio.run(library.rename_voice(request)) names = sorted(p.name for p in active.iterdir()) self.assertIn("New.1.7b.pt", names) self.assertIn("New.0.6b.pt", names) self.assertFalse([n for n in names if n.startswith("Old.")]) if __name__ == "__main__": unittest.main()