phononsynth 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.
- phononsynth/__init__.py +54 -0
- phononsynth/__main__.py +85 -0
- phononsynth/_onnxvoice.py +280 -0
- phononsynth/_tokenizer.py +41 -0
- phononsynth/api_contract.py +23 -0
- phononsynth/config.py +69 -0
- phononsynth/discovery.py +178 -0
- phononsynth/errors.py +38 -0
- phononsynth/identity.py +20 -0
- phononsynth/py.typed +0 -0
- phononsynth/types.py +87 -0
- phononsynth/voice.py +408 -0
- phononsynth-0.1.0.dist-info/METADATA +116 -0
- phononsynth-0.1.0.dist-info/RECORD +19 -0
- phononsynth-0.1.0.dist-info/WHEEL +5 -0
- phononsynth-0.1.0.dist-info/entry_points.txt +2 -0
- phononsynth-0.1.0.dist-info/licenses/LICENSE +201 -0
- phononsynth-0.1.0.dist-info/licenses/NOTICE.md +5 -0
- phononsynth-0.1.0.dist-info/top_level.txt +1 -0
phononsynth/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Phonon synthesis frontend backed by OnnxVoice."""
|
|
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 .config import SynthesisConfig
|
|
8
|
+
from .discovery import DescribedVoice, DiscoveredModel, discover_models
|
|
9
|
+
from .errors import (
|
|
10
|
+
CatalogDiscoveryError,
|
|
11
|
+
CatalogUnavailableError,
|
|
12
|
+
EmptyTextError,
|
|
13
|
+
InvalidSynthesisConfigError,
|
|
14
|
+
ManagedModelDependencyError,
|
|
15
|
+
ModelInferenceError,
|
|
16
|
+
OnnxVoiceContractError,
|
|
17
|
+
PhononSynthError,
|
|
18
|
+
TokenizerError,
|
|
19
|
+
UnsupportedModelError,
|
|
20
|
+
)
|
|
21
|
+
from .identity import runtime_identity
|
|
22
|
+
from .types import StreamChunk, SynthesisMeasure, SynthesisResult, VoiceInfo
|
|
23
|
+
from .voice import PhononVoice
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
__version__ = _distribution_version("phononsynth")
|
|
27
|
+
except PackageNotFoundError:
|
|
28
|
+
__version__ = "0+unknown"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"__version__",
|
|
32
|
+
"REQUEST_API_VERSION",
|
|
33
|
+
"PhononVoice",
|
|
34
|
+
"SynthesisConfig",
|
|
35
|
+
"SynthesisResult",
|
|
36
|
+
"SynthesisMeasure",
|
|
37
|
+
"StreamChunk",
|
|
38
|
+
"VoiceInfo",
|
|
39
|
+
"DescribedVoice",
|
|
40
|
+
"DiscoveredModel",
|
|
41
|
+
"discover_models",
|
|
42
|
+
"runtime_identity",
|
|
43
|
+
"request_api_contract",
|
|
44
|
+
"PhononSynthError",
|
|
45
|
+
"EmptyTextError",
|
|
46
|
+
"TokenizerError",
|
|
47
|
+
"UnsupportedModelError",
|
|
48
|
+
"ManagedModelDependencyError",
|
|
49
|
+
"OnnxVoiceContractError",
|
|
50
|
+
"CatalogDiscoveryError",
|
|
51
|
+
"CatalogUnavailableError",
|
|
52
|
+
"ModelInferenceError",
|
|
53
|
+
"InvalidSynthesisConfigError",
|
|
54
|
+
]
|
phononsynth/__main__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from .discovery import discover_models
|
|
8
|
+
from .voice import PhononVoice
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
12
|
+
parser = argparse.ArgumentParser(description="Synthesize one prepared Phonon request")
|
|
13
|
+
parser.add_argument("text", nargs="?")
|
|
14
|
+
parser.add_argument("--model", default="phonon-31m-en")
|
|
15
|
+
parser.add_argument("--quality")
|
|
16
|
+
parser.add_argument("--voice")
|
|
17
|
+
parser.add_argument("--duration-delta", type=float)
|
|
18
|
+
parser.add_argument("--temperature", type=float)
|
|
19
|
+
parser.add_argument("--seed", type=int)
|
|
20
|
+
parser.add_argument("--eos-threshold", type=float)
|
|
21
|
+
parser.add_argument("--frames-after-eos", type=int)
|
|
22
|
+
parser.add_argument("--min-frames-before-eos", type=int)
|
|
23
|
+
parser.add_argument("--max-frames", type=int)
|
|
24
|
+
parser.add_argument("-o", "--output", default="phonon.wav")
|
|
25
|
+
parser.add_argument("--cache-dir")
|
|
26
|
+
parser.add_argument("--catalog-url")
|
|
27
|
+
parser.add_argument("--offline", action="store_true")
|
|
28
|
+
parser.add_argument("--list-models", action="store_true")
|
|
29
|
+
return parser.parse_args(argv)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
33
|
+
args = parse_args(argv)
|
|
34
|
+
if args.list_models:
|
|
35
|
+
models = discover_models(
|
|
36
|
+
offline=args.offline,
|
|
37
|
+
cache_dir=args.cache_dir,
|
|
38
|
+
catalog_url=args.catalog_url,
|
|
39
|
+
)
|
|
40
|
+
print(
|
|
41
|
+
json.dumps(
|
|
42
|
+
[
|
|
43
|
+
{
|
|
44
|
+
"id": model.id,
|
|
45
|
+
"release_version": model.release_version,
|
|
46
|
+
"languages": model.languages,
|
|
47
|
+
"qualities": model.quality_choices,
|
|
48
|
+
"voices": model.voice_ids,
|
|
49
|
+
"default_voice": model.default_voice,
|
|
50
|
+
"license": model.license_spdx,
|
|
51
|
+
"commercial_use": model.commercial_use,
|
|
52
|
+
}
|
|
53
|
+
for model in models
|
|
54
|
+
],
|
|
55
|
+
indent=2,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return 0
|
|
59
|
+
if not args.text:
|
|
60
|
+
raise SystemExit("text is required unless --list-models is used")
|
|
61
|
+
with PhononVoice.from_pretrained(
|
|
62
|
+
args.model,
|
|
63
|
+
quality=args.quality,
|
|
64
|
+
cache_dir=args.cache_dir,
|
|
65
|
+
catalog_url=args.catalog_url,
|
|
66
|
+
offline=args.offline,
|
|
67
|
+
) as model:
|
|
68
|
+
result = model.synthesize_prepared(
|
|
69
|
+
args.text,
|
|
70
|
+
voice=args.voice,
|
|
71
|
+
duration_delta=args.duration_delta,
|
|
72
|
+
temperature=args.temperature,
|
|
73
|
+
seed=args.seed,
|
|
74
|
+
eos_threshold=args.eos_threshold,
|
|
75
|
+
frames_after_eos=args.frames_after_eos,
|
|
76
|
+
min_frames_before_eos=args.min_frames_before_eos,
|
|
77
|
+
max_frames=args.max_frames,
|
|
78
|
+
)
|
|
79
|
+
result.save_wav(args.output)
|
|
80
|
+
print(args.output)
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import ManagedModelDependencyError, OnnxVoiceContractError, UnsupportedModelError
|
|
11
|
+
|
|
12
|
+
DEFAULT_CATALOG_URL = (
|
|
13
|
+
"https://raw.githubusercontent.com/buchwandler/phonon-onnx-bundles/main/catalog/bundles.json"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ResolvedPhononModel:
|
|
19
|
+
ref: str
|
|
20
|
+
model_id: str
|
|
21
|
+
tokenizer_path: Path
|
|
22
|
+
sample_rate: int
|
|
23
|
+
voices: tuple[str, ...]
|
|
24
|
+
default_voice: str | None
|
|
25
|
+
metadata: dict[str, Any]
|
|
26
|
+
installation: Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _module() -> Any:
|
|
30
|
+
try:
|
|
31
|
+
import onnxvoice
|
|
32
|
+
except ModuleNotFoundError as exc:
|
|
33
|
+
raise OnnxVoiceContractError("OnnxVoice is required for Phonon synthesis") from exc
|
|
34
|
+
return onnxvoice
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _require_phonon_contract(module: Any) -> None:
|
|
38
|
+
"""Fail early when OnnxVoice lacks the released Phonon manager/adapter API."""
|
|
39
|
+
manager_type = getattr(module, "OnnxVoice", None)
|
|
40
|
+
missing: list[str] = []
|
|
41
|
+
if not callable(manager_type):
|
|
42
|
+
missing.append("OnnxVoice manager")
|
|
43
|
+
else:
|
|
44
|
+
required = {
|
|
45
|
+
"OnnxVoice constructor": (manager_type, ("cache_dir", "catalog_sources", "offline")),
|
|
46
|
+
"OnnxVoice.install": (
|
|
47
|
+
getattr(manager_type, "install", None),
|
|
48
|
+
("quality", "refresh", "force", "progress"),
|
|
49
|
+
),
|
|
50
|
+
"OnnxVoice.open": (
|
|
51
|
+
getattr(manager_type, "open", None),
|
|
52
|
+
("providers", "provider_options", "session_options"),
|
|
53
|
+
),
|
|
54
|
+
"OnnxVoice.open_local": (
|
|
55
|
+
getattr(manager_type, "open_local", None),
|
|
56
|
+
("system", "artifacts", "providers", "provider_options", "session_options"),
|
|
57
|
+
),
|
|
58
|
+
}
|
|
59
|
+
for label, (callable_obj, parameters) in required.items():
|
|
60
|
+
if not callable(callable_obj):
|
|
61
|
+
missing.append(label)
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
signature = inspect.signature(callable_obj)
|
|
65
|
+
except (TypeError, ValueError):
|
|
66
|
+
missing.append(f"inspectable {label} signature")
|
|
67
|
+
continue
|
|
68
|
+
accepts_keywords = any(
|
|
69
|
+
parameter.kind is inspect.Parameter.VAR_KEYWORD
|
|
70
|
+
for parameter in signature.parameters.values()
|
|
71
|
+
)
|
|
72
|
+
absent = [name for name in parameters if name not in signature.parameters]
|
|
73
|
+
if absent and not accepts_keywords:
|
|
74
|
+
missing.append(f"{label} parameters: {', '.join(absent)}")
|
|
75
|
+
try:
|
|
76
|
+
from onnxvoice.systems import get_adapter
|
|
77
|
+
|
|
78
|
+
get_adapter("phonon")
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
missing.append(f"registered Phonon adapter ({exc})")
|
|
81
|
+
if missing:
|
|
82
|
+
details = "; ".join(missing)
|
|
83
|
+
raise OnnxVoiceContractError(
|
|
84
|
+
"Installed OnnxVoice does not provide the required Phonon contract "
|
|
85
|
+
f"({details}). Install a compatible release: onnxvoice>=0.2.15,<0.3."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _optional_dependency_error(exc: Exception) -> ManagedModelDependencyError | None:
|
|
90
|
+
try:
|
|
91
|
+
from onnxvoice.errors import OptionalDependencyError
|
|
92
|
+
except ImportError:
|
|
93
|
+
return None
|
|
94
|
+
if not isinstance(exc, OptionalDependencyError):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
detail = str(exc)
|
|
98
|
+
normalized = detail.casefold()
|
|
99
|
+
if "huggingface" in normalized or "hugging face" in normalized:
|
|
100
|
+
hint = "Install 'phononsynth[huggingface]' only when using Hugging Face downloads."
|
|
101
|
+
elif any(
|
|
102
|
+
token in normalized for token in ("onnxruntime", "onnx runtime", "execution provider")
|
|
103
|
+
):
|
|
104
|
+
hint = "For CPU inference, install 'phononsynth[cpu]'."
|
|
105
|
+
else:
|
|
106
|
+
hint = ""
|
|
107
|
+
suffix = f" {hint}" if hint else ""
|
|
108
|
+
return ManagedModelDependencyError(
|
|
109
|
+
f"OnnxVoice reports a missing optional dependency: {detail}.{suffix}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def normalize_ref(value: str) -> str:
|
|
114
|
+
value = str(value).strip()
|
|
115
|
+
if not value:
|
|
116
|
+
raise ValueError("model reference cannot be empty")
|
|
117
|
+
return value if ":" in value else f"phonon:{value}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _manager(*, cache_dir: str | Path | None, offline: bool, catalog_url: str | None) -> Any:
|
|
121
|
+
module = _module()
|
|
122
|
+
_require_phonon_contract(module)
|
|
123
|
+
source = catalog_url or os.getenv("PHONONSYNTH_CATALOG_URL") or DEFAULT_CATALOG_URL
|
|
124
|
+
try:
|
|
125
|
+
return module.OnnxVoice(
|
|
126
|
+
cache_dir=cache_dir,
|
|
127
|
+
catalog_sources={"phonon": source},
|
|
128
|
+
offline=offline,
|
|
129
|
+
)
|
|
130
|
+
except TypeError as exc:
|
|
131
|
+
raise OnnxVoiceContractError(
|
|
132
|
+
"Installed OnnxVoice does not expose the expected manager constructor"
|
|
133
|
+
) from exc
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def installation_to_model(installation: Any) -> ResolvedPhononModel:
|
|
137
|
+
if str(getattr(installation, "system", "")) != "phonon":
|
|
138
|
+
raise UnsupportedModelError("expected an OnnxVoice Phonon installation")
|
|
139
|
+
try:
|
|
140
|
+
tokenizer_path = Path(installation.artifact("tokenizer").path)
|
|
141
|
+
except (KeyError, AttributeError) as exc:
|
|
142
|
+
raise UnsupportedModelError("Phonon installation must contain tokenizer.json") from exc
|
|
143
|
+
metadata = dict(getattr(installation, "metadata", {}) or {})
|
|
144
|
+
sample_rate = int(
|
|
145
|
+
getattr(installation, "sample_rate", None)
|
|
146
|
+
or (metadata.get("runtime") or {}).get("sample_rate")
|
|
147
|
+
or 24000
|
|
148
|
+
)
|
|
149
|
+
voices = tuple(str(value) for value in (getattr(installation, "voices", ()) or ()))
|
|
150
|
+
default_voice = getattr(installation, "default_voice", None)
|
|
151
|
+
return ResolvedPhononModel(
|
|
152
|
+
ref=str(getattr(installation, "ref", f"phonon:{getattr(installation, 'id', 'unknown')}")),
|
|
153
|
+
model_id=str(getattr(installation, "id", "unknown")),
|
|
154
|
+
tokenizer_path=tokenizer_path,
|
|
155
|
+
sample_rate=sample_rate,
|
|
156
|
+
voices=voices,
|
|
157
|
+
default_voice=default_voice if isinstance(default_voice, str) else None,
|
|
158
|
+
metadata=metadata,
|
|
159
|
+
installation=installation,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def install_pretrained_model(
|
|
164
|
+
ref: str,
|
|
165
|
+
*,
|
|
166
|
+
quality: str | None = None,
|
|
167
|
+
cache_dir: str | Path | None = None,
|
|
168
|
+
offline: bool = False,
|
|
169
|
+
refresh_catalog: bool = False,
|
|
170
|
+
force_download: bool = False,
|
|
171
|
+
catalog_url: str | None = None,
|
|
172
|
+
progress: Any | None = None,
|
|
173
|
+
) -> ResolvedPhononModel:
|
|
174
|
+
manager = _manager(cache_dir=cache_dir, offline=offline, catalog_url=catalog_url)
|
|
175
|
+
try:
|
|
176
|
+
installation = manager.install(
|
|
177
|
+
normalize_ref(ref),
|
|
178
|
+
quality=quality,
|
|
179
|
+
refresh=refresh_catalog,
|
|
180
|
+
force=force_download,
|
|
181
|
+
progress=progress,
|
|
182
|
+
)
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
dependency_error = _optional_dependency_error(exc)
|
|
185
|
+
if dependency_error is not None:
|
|
186
|
+
raise dependency_error from exc
|
|
187
|
+
raise
|
|
188
|
+
return installation_to_model(installation)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def open_installed_model(
|
|
192
|
+
resolved: ResolvedPhononModel,
|
|
193
|
+
*,
|
|
194
|
+
providers: Sequence[Any] | str | None = None,
|
|
195
|
+
provider_options: Sequence[dict[str, Any]] | dict[str, dict[str, Any]] | None = None,
|
|
196
|
+
session_options: Any | None = None,
|
|
197
|
+
cache_dir: str | Path | None = None,
|
|
198
|
+
offline: bool = False,
|
|
199
|
+
catalog_url: str | None = None,
|
|
200
|
+
) -> Any:
|
|
201
|
+
manager = _manager(cache_dir=cache_dir, offline=offline, catalog_url=catalog_url)
|
|
202
|
+
return manager.open(
|
|
203
|
+
resolved.installation,
|
|
204
|
+
providers=providers,
|
|
205
|
+
provider_options=provider_options,
|
|
206
|
+
session_options=session_options,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def local_artifacts(bundle_dir: str | Path) -> tuple[dict[str, Path], dict[str, Any]]:
|
|
211
|
+
root = Path(bundle_dir)
|
|
212
|
+
config_path = root / "config.json"
|
|
213
|
+
tokenizer_path = root / "tokenizer.json"
|
|
214
|
+
if not config_path.is_file() or not tokenizer_path.is_file():
|
|
215
|
+
raise UnsupportedModelError("local Phonon bundle requires config.json and tokenizer.json")
|
|
216
|
+
import json
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
220
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
221
|
+
raise UnsupportedModelError("local Phonon config.json is invalid") from exc
|
|
222
|
+
if not isinstance(config, Mapping):
|
|
223
|
+
raise UnsupportedModelError("local Phonon config.json must contain an object")
|
|
224
|
+
|
|
225
|
+
artifacts: dict[str, Path] = {"config": config_path, "tokenizer": tokenizer_path}
|
|
226
|
+
for component in ("text_conditioner", "flow_lm_main", "flow_lm_flow", "mimi_decoder"):
|
|
227
|
+
path = root / "onnx" / f"{component}.onnx"
|
|
228
|
+
if not path.is_file():
|
|
229
|
+
raise UnsupportedModelError(f"local Phonon bundle is missing {path.relative_to(root)}")
|
|
230
|
+
artifacts[f"model:{component}"] = path
|
|
231
|
+
|
|
232
|
+
runtime_config = config.get("runtime", {})
|
|
233
|
+
if runtime_config is not None and not isinstance(runtime_config, Mapping):
|
|
234
|
+
raise UnsupportedModelError("local Phonon config runtime must be an object")
|
|
235
|
+
if isinstance(runtime_config, Mapping) and "sample_rate" in runtime_config:
|
|
236
|
+
sample_rate = runtime_config["sample_rate"]
|
|
237
|
+
if isinstance(sample_rate, bool) or not isinstance(sample_rate, int) or sample_rate <= 0:
|
|
238
|
+
raise UnsupportedModelError(
|
|
239
|
+
"local Phonon runtime.sample_rate must be a positive integer"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
raw_voices = config.get("voices")
|
|
243
|
+
if isinstance(raw_voices, Mapping):
|
|
244
|
+
declarations = list(raw_voices.items())
|
|
245
|
+
if any(not isinstance(entry, Mapping) for _, entry in declarations):
|
|
246
|
+
raise UnsupportedModelError("local Phonon mapping voices must contain objects")
|
|
247
|
+
elif isinstance(raw_voices, Sequence) and not isinstance(raw_voices, (str, bytes)):
|
|
248
|
+
declarations = [
|
|
249
|
+
(entry.get("name"), entry) for entry in raw_voices if isinstance(entry, Mapping)
|
|
250
|
+
]
|
|
251
|
+
if len(declarations) != len(raw_voices):
|
|
252
|
+
raise UnsupportedModelError("local Phonon config contains an invalid voice")
|
|
253
|
+
else:
|
|
254
|
+
raise UnsupportedModelError("local Phonon config must declare voices")
|
|
255
|
+
|
|
256
|
+
voices: list[str] = []
|
|
257
|
+
for name, _entry in declarations:
|
|
258
|
+
if (
|
|
259
|
+
not isinstance(name, str)
|
|
260
|
+
or not name
|
|
261
|
+
or name != name.strip()
|
|
262
|
+
or name in {".", ".."}
|
|
263
|
+
or "/" in name
|
|
264
|
+
or "\\" in name
|
|
265
|
+
or "\x00" in name
|
|
266
|
+
or Path(name).is_absolute()
|
|
267
|
+
):
|
|
268
|
+
raise UnsupportedModelError("local Phonon config contains an unsafe voice name")
|
|
269
|
+
if name in voices:
|
|
270
|
+
raise UnsupportedModelError(f"local Phonon config declares duplicate voice {name!r}")
|
|
271
|
+
prompt = root / "voices" / f"{name}.bin"
|
|
272
|
+
if not prompt.is_file():
|
|
273
|
+
raise UnsupportedModelError(f"local Phonon bundle is missing voices/{name}.bin")
|
|
274
|
+
artifacts[f"voice_prompt:{name}"] = prompt
|
|
275
|
+
voices.append(name)
|
|
276
|
+
|
|
277
|
+
default_voice = config.get("default_voice")
|
|
278
|
+
if default_voice is not None and default_voice not in voices:
|
|
279
|
+
raise UnsupportedModelError("local Phonon default_voice must be declared in voices")
|
|
280
|
+
return artifacts, {**dict(config), "_voices": tuple(voices)}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .errors import TokenizerError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_tokenizer(path: str | Path) -> Any:
|
|
11
|
+
try:
|
|
12
|
+
from tokenizers import Tokenizer
|
|
13
|
+
except ModuleNotFoundError as exc:
|
|
14
|
+
raise TokenizerError(
|
|
15
|
+
"The 'tokenizers' package is required to read Phonon tokenizer.json"
|
|
16
|
+
) from exc
|
|
17
|
+
try:
|
|
18
|
+
return Tokenizer.from_file(str(Path(path)))
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
raise TokenizerError(f"Could not load Phonon tokenizer from {path}: {exc}") from exc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def encode_text(tokenizer: Any, text: str) -> tuple[int, ...]:
|
|
24
|
+
try:
|
|
25
|
+
encoded = tokenizer.encode(text, add_special_tokens=True)
|
|
26
|
+
except TypeError:
|
|
27
|
+
encoded = tokenizer.encode(text)
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
raise TokenizerError(f"Phonon tokenization failed: {exc}") from exc
|
|
30
|
+
|
|
31
|
+
values: Any = getattr(encoded, "ids", encoded)
|
|
32
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
|
|
33
|
+
raise TokenizerError("Phonon tokenizer returned an unsupported token sequence")
|
|
34
|
+
result: list[int] = []
|
|
35
|
+
for value in values:
|
|
36
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
37
|
+
raise TokenizerError("Phonon tokenizer returned invalid token IDs")
|
|
38
|
+
result.append(value)
|
|
39
|
+
if not result:
|
|
40
|
+
raise TokenizerError("Phonon tokenizer returned no token IDs")
|
|
41
|
+
return tuple(result)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
REQUEST_API_VERSION = "1"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def request_api_contract() -> dict[str, object]:
|
|
7
|
+
return {
|
|
8
|
+
"version": REQUEST_API_VERSION,
|
|
9
|
+
"engine": "phonon",
|
|
10
|
+
"entrypoint": "PhononVoice.synthesize_prepared",
|
|
11
|
+
"stream_entrypoint": "PhononVoice.stream_prepared",
|
|
12
|
+
"measure_entrypoint": "PhononVoice.measure_prepared",
|
|
13
|
+
"input": "prepared_text",
|
|
14
|
+
"measurement_unit": "model_tokens",
|
|
15
|
+
"max_input_tokens": None,
|
|
16
|
+
"features": {
|
|
17
|
+
"named_voices": True,
|
|
18
|
+
"streaming": True,
|
|
19
|
+
"voice_cloning": False,
|
|
20
|
+
"word_timings": False,
|
|
21
|
+
"automatic_chunking": False,
|
|
22
|
+
},
|
|
23
|
+
}
|
phononsynth/config.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .errors import InvalidSynthesisConfigError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _finite_optional(
|
|
10
|
+
value: float | None, name: str, *, minimum: float | None = None
|
|
11
|
+
) -> float | None:
|
|
12
|
+
if value is None:
|
|
13
|
+
return None
|
|
14
|
+
number = float(value)
|
|
15
|
+
if not math.isfinite(number) or (minimum is not None and number < minimum):
|
|
16
|
+
raise InvalidSynthesisConfigError(f"{name} is outside its valid range")
|
|
17
|
+
return number
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _int_optional(value: int | None, name: str, *, minimum: int) -> int | None:
|
|
21
|
+
if value is None:
|
|
22
|
+
return None
|
|
23
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
24
|
+
raise InvalidSynthesisConfigError(f"{name} must be an integer >= {minimum}")
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class SynthesisConfig:
|
|
30
|
+
"""Optional Phonon generation overrides.
|
|
31
|
+
|
|
32
|
+
``None`` uses the selected bundle default, or a Phonon-compatible fallback when absent.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
duration_delta: float | None = None
|
|
36
|
+
temperature: float | None = None
|
|
37
|
+
seed: int | None = None
|
|
38
|
+
eos_threshold: float | None = None
|
|
39
|
+
frames_after_eos: int | None = None
|
|
40
|
+
min_frames_before_eos: int | None = None
|
|
41
|
+
max_frames: int | None = None
|
|
42
|
+
|
|
43
|
+
def validated(self) -> SynthesisConfig:
|
|
44
|
+
seed = self.seed
|
|
45
|
+
if seed is not None and (isinstance(seed, bool) or not isinstance(seed, int)):
|
|
46
|
+
raise InvalidSynthesisConfigError("seed must be an integer or None")
|
|
47
|
+
return SynthesisConfig(
|
|
48
|
+
duration_delta=_finite_optional(self.duration_delta, "duration_delta"),
|
|
49
|
+
temperature=_finite_optional(self.temperature, "temperature", minimum=0.0),
|
|
50
|
+
seed=seed,
|
|
51
|
+
eos_threshold=_finite_optional(self.eos_threshold, "eos_threshold"),
|
|
52
|
+
frames_after_eos=_int_optional(self.frames_after_eos, "frames_after_eos", minimum=0),
|
|
53
|
+
min_frames_before_eos=_int_optional(
|
|
54
|
+
self.min_frames_before_eos, "min_frames_before_eos", minimum=0
|
|
55
|
+
),
|
|
56
|
+
max_frames=_int_optional(self.max_frames, "max_frames", minimum=1),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def runtime_kwargs(self) -> dict[str, object | None]:
|
|
60
|
+
value = self.validated()
|
|
61
|
+
return {
|
|
62
|
+
"duration_delta": value.duration_delta,
|
|
63
|
+
"temperature": value.temperature,
|
|
64
|
+
"seed": value.seed,
|
|
65
|
+
"eos_threshold": value.eos_threshold,
|
|
66
|
+
"frames_after_eos": value.frames_after_eos,
|
|
67
|
+
"min_frames_before_eos": value.min_frames_before_eos,
|
|
68
|
+
"max_frames": value.max_frames,
|
|
69
|
+
}
|