voicerender 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voicerender/__init__.py +127 -0
- voicerender/__main__.py +3 -0
- voicerender/_version.py +24 -0
- voicerender/capacity.py +678 -0
- voicerender/cli.py +345 -0
- voicerender/engines/__init__.py +68 -0
- voicerender/engines/api_probe.py +495 -0
- voicerender/engines/base.py +425 -0
- voicerender/engines/catalog.py +83 -0
- voicerender/engines/inflectsynth.py +920 -0
- voicerender/engines/kittensynth.py +511 -0
- voicerender/engines/kokoro.py +725 -0
- voicerender/engines/metadata.py +58 -0
- voicerender/engines/piper.py +173 -0
- voicerender/engines/pipersynth.py +570 -0
- voicerender/engines/pocket.py +169 -0
- voicerender/engines/pocketsynth.py +1234 -0
- voicerender/engines/registry.py +320 -0
- voicerender/engines/selection.py +61 -0
- voicerender/engines/supertonicsynth.py +707 -0
- voicerender/errors.py +161 -0
- voicerender/events.py +43 -0
- voicerender/handoff.py +62 -0
- voicerender/inputs/__init__.py +21 -0
- voicerender/inputs/utterplan_v5.py +897 -0
- voicerender/lexicons.py +42 -0
- voicerender/manifest.py +750 -0
- voicerender/models.py +499 -0
- voicerender/py.typed +0 -0
- voicerender/render.py +365 -0
- voicerender/render_v5.py +1193 -0
- voicerender/schemas/render-manifest-v2.schema.json +415 -0
- voicerender/storage.py +113 -0
- voicerender-0.1.1.dist-info/METADATA +254 -0
- voicerender-0.1.1.dist-info/RECORD +39 -0
- voicerender-0.1.1.dist-info/WHEEL +5 -0
- voicerender-0.1.1.dist-info/entry_points.txt +2 -0
- voicerender-0.1.1.dist-info/licenses/LICENSE +201 -0
- voicerender-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Engine-neutral synthesis contracts owned by VoiceRender."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from contextlib import AbstractContextManager
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
10
|
+
|
|
11
|
+
from .api_probe import EngineApiProbe
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .catalog import CatalogRequest, SynthesisTarget
|
|
15
|
+
from .selection import EngineRequest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def distribution_version(distribution: str) -> str | None:
|
|
19
|
+
"""Return the installed distribution version, or None when unavailable."""
|
|
20
|
+
try:
|
|
21
|
+
return importlib.metadata.version(distribution)
|
|
22
|
+
except importlib.metadata.PackageNotFoundError:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def voice_level_metadata(
|
|
27
|
+
value: Any,
|
|
28
|
+
mode: str,
|
|
29
|
+
*,
|
|
30
|
+
identity_keys: tuple[str, ...],
|
|
31
|
+
revision_keys: tuple[str, ...],
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""Map an engine voice-level calibration record to VoiceRender metadata."""
|
|
34
|
+
metadata = dict(value) if isinstance(value, Mapping) else {}
|
|
35
|
+
|
|
36
|
+
def first_present(keys: tuple[str, ...]) -> Any:
|
|
37
|
+
for key in keys:
|
|
38
|
+
if key in metadata:
|
|
39
|
+
return metadata[key]
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"mode": metadata.get("mode", mode),
|
|
44
|
+
"applied": bool(metadata.get("applied", False)),
|
|
45
|
+
"gain_db": metadata.get("gain_db"),
|
|
46
|
+
"source": metadata.get("source", "none"),
|
|
47
|
+
"reason": metadata.get("reason"),
|
|
48
|
+
"calibration_identity": first_present(identity_keys),
|
|
49
|
+
"calibration_revision": first_present(revision_keys),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class SpeechToken:
|
|
55
|
+
"""One linguistic token with offsets local to a synthesis request."""
|
|
56
|
+
|
|
57
|
+
start: int
|
|
58
|
+
end: int
|
|
59
|
+
text: str
|
|
60
|
+
pos: str | None = None
|
|
61
|
+
tag: str | None = None
|
|
62
|
+
lemma: str | None = None
|
|
63
|
+
morph: str | None = None
|
|
64
|
+
language: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class PronunciationSpan:
|
|
69
|
+
"""A request-local pronunciation override."""
|
|
70
|
+
|
|
71
|
+
start: int
|
|
72
|
+
end: int
|
|
73
|
+
phonemes: str | None = None
|
|
74
|
+
language: str | None = None
|
|
75
|
+
alphabet: str | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class SpeechRequest:
|
|
80
|
+
"""Prepared text and optional linguistic context for one engine synthesis."""
|
|
81
|
+
|
|
82
|
+
id: str
|
|
83
|
+
text: str
|
|
84
|
+
language: str
|
|
85
|
+
voice: str | None = None
|
|
86
|
+
speaker: str | int | None = None
|
|
87
|
+
pronunciation_overrides: tuple[PronunciationSpan, ...] = ()
|
|
88
|
+
tokens: tuple[SpeechToken, ...] = ()
|
|
89
|
+
whole_request_phonemes: str | None = None
|
|
90
|
+
options: Mapping[str, Any] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, slots=True)
|
|
94
|
+
class SpeechWordTiming:
|
|
95
|
+
"""A word timing expressed in request text and rendered-audio coordinates."""
|
|
96
|
+
|
|
97
|
+
text: str
|
|
98
|
+
char_start: int
|
|
99
|
+
char_end: int
|
|
100
|
+
start_sample: int
|
|
101
|
+
end_sample: int
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class RenderedSpeech:
|
|
106
|
+
"""Mono float32 audio and metadata for one independent speech request."""
|
|
107
|
+
|
|
108
|
+
id: str
|
|
109
|
+
audio: Any
|
|
110
|
+
sample_rate: int
|
|
111
|
+
warnings: tuple[str, ...] = ()
|
|
112
|
+
word_timings: tuple[SpeechWordTiming, ...] = ()
|
|
113
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class RequestMeasure:
|
|
118
|
+
"""Capacity measurement for one exact VoiceRender speech request."""
|
|
119
|
+
|
|
120
|
+
fits: bool | None
|
|
121
|
+
amount: int | None
|
|
122
|
+
maximum: int | None
|
|
123
|
+
unit: Literal["model_tokens", "phoneme_ids", "unknown"]
|
|
124
|
+
source: str
|
|
125
|
+
details: Mapping[str, Any] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True)
|
|
129
|
+
class EngineCapabilities:
|
|
130
|
+
"""Synthesis features an engine adapter can represent natively."""
|
|
131
|
+
|
|
132
|
+
id: str = ""
|
|
133
|
+
voice_binding_namespace: str | None = None
|
|
134
|
+
voice_binding_scope: Literal["request", "target"] = "request"
|
|
135
|
+
option_names: frozenset[str] = frozenset()
|
|
136
|
+
supports_named_voices: bool = False
|
|
137
|
+
supports_reference_voice: bool = False
|
|
138
|
+
supports_speakers: bool = False
|
|
139
|
+
supports_pronunciation_overrides: bool = False
|
|
140
|
+
pronunciation_alphabets: frozenset[str] = frozenset()
|
|
141
|
+
supports_linguistic_tokens: bool = False
|
|
142
|
+
supports_whole_request_phonemes: bool = False
|
|
143
|
+
supports_lexicons: bool = False
|
|
144
|
+
supports_model_sources: bool = False
|
|
145
|
+
supports_qualities: bool = False
|
|
146
|
+
supports_live: bool = False
|
|
147
|
+
supports_timestamps: bool = False
|
|
148
|
+
|
|
149
|
+
supports_voice_level_calibration: bool = False
|
|
150
|
+
supports_request_measurement: bool = False
|
|
151
|
+
# Compatibility aliases retained for the original VoiceRender renderer.
|
|
152
|
+
supports_builtin_voices: bool = True
|
|
153
|
+
supports_reference_audio: bool = False
|
|
154
|
+
supports_speaker_selection: bool = False
|
|
155
|
+
supports_word_timings: bool = False
|
|
156
|
+
options: frozenset[str] = frozenset()
|
|
157
|
+
native_semantics: frozenset[str] = frozenset()
|
|
158
|
+
composition_semantics: frozenset[str] = frozenset()
|
|
159
|
+
supports_synthesis_request: bool = False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True, slots=True)
|
|
163
|
+
class EngineDiagnostic:
|
|
164
|
+
"""Stable, machine-readable adapter selection or capability diagnostic."""
|
|
165
|
+
|
|
166
|
+
code: str
|
|
167
|
+
severity: Literal["error", "warning", "info"]
|
|
168
|
+
message: str
|
|
169
|
+
field: str | None = None
|
|
170
|
+
|
|
171
|
+
def as_dict(self) -> dict[str, str]:
|
|
172
|
+
value = {"code": self.code, "severity": self.severity, "message": self.message}
|
|
173
|
+
if self.field is not None:
|
|
174
|
+
value["field"] = self.field
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class EngineSelection:
|
|
180
|
+
"""Concrete engine selection produced during plan resolution."""
|
|
181
|
+
|
|
182
|
+
engine: str
|
|
183
|
+
target_id: str
|
|
184
|
+
language: str
|
|
185
|
+
voice: str | None = None
|
|
186
|
+
speaker: str | int | None = None
|
|
187
|
+
options: Mapping[str, Any] = field(default_factory=dict)
|
|
188
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
189
|
+
offline: bool = False
|
|
190
|
+
refresh: bool = False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class EngineSession(Protocol):
|
|
194
|
+
"""Open synthesis session that renders one VoiceRender speech request at a time."""
|
|
195
|
+
|
|
196
|
+
def measure(self, request: SpeechRequest) -> RequestMeasure:
|
|
197
|
+
"""Measure request capacity without performing acoustic inference."""
|
|
198
|
+
...
|
|
199
|
+
|
|
200
|
+
def synthesize(self, request: SpeechRequest) -> RenderedSpeech:
|
|
201
|
+
"""Synthesize one independent request without composing other requests."""
|
|
202
|
+
...
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class EngineAdapter(Protocol):
|
|
206
|
+
"""Discovery, target resolution, and session opening for one synthesis engine."""
|
|
207
|
+
|
|
208
|
+
id: str
|
|
209
|
+
package_name: str
|
|
210
|
+
|
|
211
|
+
def probe_api(self) -> EngineApiProbe:
|
|
212
|
+
"""Return the structured public API compatibility diagnosis."""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
def compatible_api(self) -> bool:
|
|
216
|
+
"""Return whether the public API is compatible (legacy wrapper)."""
|
|
217
|
+
...
|
|
218
|
+
|
|
219
|
+
def version(self) -> str | None:
|
|
220
|
+
"""Return the installed engine package version, or None."""
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
def capabilities(self) -> EngineCapabilities:
|
|
224
|
+
"""Return the synthesis features supported by this adapter."""
|
|
225
|
+
...
|
|
226
|
+
|
|
227
|
+
def discover(
|
|
228
|
+
self,
|
|
229
|
+
request: CatalogRequest,
|
|
230
|
+
) -> tuple[SynthesisTarget, ...]:
|
|
231
|
+
"""Discover available targets for this engine."""
|
|
232
|
+
...
|
|
233
|
+
|
|
234
|
+
def resolve(
|
|
235
|
+
self,
|
|
236
|
+
request: EngineRequest,
|
|
237
|
+
) -> tuple[EngineSelection, tuple[Any, ...]]:
|
|
238
|
+
"""Resolve and validate a concrete engine selection."""
|
|
239
|
+
...
|
|
240
|
+
|
|
241
|
+
def validate_selection(
|
|
242
|
+
self, selection: EngineSelection
|
|
243
|
+
) -> tuple[EngineDiagnostic, ...]:
|
|
244
|
+
"""Validate resolved engine/model/voice choices without synthesizing."""
|
|
245
|
+
...
|
|
246
|
+
|
|
247
|
+
def canonical_synthesis_identity(
|
|
248
|
+
self,
|
|
249
|
+
selection: EngineSelection,
|
|
250
|
+
) -> Mapping[str, Any]:
|
|
251
|
+
"""Return identity inputs for canonical speech synthesis."""
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
def open(
|
|
255
|
+
self,
|
|
256
|
+
selection: EngineSelection,
|
|
257
|
+
) -> AbstractContextManager[EngineSession]:
|
|
258
|
+
"""Open a rendering session for the given concrete target."""
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def validate_word_timings(
|
|
263
|
+
*,
|
|
264
|
+
text: str,
|
|
265
|
+
frame_count: int,
|
|
266
|
+
timings: Sequence[SpeechWordTiming],
|
|
267
|
+
context: Mapping[str, object] | None = None,
|
|
268
|
+
) -> tuple[SpeechWordTiming, ...]:
|
|
269
|
+
"""Validate request-text and rendered-audio coordinates for native or derived words."""
|
|
270
|
+
from numbers import Integral
|
|
271
|
+
|
|
272
|
+
label = ""
|
|
273
|
+
if context:
|
|
274
|
+
label = (
|
|
275
|
+
" ("
|
|
276
|
+
+ ", ".join(f"{key}={value}" for key, value in sorted(context.items()))
|
|
277
|
+
+ ")"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def invalid(reason: str) -> ValueError:
|
|
281
|
+
return ValueError(f"{reason}{label}")
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
isinstance(frame_count, bool)
|
|
285
|
+
or not isinstance(frame_count, Integral)
|
|
286
|
+
or frame_count < 0
|
|
287
|
+
):
|
|
288
|
+
raise invalid("frame count must be a non-negative integer")
|
|
289
|
+
previous_char_start = previous_char_end = -1
|
|
290
|
+
previous_sample_start = previous_sample_end = -1
|
|
291
|
+
for timing in timings:
|
|
292
|
+
if not isinstance(timing.text, str):
|
|
293
|
+
raise invalid("word timing text must be a string")
|
|
294
|
+
coordinates = (
|
|
295
|
+
timing.char_start,
|
|
296
|
+
timing.char_end,
|
|
297
|
+
timing.start_sample,
|
|
298
|
+
timing.end_sample,
|
|
299
|
+
)
|
|
300
|
+
if any(
|
|
301
|
+
isinstance(value, bool) or not isinstance(value, Integral)
|
|
302
|
+
for value in coordinates
|
|
303
|
+
):
|
|
304
|
+
raise invalid("word timing coordinates must be integers")
|
|
305
|
+
char_start, char_end, sample_start, sample_end = (
|
|
306
|
+
int(value) for value in coordinates
|
|
307
|
+
)
|
|
308
|
+
if not (0 <= char_start <= char_end <= len(text)):
|
|
309
|
+
raise invalid("word timing character range is outside the request text")
|
|
310
|
+
if not (0 <= sample_start <= sample_end <= frame_count):
|
|
311
|
+
raise invalid("word timing sample range is outside the rendered audio")
|
|
312
|
+
if char_start < previous_char_start or char_end < previous_char_end:
|
|
313
|
+
raise invalid("word timings are not monotonic in request text")
|
|
314
|
+
if sample_start < previous_sample_start or sample_end < previous_sample_end:
|
|
315
|
+
raise invalid("word timings are not monotonic in rendered audio")
|
|
316
|
+
previous_char_start, previous_char_end = char_start, char_end
|
|
317
|
+
previous_sample_start, previous_sample_end = sample_start, sample_end
|
|
318
|
+
return tuple(timings)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def validate_rendered_speech(
|
|
322
|
+
request: SpeechRequest,
|
|
323
|
+
result: RenderedSpeech,
|
|
324
|
+
*,
|
|
325
|
+
engine: str | None = None,
|
|
326
|
+
engine_version: str | None = None,
|
|
327
|
+
target_id: str | None = None,
|
|
328
|
+
) -> RenderedSpeech:
|
|
329
|
+
"""Validate and normalize an engine result before VoiceRender consumes it."""
|
|
330
|
+
from numbers import Integral
|
|
331
|
+
|
|
332
|
+
import numpy as np
|
|
333
|
+
|
|
334
|
+
from ..errors import EngineBackendError
|
|
335
|
+
|
|
336
|
+
def invalid(reason: str, cause: Exception | None = None) -> EngineBackendError:
|
|
337
|
+
error = EngineBackendError(
|
|
338
|
+
f"Engine returned invalid rendered speech: {reason}",
|
|
339
|
+
engine=engine,
|
|
340
|
+
engine_version=engine_version,
|
|
341
|
+
target_id=target_id,
|
|
342
|
+
language=request.language,
|
|
343
|
+
voice=request.voice,
|
|
344
|
+
speaker=request.speaker,
|
|
345
|
+
request_id=request.id,
|
|
346
|
+
)
|
|
347
|
+
if cause is not None:
|
|
348
|
+
error.__cause__ = cause
|
|
349
|
+
return error
|
|
350
|
+
|
|
351
|
+
if result.id != request.id:
|
|
352
|
+
raise invalid(
|
|
353
|
+
f"result id {result.id!r} does not match request id {request.id!r}"
|
|
354
|
+
)
|
|
355
|
+
if isinstance(result.sample_rate, bool) or not isinstance(
|
|
356
|
+
result.sample_rate, Integral
|
|
357
|
+
):
|
|
358
|
+
raise invalid("sample rate must be a positive integer")
|
|
359
|
+
sample_rate = int(result.sample_rate)
|
|
360
|
+
if sample_rate <= 0:
|
|
361
|
+
raise invalid("sample rate must be a positive integer")
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
audio = np.asarray(result.audio, dtype=np.float32)
|
|
365
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
366
|
+
raise invalid("audio must convert to float32", exc) from exc
|
|
367
|
+
if audio.ndim == 2 and audio.shape[1] == 1:
|
|
368
|
+
audio = audio[:, 0]
|
|
369
|
+
if audio.ndim != 1:
|
|
370
|
+
raise invalid("audio must be mono")
|
|
371
|
+
if audio.size == 0:
|
|
372
|
+
raise invalid("audio must be non-empty")
|
|
373
|
+
if not np.isfinite(audio).all():
|
|
374
|
+
raise invalid("audio must contain only finite samples")
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
result.word_timings = validate_word_timings(
|
|
378
|
+
text=request.text,
|
|
379
|
+
frame_count=int(audio.size),
|
|
380
|
+
timings=result.word_timings,
|
|
381
|
+
)
|
|
382
|
+
except ValueError as exc:
|
|
383
|
+
raise invalid(str(exc)) from exc
|
|
384
|
+
|
|
385
|
+
result.audio = audio
|
|
386
|
+
result.sample_rate = sample_rate
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@dataclass(frozen=True, slots=True)
|
|
391
|
+
class SynthesisRequest:
|
|
392
|
+
"""Compatibility request for the original VoiceRender v5 renderer."""
|
|
393
|
+
|
|
394
|
+
request_id: str
|
|
395
|
+
text: str
|
|
396
|
+
language: str
|
|
397
|
+
directives: Mapping[str, Any] = field(default_factory=dict)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@dataclass(frozen=True, slots=True)
|
|
401
|
+
class SynthesisOutput:
|
|
402
|
+
"""Compatibility result for adapters registered by legacy integrations."""
|
|
403
|
+
|
|
404
|
+
audio: Any
|
|
405
|
+
sample_rate: int
|
|
406
|
+
word_timings: tuple[Mapping[str, Any], ...] = ()
|
|
407
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
__all__ = [
|
|
411
|
+
"EngineAdapter",
|
|
412
|
+
"EngineCapabilities",
|
|
413
|
+
"EngineDiagnostic",
|
|
414
|
+
"EngineSelection",
|
|
415
|
+
"EngineSession",
|
|
416
|
+
"PronunciationSpan",
|
|
417
|
+
"RenderedSpeech",
|
|
418
|
+
"RequestMeasure",
|
|
419
|
+
"SpeechRequest",
|
|
420
|
+
"SpeechToken",
|
|
421
|
+
"SpeechWordTiming",
|
|
422
|
+
"SynthesisOutput",
|
|
423
|
+
"SynthesisRequest",
|
|
424
|
+
"validate_rendered_speech",
|
|
425
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Neutral catalog types for engine discovery.
|
|
2
|
+
|
|
3
|
+
This module provides engine-neutral types for representing synthesis targets
|
|
4
|
+
(voice bundles, models, etc.) without forcing them into PyKokoro-shaped data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class TargetVoice:
|
|
16
|
+
"""Typed descriptive metadata and synthesis-language capabilities for a voice.
|
|
17
|
+
|
|
18
|
+
Adapters populate canonical VoiceRender values; generic layers only project them.
|
|
19
|
+
``languages`` describes this voice's synthesis-language capabilities,
|
|
20
|
+
separately from its singular descriptive language and locale.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
id: str
|
|
24
|
+
gender: str = "unknown"
|
|
25
|
+
language: str = "unknown"
|
|
26
|
+
locale: str = "unknown"
|
|
27
|
+
language_label: str = "unknown"
|
|
28
|
+
languages: tuple[str, ...] = ()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class SynthesisTarget:
|
|
33
|
+
"""A neutral representation of a synthesis target.
|
|
34
|
+
|
|
35
|
+
For PyKokoro, this represents one model/distribution with its voices and qualities.
|
|
36
|
+
For PiperSynth, this represents one voice bundle.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
engine: str
|
|
40
|
+
id: str
|
|
41
|
+
display_name: str
|
|
42
|
+
languages: tuple[str, ...] = ()
|
|
43
|
+
status: str = "ready"
|
|
44
|
+
runtime_available: bool = True
|
|
45
|
+
sample_rate: int | None = None
|
|
46
|
+
voices: tuple[str, ...] = ()
|
|
47
|
+
speakers: tuple[str, ...] = ()
|
|
48
|
+
voice_details: tuple[TargetVoice, ...] = ()
|
|
49
|
+
default_voice: str | None = None
|
|
50
|
+
qualities: tuple[str, ...] = ()
|
|
51
|
+
aliases: tuple[str, ...] = ()
|
|
52
|
+
capabilities: frozenset[str] = frozenset()
|
|
53
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class CatalogRequest:
|
|
58
|
+
"""Request for target discovery."""
|
|
59
|
+
|
|
60
|
+
engine: str | None = None
|
|
61
|
+
language: str | None = None
|
|
62
|
+
offline: bool = False
|
|
63
|
+
refresh: bool = False
|
|
64
|
+
preference: str = "auto"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class CatalogResult:
|
|
69
|
+
"""Result of target discovery."""
|
|
70
|
+
|
|
71
|
+
targets: tuple[SynthesisTarget, ...] = ()
|
|
72
|
+
registry_source: str | None = None
|
|
73
|
+
cache_fallback: bool = False
|
|
74
|
+
offline: bool = False
|
|
75
|
+
refreshed: bool = False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"CatalogRequest",
|
|
80
|
+
"CatalogResult",
|
|
81
|
+
"SynthesisTarget",
|
|
82
|
+
"TargetVoice",
|
|
83
|
+
]
|