bolo-torch 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.
- bolo_torch/__init__.py +20 -0
- bolo_torch/__main__.py +8 -0
- bolo_torch/cli.py +163 -0
- bolo_torch/engine.py +82 -0
- bolo_torch/models/__init__.py +30 -0
- bolo_torch/models/dicow.py +413 -0
- bolo_torch/models/f5.py +267 -0
- bolo_torch/models/indic_speak.py +106 -0
- bolo_torch/models/orpheus.py +260 -0
- bolo_torch/models/parler.py +202 -0
- bolo_torch/models/separation.py +177 -0
- bolo_torch/py.typed +0 -0
- bolo_torch/runtime.py +194 -0
- bolo_torch/server.py +66 -0
- bolo_torch/text.py +68 -0
- bolo_torch-0.1.0.dist-info/METADATA +380 -0
- bolo_torch-0.1.0.dist-info/RECORD +20 -0
- bolo_torch-0.1.0.dist-info/WHEEL +4 -0
- bolo_torch-0.1.0.dist-info/entry_points.txt +5 -0
- bolo_torch-0.1.0.dist-info/licenses/LICENSE +202 -0
bolo_torch/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""bolo-torch: the PyTorch engine plugin of the bolo speech server.
|
|
2
|
+
|
|
3
|
+
bolo never imports PyTorch. It starts this package as a separate process (``python -m bolo_torch``) and talks to
|
|
4
|
+
it over the plugin protocol (bolo's docs/plugins.md). This module is what bolo's entry-point discovery imports in
|
|
5
|
+
the *server* process, so it must stay free of heavy imports: no torch here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plugin() -> list[str]:
|
|
16
|
+
"""The command line bolo runs to start the plugin (entry point ``bolo.engines: torch``)."""
|
|
17
|
+
return [sys.executable, "-m", "bolo_torch"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["__version__", "plugin"]
|
bolo_torch/__main__.py
ADDED
bolo_torch/cli.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""``bolo-torch``: serve the plugin (what bolo runs), check the environment, or separate a recording directly.
|
|
2
|
+
|
|
3
|
+
bolo-torch serve # the plugin protocol on stdin/stdout (bolo starts this)
|
|
4
|
+
bolo-torch doctor # torch build, devices, which model families are installed
|
|
5
|
+
bolo-torch separate mix.wav -o out/ # speech separation without bolo (SepFormer / PixIT)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import importlib.util
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from bolo_torch import __version__
|
|
21
|
+
|
|
22
|
+
#: separation models for ``bolo-torch separate`` (the same files as bolo's catalog entries)
|
|
23
|
+
SEPARATION_MODELS: dict[str, dict[str, Any]] = {
|
|
24
|
+
"sepformer-whamr": {
|
|
25
|
+
"kind": "sepformer", "repo": "speechbrain/sepformer-whamr",
|
|
26
|
+
"revision": "9950fd8055b38679d2af5bef2366b5dba600ec66",
|
|
27
|
+
"allow": ["hyperparams.yaml", "masknet.ckpt", "encoder.ckpt", "decoder.ckpt"],
|
|
28
|
+
"about": "SpeechBrain SepFormer, WHAMR! (noisy, reverberant), 8 kHz, 2 speakers. Apache-2.0; WSJ0-derived data",
|
|
29
|
+
},
|
|
30
|
+
"sepformer-whamr16k": {
|
|
31
|
+
"kind": "sepformer", "repo": "speechbrain/sepformer-whamr16k",
|
|
32
|
+
"revision": "21a5b500c6f52fddc387c5d9e5fb13ffd6f039c5",
|
|
33
|
+
"allow": ["hyperparams.yaml", "masknet.ckpt", "encoder.ckpt", "decoder.ckpt"],
|
|
34
|
+
"about": "SpeechBrain SepFormer, WHAMR! at 16 kHz, 2 speakers. Apache-2.0; WSJ0-derived data",
|
|
35
|
+
},
|
|
36
|
+
"pixit-ami": {
|
|
37
|
+
"kind": "pixit", "repo": "pyannote/speech-separation-ami-1.0",
|
|
38
|
+
"revision": "9486b106945ae0cc0784041a08bfcdba5edadfb9", "allow": None,
|
|
39
|
+
"about": "pyannote PixIT, joint diarization and separation, AMI meetings, 16 kHz. MIT, gated",
|
|
40
|
+
},
|
|
41
|
+
} # fmt: skip
|
|
42
|
+
|
|
43
|
+
FAMILIES = {
|
|
44
|
+
"parler": ["parler_tts", "transformers"],
|
|
45
|
+
"f5": ["transformers", "vocos", "x_transformers", "torchdiffeq", "pydub", "jieba", "pypinyin"],
|
|
46
|
+
"orpheus": ["transformers", "snac"],
|
|
47
|
+
"sepformer": ["speechbrain", "torchaudio"],
|
|
48
|
+
"pyannote": ["pyannote.audio"],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _installed(module: str) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
return importlib.util.find_spec(module) is not None
|
|
55
|
+
except (ImportError, ValueError):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def doctor() -> dict[str, Any]:
|
|
60
|
+
import torch
|
|
61
|
+
|
|
62
|
+
from bolo_torch.runtime import hf_token, pick_device
|
|
63
|
+
|
|
64
|
+
mps = getattr(torch.backends, "mps", None)
|
|
65
|
+
return {
|
|
66
|
+
"bolo_torch": __version__,
|
|
67
|
+
"python": sys.version.split()[0],
|
|
68
|
+
"torch": torch.__version__,
|
|
69
|
+
"cuda_build": torch.version.cuda,
|
|
70
|
+
"cuda_available": torch.cuda.is_available(),
|
|
71
|
+
"cuda_devices": [torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())],
|
|
72
|
+
"mps_available": bool(mps is not None and mps.is_available()),
|
|
73
|
+
"auto_device": pick_device("auto"),
|
|
74
|
+
"threads": torch.get_num_threads(),
|
|
75
|
+
"hf_token": bool(hf_token()),
|
|
76
|
+
"families": {name: all(_installed(m) for m in mods) for name, mods in FAMILIES.items()},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def separate(args: argparse.Namespace) -> dict[str, Any]:
|
|
81
|
+
import numpy as np
|
|
82
|
+
import soundfile as sf
|
|
83
|
+
from bolo.engines.base import LoadOptions, SeparationModel
|
|
84
|
+
|
|
85
|
+
from bolo_torch.engine import TorchEngine
|
|
86
|
+
from bolo_torch.models.separation import SR, resample
|
|
87
|
+
from bolo_torch.runtime import hub_snapshot
|
|
88
|
+
|
|
89
|
+
entry = SEPARATION_MODELS[args.model]
|
|
90
|
+
t0 = time.perf_counter()
|
|
91
|
+
root = hub_snapshot(entry["repo"], entry["revision"], entry["allow"])
|
|
92
|
+
t_download = time.perf_counter() - t0
|
|
93
|
+
t0 = time.perf_counter()
|
|
94
|
+
model = TorchEngine().load(
|
|
95
|
+
entry["kind"],
|
|
96
|
+
"separation",
|
|
97
|
+
root,
|
|
98
|
+
{"model_id": args.model},
|
|
99
|
+
LoadOptions(threads=args.threads, device=args.device),
|
|
100
|
+
)
|
|
101
|
+
t_load = time.perf_counter() - t0
|
|
102
|
+
assert isinstance(model, SeparationModel)
|
|
103
|
+
samples, rate = sf.read(args.input, dtype="float32", always_2d=True)
|
|
104
|
+
mono = resample(samples.mean(axis=1).astype(np.float32), int(rate), SR)
|
|
105
|
+
t0 = time.perf_counter()
|
|
106
|
+
tracks = model.separate(mono)
|
|
107
|
+
t_run = time.perf_counter() - t0
|
|
108
|
+
out = Path(args.output)
|
|
109
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
files = []
|
|
111
|
+
for i, track in enumerate(tracks):
|
|
112
|
+
path = out / f"SPEAKER_{i:02d}.wav"
|
|
113
|
+
sf.write(str(path), track, SR)
|
|
114
|
+
files.append(str(path))
|
|
115
|
+
duration = len(mono) / SR
|
|
116
|
+
turns = getattr(model, "last_turns", [])
|
|
117
|
+
return {
|
|
118
|
+
"model": args.model, "device": args.device, "threads": args.threads, "audio_s": round(duration, 2),
|
|
119
|
+
"download_s": round(t_download, 2), "load_s": round(t_load, 2), "separate_s": round(t_run, 2),
|
|
120
|
+
"rtf": round(t_run / duration, 3) if duration else None, "tracks": files,
|
|
121
|
+
"turns": [[round(s, 2), round(e, 2), k] for s, e, k in turns],
|
|
122
|
+
} # fmt: skip
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main(argv: list[str] | None = None) -> int:
|
|
126
|
+
parser = argparse.ArgumentParser(
|
|
127
|
+
prog="bolo-torch", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument("--version", action="version", version=f"bolo-torch {__version__}")
|
|
130
|
+
sub = parser.add_subparsers(dest="command")
|
|
131
|
+
sub.add_parser("serve", help="serve the plugin protocol on stdin/stdout")
|
|
132
|
+
sub.add_parser("doctor", help="report torch, devices and installed model families")
|
|
133
|
+
sep = sub.add_parser("separate", help="split a recording into one track per speaker")
|
|
134
|
+
sep.add_argument("input")
|
|
135
|
+
sep.add_argument("-o", "--output", default="separated")
|
|
136
|
+
sep.add_argument("-m", "--model", default="sepformer-whamr16k", choices=sorted(SEPARATION_MODELS))
|
|
137
|
+
sep.add_argument("--device", default=os.environ.get("BOLO_TORCH_DEVICE", "auto"))
|
|
138
|
+
sep.add_argument("--threads", type=int, default=0)
|
|
139
|
+
args = parser.parse_args(argv)
|
|
140
|
+
if args.command in (None, "serve"):
|
|
141
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="%(name)s: %(message)s")
|
|
142
|
+
from bolo_torch.engine import TorchEngine
|
|
143
|
+
from bolo_torch.server import serve
|
|
144
|
+
|
|
145
|
+
return serve(TorchEngine())
|
|
146
|
+
if args.command == "doctor":
|
|
147
|
+
print(json.dumps(doctor(), indent=2))
|
|
148
|
+
return 0
|
|
149
|
+
if args.command == "separate":
|
|
150
|
+
from bolo_torch.runtime import PluginError
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
print(json.dumps(separate(args), indent=2, ensure_ascii=False))
|
|
154
|
+
except PluginError as exc:
|
|
155
|
+
print(f"error ({exc.code}): {exc}", file=sys.stderr)
|
|
156
|
+
return 2
|
|
157
|
+
return 0
|
|
158
|
+
parser.print_help()
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
raise SystemExit(main())
|
bolo_torch/engine.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The plugin's engine: model kinds, and the device/dtype/thread choices made at load time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from bolo.engines.base import Engine, EngineStatus, LoadOptions, Model
|
|
13
|
+
|
|
14
|
+
from bolo_torch import __version__
|
|
15
|
+
from bolo_torch.runtime import InvalidRequest, LoadContext, pick_device, pick_dtype, set_threads
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("bolo_torch")
|
|
18
|
+
|
|
19
|
+
Loader = Callable[[LoadContext], Model]
|
|
20
|
+
|
|
21
|
+
#: kind -> (module, function); modules are imported only when a model of that kind is loaded
|
|
22
|
+
LOADERS: dict[str, tuple[str, str]] = {
|
|
23
|
+
"parler-tts": ("bolo_torch.models.parler", "load"),
|
|
24
|
+
"f5-tts": ("bolo_torch.models.f5", "load"),
|
|
25
|
+
"orpheus-snac": ("bolo_torch.models.orpheus", "load"),
|
|
26
|
+
"indic-speak": ("bolo_torch.models.indic_speak", "load"),
|
|
27
|
+
"sepformer": ("bolo_torch.models.separation", "load_sepformer"),
|
|
28
|
+
"pixit": ("bolo_torch.models.separation", "load_pixit"),
|
|
29
|
+
"dicow": ("bolo_torch.models.dicow", "load"),
|
|
30
|
+
}
|
|
31
|
+
#: kinds the catalog lists for this plugin that are not implemented yet
|
|
32
|
+
NOT_IMPLEMENTED = {
|
|
33
|
+
"sravaani": "SraVaani (TorchScript STT)",
|
|
34
|
+
"indicconformer-official": "IndicConformer official",
|
|
35
|
+
"diarizen": "DiariZen diarization (the plugin protocol has no diarization request yet)",
|
|
36
|
+
"spark-tts": "Spark-TTS (no packaged inference code)",
|
|
37
|
+
"maskgct": "MaskGCT (no packaged inference code)",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TorchEngine(Engine):
|
|
42
|
+
id = "torch"
|
|
43
|
+
name = "torch"
|
|
44
|
+
license = "Apache-2.0"
|
|
45
|
+
url = "https://github.com/cb-Sigma/bolo/tree/main/plugins/bolo-torch"
|
|
46
|
+
|
|
47
|
+
def __init__(self, loaders: dict[str, Loader | tuple[str, str]] | None = None) -> None:
|
|
48
|
+
self.loaders: dict[str, Loader | tuple[str, str]] = dict(LOADERS if loaders is None else loaders)
|
|
49
|
+
self.kinds = tuple(self.loaders)
|
|
50
|
+
|
|
51
|
+
def status(self) -> EngineStatus:
|
|
52
|
+
return EngineStatus(self.id, True, __version__, None)
|
|
53
|
+
|
|
54
|
+
def _loader(self, kind: str) -> Loader:
|
|
55
|
+
entry = self.loaders.get(kind)
|
|
56
|
+
if entry is None:
|
|
57
|
+
if kind in NOT_IMPLEMENTED:
|
|
58
|
+
raise InvalidRequest(f"bolo-torch {__version__} cannot load {NOT_IMPLEMENTED[kind]} models yet")
|
|
59
|
+
raise InvalidRequest(
|
|
60
|
+
f"bolo-torch {__version__} has no model kind {kind!r} (it has {', '.join(self.kinds)})"
|
|
61
|
+
)
|
|
62
|
+
if isinstance(entry, tuple):
|
|
63
|
+
module, func = entry
|
|
64
|
+
return getattr(importlib.import_module(module), func) # type: ignore[no-any-return]
|
|
65
|
+
return entry
|
|
66
|
+
|
|
67
|
+
def load(self, kind: str, task: str, path: Path, spec: dict[str, Any], opts: LoadOptions) -> Model:
|
|
68
|
+
loader = self._loader(kind)
|
|
69
|
+
device = pick_device(opts.device, spec)
|
|
70
|
+
dtype = pick_dtype(device, spec)
|
|
71
|
+
threads = set_threads(opts.threads)
|
|
72
|
+
ctx = LoadContext(
|
|
73
|
+
kind, task, path, dict(spec), device=device, dtype=dtype, threads=threads,
|
|
74
|
+
model_id=str(spec.get("model_id", path.name)),
|
|
75
|
+
) # fmt: skip
|
|
76
|
+
t0 = time.perf_counter()
|
|
77
|
+
model = loader(ctx)
|
|
78
|
+
log.info(
|
|
79
|
+
"loaded %s (%s) on %s, %s, %d threads in %.1f s",
|
|
80
|
+
ctx.model_id, kind, device, str(dtype).replace("torch.", ""), threads, time.perf_counter() - t0,
|
|
81
|
+
) # fmt: skip
|
|
82
|
+
return model
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Model families served by the plugin. Each module imports its libraries only when a model is loaded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from bolo.engines.base import FloatArray, Voice
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def voices_from_options(options: dict[str, Any]) -> list[dict[str, Any]]:
|
|
13
|
+
raw = options.get("voices") or []
|
|
14
|
+
return [dict(v) for v in raw if isinstance(v, dict) and v.get("id")]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_voices(entries: Iterable[dict[str, Any]]) -> tuple[Voice, ...]:
|
|
18
|
+
return tuple(
|
|
19
|
+
Voice(str(v["id"]), str(v.get("name", v["id"])), str(v.get("language", "")), str(v.get("gender", "")), i)
|
|
20
|
+
for i, v in enumerate(entries)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def peak_safe(audio: Any) -> FloatArray:
|
|
25
|
+
"""float32 mono, scaled down only if it would clip."""
|
|
26
|
+
out = np.asarray(audio, dtype=np.float32).reshape(-1)
|
|
27
|
+
peak = float(np.max(np.abs(out))) if out.size else 0.0
|
|
28
|
+
if peak > 1.0:
|
|
29
|
+
out = out / peak
|
|
30
|
+
return out
|