steer-tts / steertts /request.py
ksingla025's picture
SteerTTS v1 (9.6M acoustic model + Vocos; flow refiner, prompted-flow cloning, prompt adapter); CC BY-NC 4.0 weights, MIT code
e7f5286 verified
Raw History Blame Contribute Delete
4.8 kB
"""The synthesis request: one JSON object, documented by request.schema.json (shipped inside this package and at the
model repo root). normalize_request() validates a request against a loaded model and fills in defaults; it raises
ValueError with a precise message instead of silently ignoring a misspelt field, tag or control.
"""
import json
from importlib import resources
from .config import CONTROL_DIMS, CONTROL_RANGE
DEFAULTS = dict(pace=1.0, guidance=1.0, flow=True, seed=0, markup=False)
FIELDS = {"text", "voice", "ref_wav", "prompt", "tag", "ctrl", "pace", "guidance", "word_shift", "markup", "flow",
"seed", "prompt_mapping", "$schema"}
WORD_SHIFT_KEYS = {"pitch": (-12.0, 12.0), "energy": (-20.0, 20.0), "dur": (0.25, 4.0)}
def schema():
"""The JSON Schema (draft 2020-12) of a request, as a dict."""
return json.loads(resources.files(__package__).joinpath("request.schema.json").read_text())
def _num(name, v, lo, hi):
if isinstance(v, bool) or not isinstance(v, (int, float)):
raise ValueError(f"{name} must be a number, got {v!r}")
if not lo <= float(v) <= hi:
raise ValueError(f"{name}={v} is outside [{lo}, {hi}]")
return float(v)
def normalize_request(req, tts=None):
"""-> a new dict with defaults filled in. tts (a SteerTTS) enables voice / tag checks."""
if not isinstance(req, dict):
raise ValueError("a request is a JSON object")
unknown = set(req) - FIELDS
if unknown:
raise ValueError(f"unknown request field(s) {sorted(unknown)}; see request.schema.json")
out = {k: v for k, v in req.items() if k != "$schema"}
text = out.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("text must be a non-empty string")
if len(text) > 5000:
raise ValueError("text is longer than 5000 characters; split it (or stream it sentence by sentence)")
if out.get("ref_wav") is not None:
if out.get("voice"):
raise ValueError("give either voice (a built-in voice) or ref_wav (clone a reference clip), not both")
else:
out.pop("ref_wav", None)
out["voice"] = out.get("voice") or (tts.default_voice if tts is not None else None)
if tts is not None and out["voice"] not in tts.spk_ref:
raise ValueError(f"unknown voice {out['voice']!r}; list them with tts.voices / `steertts voices`")
if out.get("prompt") is not None and not isinstance(out["prompt"], str):
raise ValueError("prompt must be a string")
if out.get("tag") is not None:
if not isinstance(out["tag"], str):
raise ValueError("tag must be a string")
if tts is not None and out["tag"] not in tts.tag2i:
raise ValueError(f"unknown tag {out['tag']!r}; known: {', '.join(tts.tags)}")
ctrl = out.get("ctrl") or {}
if not isinstance(ctrl, dict):
raise ValueError("ctrl must be an object {control: z}")
for k in ctrl:
if k not in CONTROL_DIMS:
raise ValueError(f"unknown control {k!r}; known: {', '.join(CONTROL_DIMS)}")
out["ctrl"] = {k: _num(f"ctrl.{k}", v, *CONTROL_RANGE) for k, v in ctrl.items()}
defaults = dict(DEFAULTS)
if tts is not None: # the loaded model's own defaults (SteerTTS(guidance=..., flow=...))
defaults.update(guidance=tts.guidance, flow=bool(tts.use_flow))
for k, v in defaults.items():
if out.get(k) is None:
out[k] = v
out["pace"] = _num("pace", out["pace"], 0.25, 4.0)
out["guidance"] = _num("guidance", out["guidance"], 0.0, 4.0)
for k in ("flow", "markup"):
if not isinstance(out[k], bool):
raise ValueError(f"{k} must be true or false")
if isinstance(out["seed"], bool) or not isinstance(out["seed"], int) or out["seed"] < 0:
raise ValueError("seed must be a non-negative integer")
ws = out.get("word_shift")
if ws:
if not isinstance(ws, dict):
raise ValueError("word_shift must be an object {word_index: {pitch, energy, dur}}")
if out["markup"]:
raise ValueError("word_shift and markup cannot be combined (markup re-indexes words per segment)")
norm = {}
for w, sh in ws.items():
try:
wi = int(w)
except (TypeError, ValueError):
raise ValueError(f"word_shift key {w!r} is not a word index") from None
if wi < 0 or not isinstance(sh, dict) or set(sh) - set(WORD_SHIFT_KEYS):
raise ValueError(f"word_shift[{w!r}] must be a non-negative index -> {{pitch, energy, dur}}")
norm[str(wi)] = {k: _num(f"word_shift.{w}.{k}", v, *WORD_SHIFT_KEYS[k]) for k, v in sh.items()}
out["word_shift"] = norm
else:
out.pop("word_shift", None)
return out