cronos3k commited on
Commit
16e2a0a
·
verified ·
1 Parent(s): 0d1b9e9

Upload voice_library.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. voice_library.py +130 -0
voice_library.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Voice Library — persistent store of (name, audio_path, transcription) profiles.
3
+
4
+ Files live in ./voices/ (project-local, never Windows user dirs).
5
+ Index is ./voices/library.json.
6
+
7
+ Usage:
8
+ lib = VoiceLibrary()
9
+ lib.add("Alice", "/path/to/alice.wav", "Hello, my name is Alice.")
10
+ voice = lib.get("Alice")
11
+ names = lib.names()
12
+ lib.remove("Alice")
13
+ """
14
+
15
+ import json
16
+ import shutil
17
+ import time
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ VOICES_DIR = Path(__file__).parent / "voices"
23
+ LIBRARY_FILE = VOICES_DIR / "library.json"
24
+
25
+
26
+ class VoiceLibrary:
27
+ def __init__(self):
28
+ VOICES_DIR.mkdir(exist_ok=True)
29
+ self._data: dict = self._load()
30
+
31
+ # ------------------------------------------------------------------
32
+ # Persistence
33
+ # ------------------------------------------------------------------
34
+
35
+ def _load(self) -> dict:
36
+ if LIBRARY_FILE.exists():
37
+ try:
38
+ return json.loads(LIBRARY_FILE.read_text(encoding="utf-8"))
39
+ except Exception:
40
+ pass
41
+ return {"voices": {}}
42
+
43
+ def _save(self):
44
+ LIBRARY_FILE.write_text(
45
+ json.dumps(self._data, indent=2, ensure_ascii=False),
46
+ encoding="utf-8",
47
+ )
48
+
49
+ def _reload(self):
50
+ """Re-read from disk (picks up changes from other processes)."""
51
+ self._data = self._load()
52
+
53
+ # ------------------------------------------------------------------
54
+ # Core API
55
+ # ------------------------------------------------------------------
56
+
57
+ def add(self, name: str, audio_src: str, transcription: str) -> dict:
58
+ """
59
+ Add or overwrite a voice profile.
60
+
61
+ The audio file is COPIED into ./voices/ so the library is self-contained.
62
+ Returns the stored entry dict.
63
+ """
64
+ name = name.strip()
65
+ if not name:
66
+ raise ValueError("Voice name must not be empty.")
67
+
68
+ # Copy audio into voices dir
69
+ src = Path(audio_src)
70
+ # Use a safe filename derived from the name + timestamp
71
+ safe = "".join(c if c.isalnum() or c in "-_ " else "_" for c in name).strip()
72
+ dest = VOICES_DIR / f"{safe}_{int(time.time())}{src.suffix or '.wav'}"
73
+ shutil.copy2(str(src), str(dest))
74
+
75
+ entry = {
76
+ "name": name,
77
+ "audio_path": str(dest),
78
+ "transcription": transcription.strip(),
79
+ "added": datetime.now().isoformat(timespec="seconds"),
80
+ }
81
+ self._reload()
82
+ self._data["voices"][name] = entry
83
+ self._save()
84
+ return entry
85
+
86
+ def get(self, name: str) -> Optional[dict]:
87
+ """Return entry dict or None."""
88
+ self._reload()
89
+ return self._data["voices"].get(name)
90
+
91
+ def names(self) -> list[str]:
92
+ """Sorted list of voice names."""
93
+ self._reload()
94
+ return sorted(self._data["voices"].keys())
95
+
96
+ def remove(self, name: str) -> bool:
97
+ """Delete a voice profile (and its audio file). Returns True if it existed."""
98
+ self._reload()
99
+ entry = self._data["voices"].pop(name, None)
100
+ if entry is None:
101
+ return False
102
+ audio = Path(entry.get("audio_path", ""))
103
+ if audio.exists() and audio.parent == VOICES_DIR:
104
+ audio.unlink(missing_ok=True)
105
+ self._save()
106
+ return True
107
+
108
+ def all_entries(self) -> list[dict]:
109
+ """All entries as a list, sorted by name."""
110
+ self._reload()
111
+ return sorted(self._data["voices"].values(), key=lambda e: e["name"].lower())
112
+
113
+ def summary_text(self) -> str:
114
+ entries = self.all_entries()
115
+ if not entries:
116
+ return "Voice library is empty. Save a voice to get started."
117
+ lines = [f"{len(entries)} saved voice(s):"]
118
+ for e in entries:
119
+ lines.append(f" • {e['name']} — \"{e['transcription'][:60]}{'…' if len(e['transcription'])>60 else ''}\"")
120
+ return "\n".join(lines)
121
+
122
+
123
+ # Module-level singleton
124
+ _library: Optional[VoiceLibrary] = None
125
+
126
+ def get_library() -> VoiceLibrary:
127
+ global _library
128
+ if _library is None:
129
+ _library = VoiceLibrary()
130
+ return _library