Spaces:
Running on Zero
Running on Zero
File size: 9,120 Bytes
5d9be35 127a1e8 5d9be35 127a1e8 5d9be35 127a1e8 5d9be35 127a1e8 5d9be35 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | """The plugin seam of every server: ``--plugins NAME...`` imports ``zonos3_<NAME>`` and mounts the FastAPI router of
its ``plugin()`` over the server's ``Host``.
A plugin serves only ``/v2/<name>/*`` and ``/<name>``, ``/<name>/*``. It reaches the served model through ``Host``: the
server's facts (``HostFacts``), LACI takes with a duration target (``LaciTakeRequest``), the codec encode of a
reference clip and the server's shared ASR / FuseAlign workers. The sgl and mlx servers implement ``Host`` in-process;
the ggml server runs plugins in its Python host process (zonos3/workers/pyhost.py) and forwards their paths there.
"""
from __future__ import annotations
import importlib
import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Mapping, Protocol, Sequence
from fastapi import APIRouter, FastAPI
from zonos3.server.schemas import GenerateRequest
from zonos3.tts.steering import RateExtension
if TYPE_CHECKING:
import numpy as np
from zonos3.tts.edit_align import AsrWorker, FuseAlignWorker
HOST_API = 1 # bumped on any breaking change of Host / HostFacts / LaciTakeRequest / Plugin
CATCH_ALL = "/{path:path}" # the studio's catch-all route, kept last
_NAME = re.compile(r"[a-z][a-z0-9_]*")
@dataclass(frozen=True)
class HostFacts:
"""What a plugin may know of the served checkpoint and server."""
model_name: str # checkpoint dir name
rate_edges: tuple[float, ...] # speaking-rate condition-row edges (UTF-8 B/s); () without that row
rate_buckets: int # that row's bucket count (0 without)
laci_languages: frozenset[str] # TTS languages with LACI heads (--laci-heads)
speaker_modes: tuple[str, ...] # subset of ("reference", "embedding")
raw_controls: frozenset[str] # steering ids in RAW_SPACE (move the speaker embedding)
phoneme_tokens: bool # pronunciation groups are readable
rate_extension: RateExtension | None # zonos3.tts.steering.RateExtension of the bundle
worker_device: str | None = None # device for helper workers (mlx: asr_device()); None = their default
def to_json(self) -> dict:
ext = self.rate_extension
if ext is not None:
ext = {
"direction": ext.direction,
"row_edges": list(ext.row_edges),
"below": list(ext.below),
"above": list(ext.above),
"doses": {lang: {str(b): float(d) for b, d in doses.items()} for lang, doses in ext.doses.items()},
"reference_sides": list(ext.reference_sides),
}
return {
"model_name": self.model_name,
"rate_edges": list(self.rate_edges),
"rate_buckets": self.rate_buckets,
"laci_languages": sorted(self.laci_languages),
"speaker_modes": list(self.speaker_modes),
"raw_controls": sorted(self.raw_controls),
"phoneme_tokens": self.phoneme_tokens,
"rate_extension": ext,
"worker_device": self.worker_device,
}
@classmethod
def from_json(cls, d: dict) -> HostFacts:
ext = d.get("rate_extension")
if ext is not None:
ext = RateExtension(
direction=ext["direction"],
row_edges=tuple(float(e) for e in ext["row_edges"]),
below=tuple(float(e) for e in ext["below"]),
above=tuple(float(e) for e in ext["above"]),
doses={lang: {int(b): float(x) for b, x in doses.items()} for lang, doses in ext["doses"].items()},
reference_sides=tuple(ext["reference_sides"]),
)
return cls(
model_name=d["model_name"],
rate_edges=tuple(float(e) for e in d["rate_edges"]),
rate_buckets=int(d["rate_buckets"]),
laci_languages=frozenset(d["laci_languages"]),
speaker_modes=tuple(d["speaker_modes"]),
raw_controls=frozenset(d["raw_controls"]),
phoneme_tokens=bool(d["phoneme_tokens"]),
rate_extension=ext,
worker_device=d.get("worker_device"),
)
@dataclass(frozen=True)
class LaciTakeRequest:
"""One LACI take with a duration target (zonos3.tts.laci.DurationTarget), optionally on an extended
speaking-rate scale (zonos3.tts.laci.RateBuckets)."""
request: GenerateRequest # laci=True, stream=False; planned exactly as /v2/generate
target_frames: float
adjust_rate: bool = True
rate_edges: tuple[float, ...] | None = None # extended scale: None = retakes move along the row
rate_first: int = 0
rate_bucket: int | None = None # the take's bucket on the scale
# steered bucket -> dose of facts.rate_extension.direction (ValueError without it)
rate_doses: Mapping[int, float] = field(default_factory=dict)
def to_json(self, out: str) -> dict:
"""The take as the ggml server's ``laci_takes`` engine call carries it (WAV written to ``out``)."""
scale = None
if self.rate_edges is not None:
scale = {
"edges": [float(e) for e in self.rate_edges],
"first": int(self.rate_first),
"bucket": self.rate_bucket,
"doses": {str(b): float(d) for b, d in self.rate_doses.items()},
}
return {
"request": self.request.model_dump(exclude_unset=True),
"target_frames": float(self.target_frames),
"adjust_rate": bool(self.adjust_rate),
"scale": scale,
"out": out,
}
# float32 mono at zonos3.generation.SAMPLE_RATE with the server's output gain (None: no audio), the LACI record
TakeResult = tuple["np.ndarray | None", dict]
class Host(Protocol):
asr: AsrWorker # zonos3.tts.edit_align: the server's shared workers
aligner: FuseAlignWorker
def facts(self) -> HostFacts: ...
async def synthesize_laci(self, takes: Sequence[LaciTakeRequest], session_id: str | None) -> list[TakeResult]:
"""Plan every take first (ValueError -> 400 before any is queued), then run them on the scheduler with at most
2x the running-request cap in flight so other requests interleave."""
async def encode_reference(self, audio: np.ndarray, sample_rate: int, max_seconds: float) -> list[list[int]]:
"""Raw-reference prefix codes [[code]*C]*frames of a mono float32 clip (the server's codec encode)."""
class Plugin(Protocol):
name: str # [a-z][a-z0-9_]*; serves only /v2/<name>/* and /<name>, /<name>/*
host_api: int # must equal HOST_API
keepalive_paths: tuple[str, ...] # POST paths (prefix match, as KeepaliveMiddleware) that honour X-Keepalive
def router(self, host: Host) -> APIRouter: ...
def close(self) -> None: ...
def load(names: Sequence[str]) -> list[Plugin]:
"""Each name: importlib.import_module(f"zonos3_{name}").plugin(). ValueError (startup error) for a bad name,
a missing package ("plugin 'NAME': zonos3_NAME is not installed in this interpreter"), name or host_api mismatch."""
loaded: list[Plugin] = []
for name in names:
if not _NAME.fullmatch(name):
raise ValueError(f"plugin {name!r}: a plugin name is [a-z][a-z0-9_]*")
if any(p.name == name for p in loaded):
raise ValueError(f"plugin {name!r} is listed twice")
package = f"zonos3_{name}"
try:
module = importlib.import_module(package)
except ModuleNotFoundError as exc:
if exc.name != package:
raise
raise ValueError(f"plugin {name!r}: {package} is not installed in this interpreter") from None
plugin = module.plugin()
if plugin.name != name:
raise ValueError(f"plugin {name!r}: {package}.plugin() is named {plugin.name!r}")
if plugin.host_api != HOST_API:
raise ValueError(f"plugin {name!r}: built for host API {plugin.host_api}, this server has {HOST_API}")
loaded.append(plugin)
return loaded
def _owned(path: str, name: str) -> bool:
return any(path == root or path.startswith(root + "/") for root in (f"/v2/{name}", f"/{name}"))
def mount(app: FastAPI, loaded: Sequence[Plugin], host: Host, keepalive_paths: list[str]) -> None:
"""include_router(p.router(host)) for each plugin; ValueError if a route is outside /v2/<name> or /<name>;
then move every route whose path is "/{path:path}" (the studio catch-all) back to the end of app.router.routes;
keepalive_paths.extend(p.keepalive_paths)."""
for plugin in loaded:
router = plugin.router(host)
stray = [route.path for route in router.routes if not _owned(route.path, plugin.name)]
if stray:
raise ValueError(f"plugin {plugin.name!r} serves {stray} outside /v2/{plugin.name} and /{plugin.name}")
app.include_router(router)
routes = app.router.routes
routes[:] = [r for r in routes if getattr(r, "path", None) != CATCH_ALL] + [
r for r in routes if getattr(r, "path", None) == CATCH_ALL
]
for plugin in loaded:
keepalive_paths.extend(plugin.keepalive_paths)
|