gavanduffy commited on
Commit
f6723e7
·
1 Parent(s): e87a9a5

Add custom voice presets (F6, F7, M6) from JSON style embeddings

Browse files
app/config.py CHANGED
@@ -41,6 +41,12 @@ class Config:
41
  # Voice directory for custom voice samples
42
  VOICES_DIR = os.environ.get('SUPERTONIC3_VOICES_DIR', None)
43
 
 
 
 
 
 
 
44
  DEFAULT_VOICE = os.environ.get('SUPERTONIC3_DEFAULT_VOICE', 'M1')
45
 
46
  # Built-in voice names (M1-M5 = male, F1-F5 = female)
@@ -49,6 +55,9 @@ class Config:
49
  # Supported extensions for custom voice samples
50
  VOICE_EXTENSIONS = ('.wav', '.mp3', '.flac')
51
 
 
 
 
52
  # Streaming default
53
  STREAM_DEFAULT = os.environ.get('SUPERTONIC3_STREAM_DEFAULT', 'false').lower() == 'true'
54
 
 
41
  # Voice directory for custom voice samples
42
  VOICES_DIR = os.environ.get('SUPERTONIC3_VOICES_DIR', None)
43
 
44
+ # Voice presets directory (JSON style embeddings, tracked in git)
45
+ VOICE_PRESETS_DIR = os.environ.get(
46
+ 'SUPERTONIC3_VOICE_PRESETS_DIR',
47
+ str(BASE_PATH / 'voice_presets'),
48
+ )
49
+
50
  DEFAULT_VOICE = os.environ.get('SUPERTONIC3_DEFAULT_VOICE', 'M1')
51
 
52
  # Built-in voice names (M1-M5 = male, F1-F5 = female)
 
55
  # Supported extensions for custom voice samples
56
  VOICE_EXTENSIONS = ('.wav', '.mp3', '.flac')
57
 
58
+ # Supported extensions for voice preset files (JSON style embeddings)
59
+ VOICE_PRESET_EXTENSIONS = ('.json',)
60
+
61
  # Streaming default
62
  STREAM_DEFAULT = os.environ.get('SUPERTONIC3_STREAM_DEFAULT', 'false').lower() == 'true'
63
 
app/services/tts.py CHANGED
@@ -1,22 +1,28 @@
 
1
  import os
2
  import re
3
  import time
4
  from pathlib import Path
5
 
 
 
6
  from app.config import Config
7
  from app.logging_config import get_logger
8
 
9
  logger = get_logger('tts')
10
 
11
  TTS = None
 
12
 
13
 
14
  def _ensure_supertonic():
15
- global TTS
16
  if TTS is None:
17
  try:
18
  from supertonic import TTS as _TTS
 
19
  TTS = _TTS
 
20
  except ImportError as exc:
21
  raise ImportError('supertonic not found. Install with: pip install supertonic') from exc
22
 
@@ -26,6 +32,9 @@ class TTSService:
26
  self._tts = None
27
  self._model_loaded = False
28
  self._voices_dir: str | None = None
 
 
 
29
  # Pre-parse text into segments for simulated streaming
30
  self._sentence_splitter = re.compile(r'(?<=[.!?])\s+')
31
 
@@ -56,6 +65,8 @@ class TTSService:
56
  logger.error(f'Failed to load SuperTonic3 model: {e}')
57
  raise
58
 
 
 
59
  def set_voices_dir(self, voices_dir: str | None) -> None:
60
  if voices_dir and os.path.isdir(voices_dir):
61
  self._voices_dir = voices_dir
@@ -66,16 +77,61 @@ class TTSService:
66
  else:
67
  self._voices_dir = None
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def get_voice_style(self, voice_id: str):
70
  if not self.is_loaded:
71
  raise RuntimeError('Model not loaded. Call load_model() first.')
72
 
 
 
 
 
 
 
 
 
73
  voice_name = self._resolve_voice(voice_id)
74
  t0 = time.time()
75
  style = self._tts.get_voice_style(voice_name=voice_name)
76
  logger.debug(f'Voice style loaded in {time.time() - t0:.2f}s: {voice_name}')
77
  return style
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  def _resolve_voice(self, voice_id: str) -> str:
80
  voice_lower = voice_id.lower()
81
  if voice_lower in [v.lower() for v in Config.BUILTIN_VOICES]:
@@ -104,9 +160,13 @@ class TTSService:
104
  False,
105
  'HTTP/HTTPS URLs are not allowed for security reasons. Use hf:// for HuggingFace models.',
106
  )
 
107
  voice_lower = voice_id.lower()
108
  if voice_lower in [v.lower() for v in Config.BUILTIN_VOICES]:
109
  return True, f'Built-in voice: {voice_id}'
 
 
 
110
  if self._voices_dir:
111
  p = Path(self._voices_dir) / voice_id
112
  if p.exists():
@@ -156,6 +216,8 @@ class TTSService:
156
  voices: list[dict] = []
157
  for name in Config.BUILTIN_VOICES:
158
  voices.append({'id': name, 'name': name, 'type': 'builtin'})
 
 
159
  if self._voices_dir:
160
  for f in sorted(Path(self._voices_dir).iterdir()):
161
  if f.suffix.lower() in Config.VOICE_EXTENSIONS:
 
1
+ import json
2
  import os
3
  import re
4
  import time
5
  from pathlib import Path
6
 
7
+ import numpy as np
8
+
9
  from app.config import Config
10
  from app.logging_config import get_logger
11
 
12
  logger = get_logger('tts')
13
 
14
  TTS = None
15
+ STYLE_CLS = None
16
 
17
 
18
  def _ensure_supertonic():
19
+ global TTS, STYLE_CLS
20
  if TTS is None:
21
  try:
22
  from supertonic import TTS as _TTS
23
+ from supertonic.core import Style as _Style
24
  TTS = _TTS
25
+ STYLE_CLS = _Style
26
  except ImportError as exc:
27
  raise ImportError('supertonic not found. Install with: pip install supertonic') from exc
28
 
 
32
  self._tts = None
33
  self._model_loaded = False
34
  self._voices_dir: str | None = None
35
+ self._voice_presets_dir: str | None = None
36
+ self._preset_voices: dict[str, str] = {}
37
+ self._style_cache: dict[str, object] = {}
38
  # Pre-parse text into segments for simulated streaming
39
  self._sentence_splitter = re.compile(r'(?<=[.!?])\s+')
40
 
 
65
  logger.error(f'Failed to load SuperTonic3 model: {e}')
66
  raise
67
 
68
+ self.discover_presets()
69
+
70
  def set_voices_dir(self, voices_dir: str | None) -> None:
71
  if voices_dir and os.path.isdir(voices_dir):
72
  self._voices_dir = voices_dir
 
77
  else:
78
  self._voices_dir = None
79
 
80
+ def discover_presets(self) -> None:
81
+ self._preset_voices = {}
82
+ presets_dir = Config.VOICE_PRESETS_DIR
83
+ if not os.path.isdir(presets_dir):
84
+ logger.info(f'Voice presets directory not found: {presets_dir}')
85
+ return
86
+ self._voice_presets_dir = presets_dir
87
+ for f in sorted(Path(presets_dir).iterdir()):
88
+ if f.suffix.lower() in Config.VOICE_PRESET_EXTENSIONS and f.stem.isidentifier():
89
+ self._preset_voices[f.stem] = str(f)
90
+ if self._preset_voices:
91
+ logger.info(
92
+ f'Discovered {len(self._preset_voices)} voice presets: '
93
+ f'{", ".join(self._preset_voices.keys())}'
94
+ )
95
+
96
  def get_voice_style(self, voice_id: str):
97
  if not self.is_loaded:
98
  raise RuntimeError('Model not loaded. Call load_model() first.')
99
 
100
+ # Check if it's a cached preset voice
101
+ if voice_id in self._style_cache:
102
+ return self._style_cache[voice_id]
103
+
104
+ # Check if it's a known preset voice (load from JSON)
105
+ if voice_id in self._preset_voices:
106
+ return self._load_preset_style(voice_id)
107
+
108
  voice_name = self._resolve_voice(voice_id)
109
  t0 = time.time()
110
  style = self._tts.get_voice_style(voice_name=voice_name)
111
  logger.debug(f'Voice style loaded in {time.time() - t0:.2f}s: {voice_name}')
112
  return style
113
 
114
+ def _load_preset_style(self, voice_id: str):
115
+ filepath = self._preset_voices.get(voice_id)
116
+ if not filepath:
117
+ raise ValueError(f'Preset voice not found: {voice_id}')
118
+
119
+ t0 = time.time()
120
+ with open(filepath) as f:
121
+ data = json.load(f)
122
+
123
+ style_ttl = np.array(data['style_ttl']['data'], dtype=data['style_ttl']['type']).reshape(
124
+ data['style_ttl']['dims']
125
+ )
126
+ style_dp = np.array(data['style_dp']['data'], dtype=data['style_dp']['type']).reshape(
127
+ data['style_dp']['dims']
128
+ )
129
+
130
+ style = STYLE_CLS(style_ttl_onnx=style_ttl, style_dp_onnx=style_dp)
131
+ self._style_cache[voice_id] = style
132
+ logger.info(f'Loaded preset voice {voice_id} in {time.time() - t0:.2f}s')
133
+ return style
134
+
135
  def _resolve_voice(self, voice_id: str) -> str:
136
  voice_lower = voice_id.lower()
137
  if voice_lower in [v.lower() for v in Config.BUILTIN_VOICES]:
 
160
  False,
161
  'HTTP/HTTPS URLs are not allowed for security reasons. Use hf:// for HuggingFace models.',
162
  )
163
+ # Check built-in voices
164
  voice_lower = voice_id.lower()
165
  if voice_lower in [v.lower() for v in Config.BUILTIN_VOICES]:
166
  return True, f'Built-in voice: {voice_id}'
167
+ # Check preset voices (JSON style embeddings)
168
+ if voice_id in self._preset_voices:
169
+ return True, f'Preset voice: {voice_id}'
170
  if self._voices_dir:
171
  p = Path(self._voices_dir) / voice_id
172
  if p.exists():
 
216
  voices: list[dict] = []
217
  for name in Config.BUILTIN_VOICES:
218
  voices.append({'id': name, 'name': name, 'type': 'builtin'})
219
+ for name in sorted(self._preset_voices):
220
+ voices.append({'id': name, 'name': name, 'type': 'preset'})
221
  if self._voices_dir:
222
  for f in sorted(Path(self._voices_dir).iterdir()):
223
  if f.suffix.lower() in Config.VOICE_EXTENSIONS:
voice_presets/F6.json ADDED
The diff for this file is too large to render. See raw diff
 
voice_presets/F7.json ADDED
The diff for this file is too large to render. See raw diff
 
voice_presets/M6.json ADDED
The diff for this file is too large to render. See raw diff