tts-voice-creator-clone-and.../tests/test_voice_embedding_package.py
mARTin-B78 e999a8f38b fix(voices): keep per-model speaker embeddings with their voice package
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 05:40:45 +02:00

74 lines
3.3 KiB
Python

"""Per-model speaker embeddings (<name>.1.7b.pt / <name>.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()