pipersynth 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.
- pipersynth/__init__.py +120 -0
- pipersynth/__main__.py +216 -0
- pipersynth/_onnxvoice.py +317 -0
- pipersynth/_version.py +24 -0
- pipersynth/asset_manager.py +222 -0
- pipersynth/asset_progress.py +33 -0
- pipersynth/assets.py +143 -0
- pipersynth/audio.py +84 -0
- pipersynth/audio_job.py +283 -0
- pipersynth/composition.py +114 -0
- pipersynth/config.py +120 -0
- pipersynth/convenience.py +153 -0
- pipersynth/diagnostics.py +51 -0
- pipersynth/errors.py +97 -0
- pipersynth/pipeline.py +616 -0
- pipersynth/plan_adapter.py +297 -0
- pipersynth/planning.py +86 -0
- pipersynth/preparation.py +93 -0
- pipersynth/protocols.py +30 -0
- pipersynth/py.typed +0 -0
- pipersynth/session.py +265 -0
- pipersynth/types.py +269 -0
- pipersynth/voice.py +479 -0
- pipersynth-0.1.1.dist-info/METADATA +185 -0
- pipersynth-0.1.1.dist-info/RECORD +29 -0
- pipersynth-0.1.1.dist-info/WHEEL +5 -0
- pipersynth-0.1.1.dist-info/entry_points.txt +2 -0
- pipersynth-0.1.1.dist-info/licenses/LICENSE +201 -0
- pipersynth-0.1.1.dist-info/top_level.txt +1 -0
pipersynth/__init__.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Independent synthesis runtime for Piper-compatible ONNX voice models."""
|
|
2
|
+
|
|
3
|
+
from piperg2p import VoiceConfig
|
|
4
|
+
from utterplan import (
|
|
5
|
+
LinguisticsConfig,
|
|
6
|
+
PauseConfig,
|
|
7
|
+
PlannerConfig,
|
|
8
|
+
SSMDConfig,
|
|
9
|
+
UtterancePlan,
|
|
10
|
+
UtterancePlanner,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from ._version import __version__, __version_tuple__
|
|
15
|
+
except ImportError:
|
|
16
|
+
__version__ = "0.1.1"
|
|
17
|
+
__version_tuple__ = (0, 1, 1)
|
|
18
|
+
from .asset_manager import CacheInfo, VoiceAssetManager, list_cached_voices, list_voices
|
|
19
|
+
from .asset_progress import AssetProgressCallback, AssetProgressEvent, ConsoleAssetProgress
|
|
20
|
+
from .assets import VoiceBundle, VoiceMetadata, load_catalog_voice
|
|
21
|
+
from .config import GenerationConfig, PipelineConfig
|
|
22
|
+
from .convenience import synthesize, synthesize_to_wav
|
|
23
|
+
from .diagnostics import RuntimeDiagnostics, TimingDiagnostics
|
|
24
|
+
from .errors import (
|
|
25
|
+
AssetCacheError,
|
|
26
|
+
AssetDownloadError,
|
|
27
|
+
AssetError,
|
|
28
|
+
CatalogUnavailableError,
|
|
29
|
+
ConfigFileNotFoundError,
|
|
30
|
+
InvalidSpeakerError,
|
|
31
|
+
InvalidSynthesisConfigError,
|
|
32
|
+
ModelFileNotFoundError,
|
|
33
|
+
ModelInferenceError,
|
|
34
|
+
ModelLoadError,
|
|
35
|
+
OfflineAssetError,
|
|
36
|
+
OptionalDependencyError,
|
|
37
|
+
PiperSynthError,
|
|
38
|
+
PlanRenderingError,
|
|
39
|
+
PlanSampleRateMismatchError,
|
|
40
|
+
SessionCreationError,
|
|
41
|
+
SynthesisError,
|
|
42
|
+
TextPreparationError,
|
|
43
|
+
UnsupportedModelError,
|
|
44
|
+
UnsupportedPlanDirectiveError,
|
|
45
|
+
UnsupportedPlanLanguageError,
|
|
46
|
+
VoiceBindingError,
|
|
47
|
+
VoiceClosedError,
|
|
48
|
+
VoiceNotFoundError,
|
|
49
|
+
)
|
|
50
|
+
from .pipeline import PiperPipeline, PreparedAudioUnits, build_pipeline
|
|
51
|
+
from .preparation import IdentityTextPreparer, PreparedTextResult, SpokenformTextPreparer
|
|
52
|
+
from .session import ProviderConfig, available_providers
|
|
53
|
+
from .types import AudioChunk, AudioResult, AudioUnitDescriptor, AudioUnitResult, SynthesisConfig
|
|
54
|
+
from .voice import PiperVoice
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"AudioChunk",
|
|
58
|
+
"AudioResult",
|
|
59
|
+
"AudioUnitDescriptor",
|
|
60
|
+
"AudioUnitResult",
|
|
61
|
+
"AssetCacheError",
|
|
62
|
+
"AssetDownloadError",
|
|
63
|
+
"AssetError",
|
|
64
|
+
"AssetProgressCallback",
|
|
65
|
+
"AssetProgressEvent",
|
|
66
|
+
"GenerationConfig",
|
|
67
|
+
"LinguisticsConfig",
|
|
68
|
+
"PauseConfig",
|
|
69
|
+
"PlannerConfig",
|
|
70
|
+
"SSMDConfig",
|
|
71
|
+
"UtterancePlan",
|
|
72
|
+
"UtterancePlanner",
|
|
73
|
+
"CacheInfo",
|
|
74
|
+
"VoiceAssetManager",
|
|
75
|
+
"CatalogUnavailableError",
|
|
76
|
+
"ConsoleAssetProgress",
|
|
77
|
+
"IdentityTextPreparer",
|
|
78
|
+
"PiperPipeline",
|
|
79
|
+
"PiperSynthError",
|
|
80
|
+
"PlanRenderingError",
|
|
81
|
+
"PlanSampleRateMismatchError",
|
|
82
|
+
"ConfigFileNotFoundError",
|
|
83
|
+
"InvalidSpeakerError",
|
|
84
|
+
"InvalidSynthesisConfigError",
|
|
85
|
+
"ModelFileNotFoundError",
|
|
86
|
+
"ModelInferenceError",
|
|
87
|
+
"ModelLoadError",
|
|
88
|
+
"OptionalDependencyError",
|
|
89
|
+
"OfflineAssetError",
|
|
90
|
+
"PiperVoice",
|
|
91
|
+
"PipelineConfig",
|
|
92
|
+
"PreparedAudioUnits",
|
|
93
|
+
"PreparedTextResult",
|
|
94
|
+
"ProviderConfig",
|
|
95
|
+
"RuntimeDiagnostics",
|
|
96
|
+
"SessionCreationError",
|
|
97
|
+
"SynthesisError",
|
|
98
|
+
"TextPreparationError",
|
|
99
|
+
"TimingDiagnostics",
|
|
100
|
+
"UnsupportedModelError",
|
|
101
|
+
"UnsupportedPlanDirectiveError",
|
|
102
|
+
"UnsupportedPlanLanguageError",
|
|
103
|
+
"VoiceBindingError",
|
|
104
|
+
"VoiceClosedError",
|
|
105
|
+
"SpokenformTextPreparer",
|
|
106
|
+
"SynthesisConfig",
|
|
107
|
+
"VoiceBundle",
|
|
108
|
+
"VoiceConfig",
|
|
109
|
+
"VoiceMetadata",
|
|
110
|
+
"VoiceNotFoundError",
|
|
111
|
+
"available_providers",
|
|
112
|
+
"build_pipeline",
|
|
113
|
+
"list_cached_voices",
|
|
114
|
+
"load_catalog_voice",
|
|
115
|
+
"list_voices",
|
|
116
|
+
"synthesize",
|
|
117
|
+
"synthesize_to_wav",
|
|
118
|
+
"__version__",
|
|
119
|
+
"__version_tuple__",
|
|
120
|
+
]
|
pipersynth/__main__.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
from .asset_manager import VoiceAssetManager
|
|
7
|
+
from .config import GenerationConfig, PipelineConfig
|
|
8
|
+
from .convenience import synthesize_to_wav
|
|
9
|
+
from .pipeline import PiperPipeline
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _speaker(value: str) -> int | str:
|
|
13
|
+
try:
|
|
14
|
+
return int(value)
|
|
15
|
+
except ValueError:
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _legacy_parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description="Synthesize speech with a Piper-compatible ONNX voice without the Piper runtime"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("model", help="Path to .onnx voice model")
|
|
24
|
+
parser.add_argument("text", help="Text to synthesize")
|
|
25
|
+
parser.add_argument("-o", "--output", default="output.wav")
|
|
26
|
+
parser.add_argument("--config", default=None, help="Path to .onnx.json config")
|
|
27
|
+
parser.add_argument("--speaker", type=_speaker, default=None)
|
|
28
|
+
parser.add_argument("--length-scale", type=float, default=None)
|
|
29
|
+
parser.add_argument("--noise-scale", type=float, default=None)
|
|
30
|
+
parser.add_argument("--noise-w-scale", type=float, default=None)
|
|
31
|
+
parser.add_argument("--noise-w", type=float, default=None, help="Deprecated noise width alias")
|
|
32
|
+
parser.add_argument("--sentence-silence", type=float, default=0.0)
|
|
33
|
+
parser.add_argument("--volume", type=float, default=1.0)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--provider", action="append", default=None, help="ONNX provider; repeat for priority"
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--language", default=None, help="Language for optional written-text preparation"
|
|
39
|
+
)
|
|
40
|
+
preparation = parser.add_mutually_exclusive_group()
|
|
41
|
+
preparation.add_argument(
|
|
42
|
+
"--prepare-text", action="store_true", help="Use spokenform preparation"
|
|
43
|
+
)
|
|
44
|
+
preparation.add_argument(
|
|
45
|
+
"--no-prepare-text", action="store_true", help="Use identity preparation"
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument("--no-normalize", action="store_true")
|
|
48
|
+
return parser
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _add_managed_options(parser: argparse.ArgumentParser) -> None:
|
|
52
|
+
parser.add_argument("--cache-dir", default=None)
|
|
53
|
+
parser.add_argument("--offline", action="store_true")
|
|
54
|
+
parser.add_argument("--refresh-catalog", action="store_true")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
58
|
+
parser = argparse.ArgumentParser(description="Synthesize speech with PiperSynth catalog voices")
|
|
59
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
60
|
+
|
|
61
|
+
voices = commands.add_parser("voices")
|
|
62
|
+
voice_commands = voices.add_subparsers(dest="voice_command", required=True)
|
|
63
|
+
listing = voice_commands.add_parser("list")
|
|
64
|
+
_add_managed_options(listing)
|
|
65
|
+
listing.add_argument("--language")
|
|
66
|
+
listing.add_argument("--quality")
|
|
67
|
+
show = voice_commands.add_parser("show")
|
|
68
|
+
_add_managed_options(show)
|
|
69
|
+
show.add_argument("voice")
|
|
70
|
+
for name in ("download", "path", "remove", "license"):
|
|
71
|
+
command = voice_commands.add_parser(name)
|
|
72
|
+
_add_managed_options(command)
|
|
73
|
+
command.add_argument("voice")
|
|
74
|
+
download = voice_commands.choices["download"]
|
|
75
|
+
download.add_argument("--force-download", action="store_true")
|
|
76
|
+
|
|
77
|
+
speak = commands.add_parser("speak")
|
|
78
|
+
_add_managed_options(speak)
|
|
79
|
+
speak.add_argument("--voice", required=True)
|
|
80
|
+
speak.add_argument("text")
|
|
81
|
+
speak.add_argument("-o", "--output", default="output.wav")
|
|
82
|
+
speak.add_argument("--speaker", type=_speaker, default=None)
|
|
83
|
+
speak.add_argument("--length-scale", type=float, default=None)
|
|
84
|
+
speak.add_argument("--noise-scale", type=float, default=None)
|
|
85
|
+
speak.add_argument("--noise-w-scale", type=float, default=None)
|
|
86
|
+
speak.add_argument("--sentence-silence", type=float, default=0.0)
|
|
87
|
+
speak.add_argument("--volume", type=float, default=1.0)
|
|
88
|
+
speak.add_argument("--provider", action="append", default=None)
|
|
89
|
+
speak.add_argument("--prepare-text", action="store_true")
|
|
90
|
+
speak.add_argument("--language", default=None)
|
|
91
|
+
speak.add_argument("--no-normalize", action="store_true")
|
|
92
|
+
speak.add_argument("--force-download", action="store_true")
|
|
93
|
+
|
|
94
|
+
cache = commands.add_parser("cache")
|
|
95
|
+
cache_commands = cache.add_subparsers(dest="cache_command", required=True)
|
|
96
|
+
for name in ("info", "list", "prune", "clear"):
|
|
97
|
+
command = cache_commands.add_parser(name)
|
|
98
|
+
command.add_argument("--cache-dir", default=None)
|
|
99
|
+
cache_commands.choices["clear"].add_argument("--voices", action="store_true")
|
|
100
|
+
return parser
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _run_legacy(args: argparse.Namespace) -> None:
|
|
104
|
+
noise_w_scale = args.noise_w_scale
|
|
105
|
+
if args.noise_w is not None:
|
|
106
|
+
if noise_w_scale is not None:
|
|
107
|
+
raise ValueError("--noise-w and --noise-w-scale cannot both be set")
|
|
108
|
+
noise_w_scale = args.noise_w
|
|
109
|
+
generation = GenerationConfig(
|
|
110
|
+
speaker=args.speaker,
|
|
111
|
+
length_scale=args.length_scale,
|
|
112
|
+
noise_scale=args.noise_scale,
|
|
113
|
+
noise_w_scale=noise_w_scale,
|
|
114
|
+
normalize_audio=not args.no_normalize,
|
|
115
|
+
volume=args.volume,
|
|
116
|
+
sentence_silence=args.sentence_silence,
|
|
117
|
+
)
|
|
118
|
+
config = PipelineConfig(
|
|
119
|
+
model_path=args.model,
|
|
120
|
+
config_path=args.config,
|
|
121
|
+
generation=generation,
|
|
122
|
+
providers=tuple(args.provider) if args.provider is not None else None,
|
|
123
|
+
text_preparation="spokenform" if args.prepare_text else "identity",
|
|
124
|
+
language=args.language,
|
|
125
|
+
)
|
|
126
|
+
with PiperPipeline(config) as pipeline:
|
|
127
|
+
pipeline.run(args.text).save_wav(args.output)
|
|
128
|
+
print(args.output)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _run_managed(args: argparse.Namespace) -> None:
|
|
132
|
+
offline = True if args.offline else None
|
|
133
|
+
manager = VoiceAssetManager(args.cache_dir, offline=offline)
|
|
134
|
+
if args.command == "voices":
|
|
135
|
+
if args.voice_command == "list":
|
|
136
|
+
for voice in manager.list_voices(
|
|
137
|
+
language=args.language, quality=args.quality, refresh=args.refresh_catalog
|
|
138
|
+
):
|
|
139
|
+
print(f"{voice.id}\t{voice.name}")
|
|
140
|
+
elif args.voice_command == "show":
|
|
141
|
+
voice = manager.get_voice_metadata(args.voice, refresh=args.refresh_catalog)
|
|
142
|
+
print(f"id: {voice.id}")
|
|
143
|
+
print(f"name: {voice.name}")
|
|
144
|
+
print(f"language: {voice.language_code}")
|
|
145
|
+
print(f"quality: {voice.quality}")
|
|
146
|
+
print(f"speakers: {voice.num_speakers}")
|
|
147
|
+
print(f"aliases: {', '.join(voice.aliases)}")
|
|
148
|
+
elif args.voice_command == "download":
|
|
149
|
+
bundle = manager.resolve_voice(
|
|
150
|
+
args.voice,
|
|
151
|
+
refresh_catalog=args.refresh_catalog,
|
|
152
|
+
force_download=args.force_download,
|
|
153
|
+
)
|
|
154
|
+
print(bundle.directory)
|
|
155
|
+
elif args.voice_command == "path":
|
|
156
|
+
print(manager.voice_cache_path(args.voice))
|
|
157
|
+
elif args.voice_command == "remove":
|
|
158
|
+
manager.remove_voice(args.voice)
|
|
159
|
+
elif args.voice_command == "license":
|
|
160
|
+
print(
|
|
161
|
+
manager.resolve_voice(
|
|
162
|
+
args.voice, refresh_catalog=args.refresh_catalog
|
|
163
|
+
).model_card_text
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
if args.command == "speak":
|
|
167
|
+
output = synthesize_to_wav(
|
|
168
|
+
args.text,
|
|
169
|
+
args.output,
|
|
170
|
+
voice=args.voice,
|
|
171
|
+
speaker=args.speaker,
|
|
172
|
+
length_scale=args.length_scale,
|
|
173
|
+
noise_scale=args.noise_scale,
|
|
174
|
+
noise_w_scale=args.noise_w_scale,
|
|
175
|
+
sentence_silence=args.sentence_silence,
|
|
176
|
+
volume=args.volume,
|
|
177
|
+
normalize_audio=not args.no_normalize,
|
|
178
|
+
providers=args.provider,
|
|
179
|
+
cache_dir=args.cache_dir,
|
|
180
|
+
offline=offline,
|
|
181
|
+
refresh_catalog=args.refresh_catalog,
|
|
182
|
+
force_download=args.force_download,
|
|
183
|
+
text_preparation="spokenform" if args.prepare_text else "identity",
|
|
184
|
+
language=args.language,
|
|
185
|
+
)
|
|
186
|
+
print(output)
|
|
187
|
+
return
|
|
188
|
+
if args.cache_command == "info":
|
|
189
|
+
info = manager.cache_info()
|
|
190
|
+
print(f"directory: {info.directory}")
|
|
191
|
+
print(f"catalog: {info.catalog_path}")
|
|
192
|
+
print(f"cached voices: {', '.join(info.cached_voices)}")
|
|
193
|
+
elif args.cache_command == "list":
|
|
194
|
+
for bundle in manager.cached_voices():
|
|
195
|
+
print(bundle.directory)
|
|
196
|
+
elif args.cache_command == "prune":
|
|
197
|
+
for path in manager.prune():
|
|
198
|
+
print(path)
|
|
199
|
+
elif args.cache_command == "clear":
|
|
200
|
+
manager.clear(voices=args.voices)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
204
|
+
arguments = list(argv) if argv is not None else None
|
|
205
|
+
if arguments is None:
|
|
206
|
+
import sys
|
|
207
|
+
|
|
208
|
+
arguments = sys.argv[1:]
|
|
209
|
+
if not arguments or arguments[0] not in {"voices", "speak", "cache"}:
|
|
210
|
+
_run_legacy(_legacy_parser().parse_args(arguments))
|
|
211
|
+
else:
|
|
212
|
+
_run_managed(_build_parser().parse_args(arguments))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
pipersynth/_onnxvoice.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from .asset_progress import AssetProgressEvent, AssetProgressPhase
|
|
9
|
+
from .errors import (
|
|
10
|
+
AssetCacheError,
|
|
11
|
+
AssetDownloadError,
|
|
12
|
+
AssetError,
|
|
13
|
+
CatalogUnavailableError,
|
|
14
|
+
ModelInferenceError,
|
|
15
|
+
OptionalDependencyError,
|
|
16
|
+
SessionCreationError,
|
|
17
|
+
UnsupportedModelError,
|
|
18
|
+
VoiceNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class ResolvedPiperVoice:
|
|
24
|
+
"""PiperSynth's read-only view of an OnnxVoice installation."""
|
|
25
|
+
|
|
26
|
+
ref: str | None
|
|
27
|
+
voice_id: str | None
|
|
28
|
+
model_path: Path
|
|
29
|
+
config_path: Path
|
|
30
|
+
model_card_path: Path | None
|
|
31
|
+
language_code: str | None
|
|
32
|
+
source_revision: str | None
|
|
33
|
+
metadata: Mapping[str, Any]
|
|
34
|
+
installation: Any | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _onnxvoice() -> Any:
|
|
38
|
+
try:
|
|
39
|
+
import onnxvoice
|
|
40
|
+
except ModuleNotFoundError as exc:
|
|
41
|
+
raise OptionalDependencyError(
|
|
42
|
+
"OnnxVoice is required for Piper model loading. Install pipersynth[cpu] or "
|
|
43
|
+
"pipersynth[gpu]."
|
|
44
|
+
) from exc
|
|
45
|
+
return onnxvoice
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_piper_ref(ref: str) -> str:
|
|
49
|
+
if not isinstance(ref, str) or not ref.strip():
|
|
50
|
+
raise VoiceNotFoundError("Piper voice reference must not be empty")
|
|
51
|
+
value = ref.strip()
|
|
52
|
+
if ":" not in value:
|
|
53
|
+
return f"piper:{value}"
|
|
54
|
+
system, item_id = value.split(":", 1)
|
|
55
|
+
if system.casefold() != "piper" or not item_id:
|
|
56
|
+
raise VoiceNotFoundError(f"Not a Piper voice reference: {ref!r}")
|
|
57
|
+
return f"piper:{item_id}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_provider_request(
|
|
61
|
+
providers: Sequence[Any] | str | None,
|
|
62
|
+
provider_options: Mapping[str, Any] | None,
|
|
63
|
+
) -> tuple[str | Sequence[str] | None, Any | None]:
|
|
64
|
+
"""Translate PiperSynth provider compatibility values to OnnxVoice values."""
|
|
65
|
+
|
|
66
|
+
if providers is None:
|
|
67
|
+
return "CPUExecutionProvider", provider_options
|
|
68
|
+
if isinstance(providers, str):
|
|
69
|
+
return providers, provider_options
|
|
70
|
+
|
|
71
|
+
names: list[str] = []
|
|
72
|
+
options: list[dict[str, Any]] = []
|
|
73
|
+
has_explicit_options = False
|
|
74
|
+
for provider in providers:
|
|
75
|
+
if hasattr(provider, "name"):
|
|
76
|
+
name = str(provider.name)
|
|
77
|
+
value = dict(getattr(provider, "options", {}) or {})
|
|
78
|
+
has_explicit_options = has_explicit_options or bool(value)
|
|
79
|
+
elif isinstance(provider, tuple):
|
|
80
|
+
name, raw_options = provider
|
|
81
|
+
value = dict(raw_options or {})
|
|
82
|
+
has_explicit_options = True
|
|
83
|
+
else:
|
|
84
|
+
name = str(provider)
|
|
85
|
+
value = dict(provider_options or {})
|
|
86
|
+
names.append(name)
|
|
87
|
+
options.append(value)
|
|
88
|
+
if not names:
|
|
89
|
+
raise ValueError("providers must not be empty")
|
|
90
|
+
return names, options if has_explicit_options else provider_options
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def adapt_progress(
|
|
94
|
+
callback: Callable[[AssetProgressEvent], None] | None,
|
|
95
|
+
) -> Callable[[Any], None] | None:
|
|
96
|
+
if callback is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def emit(event: Any) -> None:
|
|
100
|
+
phase = str(getattr(event, "phase", "asset"))
|
|
101
|
+
phase_map = {
|
|
102
|
+
"catalog_started": "catalog-refresh",
|
|
103
|
+
"catalog_cached": "catalog-load",
|
|
104
|
+
"catalog_completed": "catalog-load",
|
|
105
|
+
"install_started": "voice-resolve",
|
|
106
|
+
"install_completed": "download-complete",
|
|
107
|
+
"download_started": "download-start",
|
|
108
|
+
"download_progress": "download-start",
|
|
109
|
+
"download_completed": "download-complete",
|
|
110
|
+
"artifact_cached": "cache-hit",
|
|
111
|
+
}
|
|
112
|
+
raw_ref = getattr(event, "ref", None)
|
|
113
|
+
voice_id = (
|
|
114
|
+
raw_ref.split(":", 1)[1] if isinstance(raw_ref, str) and ":" in raw_ref else raw_ref
|
|
115
|
+
)
|
|
116
|
+
callback(
|
|
117
|
+
AssetProgressEvent(
|
|
118
|
+
cast(AssetProgressPhase, phase_map.get(phase, "voice-resolve")),
|
|
119
|
+
voice_id=voice_id,
|
|
120
|
+
message=getattr(event, "message", None),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return emit
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _map_error(exc: Exception, *, operation: str) -> Exception:
|
|
128
|
+
name = type(exc).__name__
|
|
129
|
+
if name in {"OptionalDependencyError", "ImportError"}:
|
|
130
|
+
return OptionalDependencyError(str(exc))
|
|
131
|
+
if name in {"AssetNotFoundError", "NotInstalledError"}:
|
|
132
|
+
return VoiceNotFoundError(str(exc))
|
|
133
|
+
if name in {"OfflineError"}:
|
|
134
|
+
from .errors import OfflineAssetError
|
|
135
|
+
|
|
136
|
+
return OfflineAssetError(str(exc))
|
|
137
|
+
if name in {"IntegrityError", "ManifestError", "UnsafePathError", "LockError"}:
|
|
138
|
+
return AssetCacheError(str(exc))
|
|
139
|
+
if name in {"CatalogError"}:
|
|
140
|
+
return CatalogUnavailableError(str(exc))
|
|
141
|
+
if name in {"RuntimeContractError", "CapabilityError", "UnsupportedSystemError"}:
|
|
142
|
+
return UnsupportedModelError(str(exc))
|
|
143
|
+
if operation == "infer":
|
|
144
|
+
return ModelInferenceError("ONNX model inference failed")
|
|
145
|
+
if operation == "open":
|
|
146
|
+
return SessionCreationError("Could not open Piper ONNX runtime")
|
|
147
|
+
return AssetDownloadError(str(exc)) if operation == "install" else AssetError(str(exc))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _call(operation: str, function: Callable[[], Any]) -> Any:
|
|
151
|
+
try:
|
|
152
|
+
return function()
|
|
153
|
+
except (FileNotFoundError, ValueError, TypeError):
|
|
154
|
+
raise
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
mapped = _map_error(exc, operation=operation)
|
|
157
|
+
raise mapped from exc
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def open_local_voice(
|
|
161
|
+
model_path: str | Path,
|
|
162
|
+
config_path: str | Path,
|
|
163
|
+
*,
|
|
164
|
+
providers: Sequence[Any] | str | None = None,
|
|
165
|
+
provider_options: Mapping[str, Any] | None = None,
|
|
166
|
+
session_options: Any | None = None,
|
|
167
|
+
) -> Any:
|
|
168
|
+
requested, options = normalize_provider_request(providers, provider_options)
|
|
169
|
+
module = _onnxvoice()
|
|
170
|
+
return _call(
|
|
171
|
+
"open",
|
|
172
|
+
lambda: module.open_local(
|
|
173
|
+
system="piper",
|
|
174
|
+
model=model_path,
|
|
175
|
+
config=config_path,
|
|
176
|
+
providers=requested,
|
|
177
|
+
provider_options=options,
|
|
178
|
+
session_options=session_options,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def install_pretrained_voice(
|
|
184
|
+
ref: str,
|
|
185
|
+
*,
|
|
186
|
+
cache_dir: str | Path | None = None,
|
|
187
|
+
offline: bool | None = None,
|
|
188
|
+
refresh_catalog: bool = False,
|
|
189
|
+
force_download: bool = False,
|
|
190
|
+
progress: Callable[[AssetProgressEvent], None] | None = None,
|
|
191
|
+
) -> ResolvedPiperVoice:
|
|
192
|
+
normalized = normalize_piper_ref(ref)
|
|
193
|
+
module = _onnxvoice()
|
|
194
|
+
manager = module.OnnxVoice(cache_dir=cache_dir, offline=bool(offline))
|
|
195
|
+
installation = _call(
|
|
196
|
+
"install",
|
|
197
|
+
lambda: manager.install(
|
|
198
|
+
normalized,
|
|
199
|
+
refresh=refresh_catalog,
|
|
200
|
+
force=force_download,
|
|
201
|
+
progress=adapt_progress(progress),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
return installation_to_voice_info(installation, ref=normalized)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def open_installed_voice(
|
|
208
|
+
resolved: ResolvedPiperVoice,
|
|
209
|
+
*,
|
|
210
|
+
providers: Sequence[Any] | str | None = None,
|
|
211
|
+
provider_options: Mapping[str, Any] | None = None,
|
|
212
|
+
session_options: Any | None = None,
|
|
213
|
+
) -> Any:
|
|
214
|
+
requested, options = normalize_provider_request(providers, provider_options)
|
|
215
|
+
module = _onnxvoice()
|
|
216
|
+
manager = module.OnnxVoice()
|
|
217
|
+
return _call(
|
|
218
|
+
"open",
|
|
219
|
+
lambda: manager.open(
|
|
220
|
+
resolved.installation,
|
|
221
|
+
providers=requested,
|
|
222
|
+
provider_options=options,
|
|
223
|
+
session_options=session_options,
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def installation_to_voice_info(installation: Any, *, ref: str | None = None) -> ResolvedPiperVoice:
|
|
229
|
+
try:
|
|
230
|
+
model = installation.artifact("model").path
|
|
231
|
+
config = installation.artifact("config").path
|
|
232
|
+
except (KeyError, AttributeError) as exc:
|
|
233
|
+
raise UnsupportedModelError(
|
|
234
|
+
"Piper installation must contain model and config artifacts"
|
|
235
|
+
) from exc
|
|
236
|
+
try:
|
|
237
|
+
model_card = installation.artifact("model_card").path
|
|
238
|
+
except KeyError:
|
|
239
|
+
model_card = None
|
|
240
|
+
raw_metadata = dict(getattr(installation, "metadata", {}) or {})
|
|
241
|
+
language = raw_metadata.get("language") or {}
|
|
242
|
+
language_code = language.get("code") if isinstance(language, Mapping) else None
|
|
243
|
+
return ResolvedPiperVoice(
|
|
244
|
+
ref=ref or getattr(installation, "ref", None),
|
|
245
|
+
voice_id=getattr(installation, "id", None),
|
|
246
|
+
model_path=Path(model),
|
|
247
|
+
config_path=Path(config),
|
|
248
|
+
model_card_path=Path(model_card) if model_card is not None else None,
|
|
249
|
+
language_code=str(language_code) if language_code is not None else None,
|
|
250
|
+
source_revision=(
|
|
251
|
+
str(raw_metadata["source_revision"])
|
|
252
|
+
if raw_metadata.get("source_revision") is not None
|
|
253
|
+
else None
|
|
254
|
+
),
|
|
255
|
+
metadata=raw_metadata,
|
|
256
|
+
installation=installation,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def runtime_diagnostics(runtime: Any) -> dict[str, Any]:
|
|
261
|
+
installation = getattr(runtime, "installation", None)
|
|
262
|
+
model_path = None
|
|
263
|
+
config_path = None
|
|
264
|
+
if installation is not None:
|
|
265
|
+
try:
|
|
266
|
+
model_path = str(installation.artifact("model").path)
|
|
267
|
+
except (KeyError, AttributeError):
|
|
268
|
+
pass
|
|
269
|
+
try:
|
|
270
|
+
config_path = str(installation.artifact("config").path)
|
|
271
|
+
except (KeyError, AttributeError):
|
|
272
|
+
pass
|
|
273
|
+
session = getattr(runtime, "session", None)
|
|
274
|
+
return {
|
|
275
|
+
"model_path": model_path,
|
|
276
|
+
"config_path": config_path,
|
|
277
|
+
"providers_requested": tuple(getattr(session, "provider_request", ()) or ()),
|
|
278
|
+
"providers_active": tuple(getattr(session, "resolved_providers", ()) or ()),
|
|
279
|
+
"model_inputs": tuple(getattr(session, "input_names", ()) or ()),
|
|
280
|
+
"model_outputs": tuple(getattr(session, "output_names", ()) or ()),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def summarize_array(value: Any) -> dict[str, Any]:
|
|
285
|
+
import numpy as np
|
|
286
|
+
|
|
287
|
+
array = np.asarray(value)
|
|
288
|
+
return {"shape": [int(size) for size in array.shape], "dtype": str(array.dtype)}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def summarize_inference(result: Any) -> tuple[dict[str, Any] | None, dict[str, Any]]:
|
|
292
|
+
timing = getattr(result, "timings", None)
|
|
293
|
+
timings = summarize_array(timing) if timing is not None else None
|
|
294
|
+
outputs = {
|
|
295
|
+
str(name): summarize_array(value)
|
|
296
|
+
for name, value in dict(getattr(result, "outputs", {}) or {}).items()
|
|
297
|
+
}
|
|
298
|
+
return timings, outputs
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def available_providers() -> tuple[str, ...]:
|
|
302
|
+
return tuple(_call("open", lambda: _onnxvoice().available_providers()))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
__all__ = [
|
|
306
|
+
"ResolvedPiperVoice",
|
|
307
|
+
"adapt_progress",
|
|
308
|
+
"available_providers",
|
|
309
|
+
"installation_to_voice_info",
|
|
310
|
+
"install_pretrained_voice",
|
|
311
|
+
"normalize_piper_ref",
|
|
312
|
+
"normalize_provider_request",
|
|
313
|
+
"open_installed_voice",
|
|
314
|
+
"open_local_voice",
|
|
315
|
+
"runtime_diagnostics",
|
|
316
|
+
"summarize_inference",
|
|
317
|
+
]
|
pipersynth/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g5927e1531'
|