inflectsynth 0.1.0__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.
- inflectsynth/__init__.py +92 -0
- inflectsynth/__main__.py +56 -0
- inflectsynth/_onnxvoice.py +298 -0
- inflectsynth/api_contract.py +29 -0
- inflectsynth/catalog.py +71 -0
- inflectsynth/config.py +55 -0
- inflectsynth/data/voice_level_calibration.json +32 -0
- inflectsynth/discovery.py +207 -0
- inflectsynth/errors.py +67 -0
- inflectsynth/identity.py +33 -0
- inflectsynth/py.typed +0 -0
- inflectsynth/types.py +88 -0
- inflectsynth/voice.py +305 -0
- inflectsynth/voice_level.py +393 -0
- inflectsynth-0.1.0.dist-info/METADATA +171 -0
- inflectsynth-0.1.0.dist-info/RECORD +21 -0
- inflectsynth-0.1.0.dist-info/WHEEL +5 -0
- inflectsynth-0.1.0.dist-info/entry_points.txt +2 -0
- inflectsynth-0.1.0.dist-info/licenses/LICENSE +201 -0
- inflectsynth-0.1.0.dist-info/licenses/NOTICE +7 -0
- inflectsynth-0.1.0.dist-info/top_level.txt +1 -0
inflectsynth/__init__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Inflect v2 prepared-text synthesis backed by ONNXVoice and InflectG2P."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _distribution_version
|
|
5
|
+
|
|
6
|
+
from .api_contract import REQUEST_API_VERSION, request_api_contract
|
|
7
|
+
from .catalog import list_models
|
|
8
|
+
from .config import SynthesisConfig
|
|
9
|
+
from .discovery import DescribedVoice, DiscoveredModel, discover_models
|
|
10
|
+
from .errors import (
|
|
11
|
+
ArtifactIntegrityError,
|
|
12
|
+
CatalogDiscoveryError,
|
|
13
|
+
CatalogError,
|
|
14
|
+
CatalogUnavailableError,
|
|
15
|
+
EmptyTextError,
|
|
16
|
+
InflectSynthError,
|
|
17
|
+
InvalidSeedError,
|
|
18
|
+
InvalidSpeedError,
|
|
19
|
+
InvalidSynthesisConfigError,
|
|
20
|
+
InvalidVariationError,
|
|
21
|
+
InvalidVoiceError,
|
|
22
|
+
ModelInferenceError,
|
|
23
|
+
ModelNotFoundError,
|
|
24
|
+
OfflineModelError,
|
|
25
|
+
OnnxVoiceContractError,
|
|
26
|
+
ProviderUnavailableError,
|
|
27
|
+
UnsupportedModelError,
|
|
28
|
+
)
|
|
29
|
+
from .identity import runtime_identity
|
|
30
|
+
from .types import ModelInfo, SynthesisResult, VoiceInfo
|
|
31
|
+
from .voice import InflectVoice
|
|
32
|
+
from .voice_level import (
|
|
33
|
+
CalibrationDataError,
|
|
34
|
+
VoiceCalibrationCatalog,
|
|
35
|
+
VoiceCalibrationKey,
|
|
36
|
+
VoiceLevelApplication,
|
|
37
|
+
VoiceLevelCalibration,
|
|
38
|
+
VoiceLevelConfig,
|
|
39
|
+
VoiceLevelMode,
|
|
40
|
+
apply_voice_level_calibration,
|
|
41
|
+
default_voice_calibration,
|
|
42
|
+
load_voice_calibration,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
__version__ = _distribution_version("inflectsynth")
|
|
47
|
+
except PackageNotFoundError:
|
|
48
|
+
__version__ = "0+unknown"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"__version__",
|
|
52
|
+
"REQUEST_API_VERSION",
|
|
53
|
+
"request_api_contract",
|
|
54
|
+
"runtime_identity",
|
|
55
|
+
"discover_models",
|
|
56
|
+
"DescribedVoice",
|
|
57
|
+
"DiscoveredModel",
|
|
58
|
+
"InflectVoice",
|
|
59
|
+
"SynthesisConfig",
|
|
60
|
+
"SynthesisResult",
|
|
61
|
+
"VoiceInfo",
|
|
62
|
+
"VoiceLevelConfig",
|
|
63
|
+
"VoiceLevelMode",
|
|
64
|
+
"VoiceCalibrationKey",
|
|
65
|
+
"VoiceLevelCalibration",
|
|
66
|
+
"VoiceCalibrationCatalog",
|
|
67
|
+
"VoiceLevelApplication",
|
|
68
|
+
"CalibrationDataError",
|
|
69
|
+
"apply_voice_level_calibration",
|
|
70
|
+
"default_voice_calibration",
|
|
71
|
+
"load_voice_calibration",
|
|
72
|
+
"InflectSynthError",
|
|
73
|
+
"EmptyTextError",
|
|
74
|
+
"InvalidSpeedError",
|
|
75
|
+
"InvalidVariationError",
|
|
76
|
+
"InvalidSeedError",
|
|
77
|
+
"InvalidVoiceError",
|
|
78
|
+
"InvalidSynthesisConfigError",
|
|
79
|
+
"UnsupportedModelError",
|
|
80
|
+
"OnnxVoiceContractError",
|
|
81
|
+
"ModelInferenceError",
|
|
82
|
+
"CatalogDiscoveryError",
|
|
83
|
+
"CatalogUnavailableError",
|
|
84
|
+
# Transitional prototype aliases, not part of the v0.1.0 request contract.
|
|
85
|
+
"list_models",
|
|
86
|
+
"ModelInfo",
|
|
87
|
+
"CatalogError",
|
|
88
|
+
"ModelNotFoundError",
|
|
89
|
+
"ArtifactIntegrityError",
|
|
90
|
+
"OfflineModelError",
|
|
91
|
+
"ProviderUnavailableError",
|
|
92
|
+
]
|
inflectsynth/__main__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from .catalog import list_models
|
|
6
|
+
from .voice import InflectVoice
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_args() -> argparse.Namespace:
|
|
10
|
+
parser = argparse.ArgumentParser(description="Synthesize speech with Inflect v2 ONNX models")
|
|
11
|
+
parser.add_argument("text", nargs="?")
|
|
12
|
+
parser.add_argument("--model", default="nano-v2")
|
|
13
|
+
parser.add_argument("--voice", default="default")
|
|
14
|
+
parser.add_argument("--speed", type=float, default=1.0)
|
|
15
|
+
parser.add_argument("--variation", type=float, default=0.667)
|
|
16
|
+
parser.add_argument("--seed", type=int, default=0)
|
|
17
|
+
parser.add_argument("--provider", default="cpu")
|
|
18
|
+
parser.add_argument("--cache-dir")
|
|
19
|
+
parser.add_argument("--catalog-url")
|
|
20
|
+
parser.add_argument("--offline", action="store_true")
|
|
21
|
+
parser.add_argument("--force-download", action="store_true")
|
|
22
|
+
parser.add_argument("--list-models", action="store_true")
|
|
23
|
+
parser.add_argument("-o", "--output", default="inflect.wav")
|
|
24
|
+
return parser.parse_args()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
args = parse_args()
|
|
29
|
+
if args.list_models:
|
|
30
|
+
for model in list_models(catalog_url=args.catalog_url):
|
|
31
|
+
print(f"{model.id}\t{model.name}\tvoices={','.join(v.id for v in model.voices)}")
|
|
32
|
+
return 0
|
|
33
|
+
if not args.text:
|
|
34
|
+
raise SystemExit("text is required unless --list-models is used")
|
|
35
|
+
with InflectVoice.from_pretrained(
|
|
36
|
+
args.model,
|
|
37
|
+
cache_dir=args.cache_dir,
|
|
38
|
+
catalog_url=args.catalog_url,
|
|
39
|
+
offline=args.offline,
|
|
40
|
+
force_download=args.force_download,
|
|
41
|
+
providers=args.provider,
|
|
42
|
+
) as engine:
|
|
43
|
+
result = engine.synthesize(
|
|
44
|
+
args.text,
|
|
45
|
+
voice=args.voice,
|
|
46
|
+
speed=args.speed,
|
|
47
|
+
variation=args.variation,
|
|
48
|
+
seed=args.seed,
|
|
49
|
+
)
|
|
50
|
+
result.save_wav(args.output)
|
|
51
|
+
print(args.output)
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .errors import (
|
|
12
|
+
CatalogDiscoveryError,
|
|
13
|
+
CatalogUnavailableError,
|
|
14
|
+
InflectSynthError,
|
|
15
|
+
OnnxVoiceContractError,
|
|
16
|
+
UnsupportedModelError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_CATALOG_URL = (
|
|
20
|
+
"https://raw.githubusercontent.com/buchwandler/inflect-onnx-bundles/main/catalog/models.json"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ResolvedInflectModel:
|
|
26
|
+
ref: str | None
|
|
27
|
+
model_id: str
|
|
28
|
+
duration_path: Path
|
|
29
|
+
decode_path: Path
|
|
30
|
+
sample_rate: int
|
|
31
|
+
metadata: dict[str, Any]
|
|
32
|
+
installation: Any
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize_ref(value: str) -> str:
|
|
36
|
+
if not isinstance(value, str) or not value.strip():
|
|
37
|
+
raise OnnxVoiceContractError("Inflect model reference must not be empty")
|
|
38
|
+
return value if ":" in value else f"inflect:{value}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _module() -> ModuleType:
|
|
42
|
+
"""Import ONNXVoice only when an operation needs its managed runtime."""
|
|
43
|
+
try:
|
|
44
|
+
module = importlib.import_module("onnxvoice")
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
raise OnnxVoiceContractError(
|
|
47
|
+
"ONNXVoice with the Inflect adapter is required; install a compatible onnxvoice release"
|
|
48
|
+
) from exc
|
|
49
|
+
|
|
50
|
+
manager_type = getattr(module, "OnnxVoice", None)
|
|
51
|
+
systems = getattr(module, "registered_systems", None)
|
|
52
|
+
try:
|
|
53
|
+
available_systems = systems() if callable(systems) else ()
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
raise OnnxVoiceContractError("Could not inspect the ONNXVoice adapter registry") from exc
|
|
56
|
+
if manager_type is None or "inflect" not in available_systems:
|
|
57
|
+
raise OnnxVoiceContractError(
|
|
58
|
+
"Installed ONNXVoice does not expose the required Inflect adapter contract"
|
|
59
|
+
)
|
|
60
|
+
return module
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _manager(
|
|
64
|
+
*,
|
|
65
|
+
cache_dir: str | Path | None,
|
|
66
|
+
offline: bool,
|
|
67
|
+
catalog_url: str | None,
|
|
68
|
+
) -> Any:
|
|
69
|
+
module = _module()
|
|
70
|
+
source = catalog_url or os.environ.get("INFLECTSYNTH_CATALOG_URL") or DEFAULT_CATALOG_URL
|
|
71
|
+
try:
|
|
72
|
+
return module.OnnxVoice(
|
|
73
|
+
cache_dir=cache_dir,
|
|
74
|
+
catalog_sources={"inflect": source},
|
|
75
|
+
offline=offline,
|
|
76
|
+
)
|
|
77
|
+
except TypeError as exc:
|
|
78
|
+
raise OnnxVoiceContractError(
|
|
79
|
+
"Installed ONNXVoice does not support the Inflect catalog_sources manager contract"
|
|
80
|
+
) from exc
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _translate_onnxvoice_error(exc: Exception, operation: str) -> Exception:
|
|
84
|
+
"""Translate ONNXVoice's public error families to stable InflectSynth errors."""
|
|
85
|
+
try:
|
|
86
|
+
errors = importlib.import_module("onnxvoice.errors")
|
|
87
|
+
except Exception:
|
|
88
|
+
return OnnxVoiceContractError(f"ONNXVoice {operation} operation failed")
|
|
89
|
+
|
|
90
|
+
def is_error(name: str) -> bool:
|
|
91
|
+
error_type = getattr(errors, name, None)
|
|
92
|
+
return isinstance(error_type, type) and isinstance(exc, error_type)
|
|
93
|
+
|
|
94
|
+
message = f"ONNXVoice {operation} operation failed: {exc}"
|
|
95
|
+
if is_error("OfflineError"):
|
|
96
|
+
return CatalogUnavailableError(message)
|
|
97
|
+
if is_error("CatalogError"):
|
|
98
|
+
return CatalogDiscoveryError(message)
|
|
99
|
+
if is_error("AssetNotFoundError") or is_error("IntegrityError"):
|
|
100
|
+
return UnsupportedModelError(message)
|
|
101
|
+
if is_error("AssetDownloadError"):
|
|
102
|
+
return UnsupportedModelError(message)
|
|
103
|
+
if is_error("RuntimeContractError") or is_error("OptionalDependencyError"):
|
|
104
|
+
return OnnxVoiceContractError(message)
|
|
105
|
+
if is_error("UnsupportedSystemError"):
|
|
106
|
+
return UnsupportedModelError(message)
|
|
107
|
+
if operation == "catalog":
|
|
108
|
+
return CatalogUnavailableError(message)
|
|
109
|
+
if operation == "install":
|
|
110
|
+
return UnsupportedModelError(message)
|
|
111
|
+
return OnnxVoiceContractError(message)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def installation_to_model(installation: Any) -> ResolvedInflectModel:
|
|
115
|
+
"""Validate the public ONNXVoice installation shape required by InflectSynth."""
|
|
116
|
+
if getattr(installation, "system", None) != "inflect":
|
|
117
|
+
raise OnnxVoiceContractError("ONNXVoice installation system must be 'inflect'")
|
|
118
|
+
require_artifact = getattr(installation, "require_artifact", None)
|
|
119
|
+
if not callable(require_artifact):
|
|
120
|
+
raise OnnxVoiceContractError("ONNXVoice installation must support require_artifact(role)")
|
|
121
|
+
try:
|
|
122
|
+
duration = require_artifact("duration")
|
|
123
|
+
decode = require_artifact("decode")
|
|
124
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
125
|
+
raise OnnxVoiceContractError(
|
|
126
|
+
"ONNXVoice Inflect installation must contain duration and decode artifacts"
|
|
127
|
+
) from exc
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
duration_path = Path(duration.path)
|
|
131
|
+
decode_path = Path(decode.path)
|
|
132
|
+
metadata_value = installation.metadata
|
|
133
|
+
metadata = dict(metadata_value) if isinstance(metadata_value, Mapping) else {}
|
|
134
|
+
model_id = installation.id
|
|
135
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
136
|
+
raise OnnxVoiceContractError("ONNXVoice returned an invalid Inflect installation") from exc
|
|
137
|
+
if not isinstance(model_id, str) or not model_id:
|
|
138
|
+
raise OnnxVoiceContractError("ONNXVoice Inflect installation must have a model id")
|
|
139
|
+
|
|
140
|
+
sample_rate = getattr(installation, "sample_rate", None)
|
|
141
|
+
if sample_rate is None:
|
|
142
|
+
sample_rate = metadata.get("sample_rate")
|
|
143
|
+
if sample_rate is None:
|
|
144
|
+
sample_rate = 24_000
|
|
145
|
+
if isinstance(sample_rate, bool) or not isinstance(sample_rate, int) or sample_rate <= 0:
|
|
146
|
+
raise OnnxVoiceContractError("ONNXVoice Inflect sample rate must be a positive integer")
|
|
147
|
+
|
|
148
|
+
metadata.setdefault("id", model_id)
|
|
149
|
+
metadata.setdefault("sample_rate", sample_rate)
|
|
150
|
+
metadata.setdefault("default_voice", getattr(installation, "default_voice", None) or "default")
|
|
151
|
+
if "voices" not in metadata:
|
|
152
|
+
voice_details = metadata.get("voice_details")
|
|
153
|
+
if isinstance(voice_details, Mapping):
|
|
154
|
+
metadata["voices"] = {key: dict(value) for key, value in voice_details.items()}
|
|
155
|
+
|
|
156
|
+
ref_value = getattr(installation, "ref", None)
|
|
157
|
+
ref = ref_value if isinstance(ref_value, str) else normalize_ref(model_id)
|
|
158
|
+
return ResolvedInflectModel(
|
|
159
|
+
ref=ref,
|
|
160
|
+
model_id=model_id,
|
|
161
|
+
duration_path=duration_path,
|
|
162
|
+
decode_path=decode_path,
|
|
163
|
+
sample_rate=sample_rate,
|
|
164
|
+
metadata=metadata,
|
|
165
|
+
installation=installation,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def install_pretrained_model(
|
|
170
|
+
model: str,
|
|
171
|
+
*,
|
|
172
|
+
cache_dir: str | Path | None = None,
|
|
173
|
+
offline: bool = False,
|
|
174
|
+
refresh_catalog: bool = False,
|
|
175
|
+
force_download: bool = False,
|
|
176
|
+
catalog_url: str | None = None,
|
|
177
|
+
progress: Any | None = None,
|
|
178
|
+
) -> ResolvedInflectModel:
|
|
179
|
+
manager = _manager(cache_dir=cache_dir, offline=offline, catalog_url=catalog_url)
|
|
180
|
+
ref = normalize_ref(model)
|
|
181
|
+
try:
|
|
182
|
+
installation = manager.install(
|
|
183
|
+
ref,
|
|
184
|
+
refresh=refresh_catalog,
|
|
185
|
+
force=force_download,
|
|
186
|
+
progress=progress,
|
|
187
|
+
)
|
|
188
|
+
except TypeError as exc:
|
|
189
|
+
raise OnnxVoiceContractError(
|
|
190
|
+
"Installed ONNXVoice does not support the Inflect install contract"
|
|
191
|
+
) from exc
|
|
192
|
+
except InflectSynthError:
|
|
193
|
+
raise
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
raise _translate_onnxvoice_error(exc, "install") from exc
|
|
196
|
+
return installation_to_model(installation)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def open_installed_model(
|
|
200
|
+
model: ResolvedInflectModel,
|
|
201
|
+
*,
|
|
202
|
+
cache_dir: str | Path | None = None,
|
|
203
|
+
offline: bool = False,
|
|
204
|
+
catalog_url: str | None = None,
|
|
205
|
+
providers: Sequence[Any] | str | None = None,
|
|
206
|
+
provider_options: Sequence[dict[str, Any]] | dict[str, dict[str, Any]] | None = None,
|
|
207
|
+
session_options: Any | None = None,
|
|
208
|
+
) -> Any:
|
|
209
|
+
manager = _manager(cache_dir=cache_dir, offline=offline, catalog_url=catalog_url)
|
|
210
|
+
try:
|
|
211
|
+
return manager.open(
|
|
212
|
+
model.installation,
|
|
213
|
+
providers=providers,
|
|
214
|
+
provider_options=provider_options,
|
|
215
|
+
session_options=session_options,
|
|
216
|
+
)
|
|
217
|
+
except TypeError as exc:
|
|
218
|
+
raise OnnxVoiceContractError(
|
|
219
|
+
"Installed ONNXVoice does not support provider/session options for Inflect models"
|
|
220
|
+
) from exc
|
|
221
|
+
except InflectSynthError:
|
|
222
|
+
raise
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
raise _translate_onnxvoice_error(exc, "open") from exc
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def open_local_model(
|
|
228
|
+
*,
|
|
229
|
+
duration_path: str | Path,
|
|
230
|
+
decode_path: str | Path,
|
|
231
|
+
sample_rate: int = 24_000,
|
|
232
|
+
metadata: Mapping[str, Any] | None = None,
|
|
233
|
+
providers: Sequence[Any] | str | None = None,
|
|
234
|
+
provider_options: Sequence[dict[str, Any]] | dict[str, dict[str, Any]] | None = None,
|
|
235
|
+
session_options: Any | None = None,
|
|
236
|
+
) -> Any:
|
|
237
|
+
module = _module()
|
|
238
|
+
try:
|
|
239
|
+
return module.OnnxVoice.open_local(
|
|
240
|
+
system="inflect",
|
|
241
|
+
artifacts={"duration": duration_path, "decode": decode_path},
|
|
242
|
+
sample_rate=sample_rate,
|
|
243
|
+
metadata=dict(metadata or {}),
|
|
244
|
+
providers=providers,
|
|
245
|
+
provider_options=provider_options,
|
|
246
|
+
session_options=session_options,
|
|
247
|
+
)
|
|
248
|
+
except TypeError as exc:
|
|
249
|
+
raise OnnxVoiceContractError(
|
|
250
|
+
"Installed ONNXVoice does not support local Inflect split-artifact opening"
|
|
251
|
+
) from exc
|
|
252
|
+
except InflectSynthError:
|
|
253
|
+
raise
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
raise _translate_onnxvoice_error(exc, "open_local") from exc
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def catalog_items(
|
|
259
|
+
*,
|
|
260
|
+
language: str | None = None,
|
|
261
|
+
offline: bool = False,
|
|
262
|
+
refresh: bool = False,
|
|
263
|
+
cache_dir: str | Path | None = None,
|
|
264
|
+
catalog_url: str | None = None,
|
|
265
|
+
) -> tuple[Any, ...]:
|
|
266
|
+
"""Return catalog records only; never install or open model artifacts."""
|
|
267
|
+
manager = _manager(cache_dir=cache_dir, offline=offline, catalog_url=catalog_url)
|
|
268
|
+
try:
|
|
269
|
+
return tuple(manager.list("inflect", language=language, refresh=refresh))
|
|
270
|
+
except TypeError as exc:
|
|
271
|
+
raise OnnxVoiceContractError(
|
|
272
|
+
"Installed ONNXVoice does not support Inflect catalog discovery"
|
|
273
|
+
) from exc
|
|
274
|
+
except InflectSynthError:
|
|
275
|
+
raise
|
|
276
|
+
except Exception as exc:
|
|
277
|
+
raise _translate_onnxvoice_error(exc, "catalog") from exc
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def catalog_voices(
|
|
281
|
+
*,
|
|
282
|
+
language: str | None = None,
|
|
283
|
+
offline: bool = False,
|
|
284
|
+
refresh: bool = False,
|
|
285
|
+
cache_dir: str | Path | None = None,
|
|
286
|
+
catalog_url: str | None = None,
|
|
287
|
+
) -> tuple[Any, ...]:
|
|
288
|
+
manager = _manager(cache_dir=cache_dir, offline=offline, catalog_url=catalog_url)
|
|
289
|
+
try:
|
|
290
|
+
return tuple(manager.list_voices("inflect", language=language, refresh=refresh))
|
|
291
|
+
except TypeError as exc:
|
|
292
|
+
raise OnnxVoiceContractError(
|
|
293
|
+
"Installed ONNXVoice does not support Inflect voice discovery"
|
|
294
|
+
) from exc
|
|
295
|
+
except InflectSynthError:
|
|
296
|
+
raise
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
raise _translate_onnxvoice_error(exc, "catalog") from exc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Versioned, dependency-light declaration of the Readio request contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
|
|
8
|
+
REQUEST_API_VERSION = 1
|
|
9
|
+
|
|
10
|
+
_REQUEST_API_CONTRACT: Mapping[str, str | bool] = MappingProxyType(
|
|
11
|
+
{
|
|
12
|
+
"entrypoint": "InflectVoice.synthesize_prepared",
|
|
13
|
+
"supports_linguistic_tokens": False,
|
|
14
|
+
"supports_pronunciation_overrides": False,
|
|
15
|
+
"supports_whole_request_phonemes": False,
|
|
16
|
+
"supports_speakers": False,
|
|
17
|
+
"supports_word_timings": False,
|
|
18
|
+
"supports_voice_level": True,
|
|
19
|
+
"caller_owns_text_boundaries": True,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def request_api_contract() -> Mapping[str, str | bool]:
|
|
25
|
+
"""Return an immutable declaration without catalog or model access."""
|
|
26
|
+
return _REQUEST_API_CONTRACT
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["REQUEST_API_VERSION", "request_api_contract"]
|
inflectsynth/catalog.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._onnxvoice import catalog_items, catalog_voices
|
|
7
|
+
from .types import ModelInfo, VoiceInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_models(
|
|
11
|
+
*,
|
|
12
|
+
language: str | None = None,
|
|
13
|
+
offline: bool = False,
|
|
14
|
+
refresh: bool = False,
|
|
15
|
+
cache_dir: str | Path | None = None,
|
|
16
|
+
catalog_url: str | None = None,
|
|
17
|
+
) -> tuple[ModelInfo, ...]:
|
|
18
|
+
"""Compatibility discovery adapter backed exclusively by ONNXVoice."""
|
|
19
|
+
items = catalog_items(
|
|
20
|
+
language=language,
|
|
21
|
+
offline=offline,
|
|
22
|
+
refresh=refresh,
|
|
23
|
+
cache_dir=cache_dir,
|
|
24
|
+
catalog_url=catalog_url,
|
|
25
|
+
)
|
|
26
|
+
voices = catalog_voices(
|
|
27
|
+
language=language,
|
|
28
|
+
offline=offline,
|
|
29
|
+
refresh=refresh,
|
|
30
|
+
cache_dir=cache_dir,
|
|
31
|
+
catalog_url=catalog_url,
|
|
32
|
+
)
|
|
33
|
+
voices_by_model: dict[str, list[Any]] = {}
|
|
34
|
+
for record in voices:
|
|
35
|
+
voices_by_model.setdefault(record.asset_id, []).append(record)
|
|
36
|
+
|
|
37
|
+
models: list[ModelInfo] = []
|
|
38
|
+
for item in items:
|
|
39
|
+
metadata = item.metadata if isinstance(item.metadata, dict) else {}
|
|
40
|
+
upstream = metadata.get("upstream")
|
|
41
|
+
upstream = upstream if isinstance(upstream, dict) else {}
|
|
42
|
+
voice_details = metadata.get("voice_details")
|
|
43
|
+
voice_details = voice_details if isinstance(voice_details, dict) else {}
|
|
44
|
+
model_voices = tuple(
|
|
45
|
+
VoiceInfo(
|
|
46
|
+
id=record.voice_id,
|
|
47
|
+
name=str(voice_details.get(record.voice_id, {}).get("name", record.voice_id)),
|
|
48
|
+
language=record.metadata.locale,
|
|
49
|
+
gender=record.gender,
|
|
50
|
+
synthetic=bool(voice_details.get(record.voice_id, {}).get("synthetic", True)),
|
|
51
|
+
)
|
|
52
|
+
for record in voices_by_model.get(item.id, [])
|
|
53
|
+
)
|
|
54
|
+
models.append(
|
|
55
|
+
ModelInfo(
|
|
56
|
+
id=item.id,
|
|
57
|
+
name=str(metadata.get("name", item.id)),
|
|
58
|
+
aliases=tuple(item.aliases),
|
|
59
|
+
language=str(metadata.get("language", "en-US")),
|
|
60
|
+
sample_rate=int(item.sample_rate or 24_000),
|
|
61
|
+
default_voice=item.default_voice or "default",
|
|
62
|
+
voices=model_voices,
|
|
63
|
+
revision=str(
|
|
64
|
+
metadata.get("source_revision")
|
|
65
|
+
or upstream.get("source_revision")
|
|
66
|
+
or upstream.get("revision")
|
|
67
|
+
or ""
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return tuple(models)
|
inflectsynth/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from numbers import Real
|
|
6
|
+
|
|
7
|
+
from .errors import (
|
|
8
|
+
InvalidSeedError,
|
|
9
|
+
InvalidSpeedError,
|
|
10
|
+
InvalidSynthesisConfigError,
|
|
11
|
+
InvalidVariationError,
|
|
12
|
+
)
|
|
13
|
+
from .voice_level import VoiceLevelConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class SynthesisConfig:
|
|
18
|
+
speed: float = 1.0
|
|
19
|
+
variation: float = 0.667
|
|
20
|
+
seed: int = 0
|
|
21
|
+
voice_level: VoiceLevelConfig = field(default_factory=VoiceLevelConfig)
|
|
22
|
+
|
|
23
|
+
def validated(self) -> SynthesisConfig:
|
|
24
|
+
speed = _finite_control(self.speed, "speed", InvalidSpeedError)
|
|
25
|
+
if not 0.5 <= speed <= 2.0:
|
|
26
|
+
raise InvalidSpeedError("speed must be between 0.5 and 2.0")
|
|
27
|
+
variation = _finite_control(self.variation, "variation", InvalidVariationError)
|
|
28
|
+
if not 0.0 <= variation <= 1.0:
|
|
29
|
+
raise InvalidVariationError("variation must be between 0.0 and 1.0")
|
|
30
|
+
if isinstance(self.seed, bool) or not isinstance(self.seed, int) or self.seed < 0:
|
|
31
|
+
raise InvalidSeedError("seed must be a non-negative integer")
|
|
32
|
+
if not isinstance(self.voice_level, VoiceLevelConfig):
|
|
33
|
+
raise InvalidSynthesisConfigError("voice_level must be a VoiceLevelConfig")
|
|
34
|
+
return SynthesisConfig(
|
|
35
|
+
speed=speed,
|
|
36
|
+
variation=variation,
|
|
37
|
+
seed=self.seed,
|
|
38
|
+
voice_level=self.voice_level,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _finite_control(
|
|
43
|
+
value: object,
|
|
44
|
+
name: str,
|
|
45
|
+
error_type: type[InvalidSynthesisConfigError],
|
|
46
|
+
) -> float:
|
|
47
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
48
|
+
raise error_type(f"{name} must be a finite real number")
|
|
49
|
+
try:
|
|
50
|
+
number = float(value)
|
|
51
|
+
except (OverflowError, ValueError):
|
|
52
|
+
raise error_type(f"{name} must be a finite real number") from None
|
|
53
|
+
if not math.isfinite(number):
|
|
54
|
+
raise error_type(f"{name} must be finite")
|
|
55
|
+
return number
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"corpus": "inflectsynth-prepared-speech-v1",
|
|
3
|
+
"generated_with": {
|
|
4
|
+
"audiosig": "0.1.6",
|
|
5
|
+
"inflectg2p": "0.1.0",
|
|
6
|
+
"inflectsynth": "0.1.dev2+gaed518a21",
|
|
7
|
+
"onnxvoice": "0.2.5"
|
|
8
|
+
},
|
|
9
|
+
"method": "bs1770",
|
|
10
|
+
"reference_lufs": -24.0,
|
|
11
|
+
"schema": 1,
|
|
12
|
+
"voices": {
|
|
13
|
+
"inflect:micro-v2:default": {
|
|
14
|
+
"corpus_version": "inflectsynth-prepared-speech-v1",
|
|
15
|
+
"gain_db": -0.3353773192815517,
|
|
16
|
+
"mad_lu": 0.28835105130528405,
|
|
17
|
+
"measured_lufs": -23.66462268071845,
|
|
18
|
+
"method": "bs1770",
|
|
19
|
+
"reference_lufs": -24.0,
|
|
20
|
+
"samples": 9
|
|
21
|
+
},
|
|
22
|
+
"inflect:nano-v2:default": {
|
|
23
|
+
"corpus_version": "inflectsynth-prepared-speech-v1",
|
|
24
|
+
"gain_db": -0.042377569773361046,
|
|
25
|
+
"mad_lu": 0.2913390390127759,
|
|
26
|
+
"measured_lufs": -23.95762243022664,
|
|
27
|
+
"method": "bs1770",
|
|
28
|
+
"reference_lufs": -24.0,
|
|
29
|
+
"samples": 9
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|