inoryQwQ
feat: add AX620E and AX637 support (encoder NPU + decoder ONNX fallback)
0c313fe
Raw
History Blame Contribute Delete
8.24 kB
"""
Inflect-Micro-v2 TTS Engine — multi-chip, zero PyTorch.
Supports: AX650 (full NPU), AX620E (encoder NPU + decoder ONNX), AX637 (encoder NPU + decoder ONNX).
"""
from __future__ import annotations
import sys, os, io, math, re, json
from pathlib import Path
import numpy as np
import onnxruntime as ort
PKG = Path(__file__).resolve().parent
sys.path.insert(0, str(PKG))
sys.path.insert(0, str(PKG / "runtime"))
from inflect_vits_frontend import run_vits_frontend
from text.symbols import symbols
from text import cleaned_text_to_sequence
# ── numpy 工具函数 ──────────────────────────────────────────────────────
def _intersperse(lst, item):
result = [item] * (len(lst) * 2 + 1)
result[1::2] = lst
return result
def _sequence_mask(length, max_length=None):
if max_length is None:
max_length = int(length.max())
return np.arange(max_length, dtype=length.dtype)[None, :] < length[:, None]
def _generate_path(duration, mask):
b, _, t_y, t_x = mask.shape
cum_duration = np.cumsum(duration, axis=-1)
cum_duration_flat = cum_duration.reshape(b * t_x)
path = _sequence_mask(cum_duration_flat, t_y).astype(mask.dtype)
path = path.reshape(b, t_x, t_y)
padded = np.pad(path[:, :-1], ((0, 0), (1, 0), (0, 0)))
path = path - padded
path = path[:, np.newaxis, :, :].transpose(0, 1, 3, 2) * mask
return path
# ── 引擎 ────────────────────────────────────────────────────────────────
class InflectTTSEngine:
"""Inflect-Micro-v2 TTS — 多芯片,零 PyTorch。"""
def __init__(self, model_dir: str | Path | None = None, chip: str = "ax650"):
if model_dir:
self.root = Path(model_dir)
else:
# Default: models/{chip}/
self.root = PKG.parent.parent / "models" / chip
self.chip = chip
# 加载配置
with open(PKG / "config.json") as f:
self.hps = json.load(f)
self.sample_rate = int(self.hps["data"]["sampling_rate"])
self.hop_length = int(self.hps["data"]["hop_length"])
self.hidden_channels = int(self.hps["model"]["hidden_channels"])
self.inter_channels = int(self.hps["model"]["inter_channels"])
# 加载 chip-specific model_meta
meta_path = self.root / "model_meta.json"
if meta_path.exists():
with open(meta_path) as f:
self.meta = json.load(f)
else:
self.meta = {}
# Embedding (n_vocab, hidden_channels)
self._emb = np.load(str(self.root / "emb.npy"))
self._emb_scale = math.sqrt(self.hidden_channels)
# Duration Predictor ONNX
self._dp_sess = ort.InferenceSession(
str(self.root / "dp.onnx"), providers=["CPUExecutionProvider"]
)
# Encoder — always AXMODEL
try:
import axengine as axe
enc_path = str(self.root / "inflect_encoder.axmodel")
self.enc_session = axe.InferenceSession(enc_path)
self._has_npu = True
except ImportError:
self._has_npu = False
print("[WARN] axengine not available, encoder will use CPU ONNX")
self.enc_session = ort.InferenceSession(
str(self.root / "inflect_encoder.onnx"),
providers=["CPUExecutionProvider"],
)
# Decoder — AXMODEL or ONNX fallback
dec_axmodel = self.root / "inflect_decoder.axmodel"
if dec_axmodel.exists() and self._has_npu:
self.dec_session = axe.InferenceSession(str(dec_axmodel))
self._dec_is_npu = True
else:
dec_onnx = self.root / "inflect_decoder.onnx"
self.dec_session = ort.InferenceSession(
str(dec_onnx), providers=["CPUExecutionProvider"]
)
self._dec_is_npu = False
if self._has_npu:
print(f"[INFO] {chip}: decoder running on CPU (ONNX)")
def synthesize(self, text: str, speed: float = 1.0, variation: float = 0.667,
seed: int = 0):
normalized = " ".join(text.split())
if not normalized:
raise ValueError("Text must not be empty.")
sentences = [p.strip() for p in re.split(r"(?<=[.!?;:])\s+", normalized) if p.strip()]
if not sentences:
sentences = [normalized]
pieces = []
for idx, chunk in enumerate(sentences):
if idx > 0:
pieces.append(np.zeros(round(self.sample_rate * 0.08), dtype=np.float32))
phonemes = run_vits_frontend(chunk).phoneme_text
seq = cleaned_text_to_sequence(phonemes)
if self.hps["data"]["add_blank"]:
seq = _intersperse(seq, 0)
if not seq:
continue
tokens = np.array(seq, dtype=np.int64)
tlen = len(tokens)
# ── Embedding ──
x_emb = self._emb[tokens] * self._emb_scale
x_emb = x_emb.T[np.newaxis, :, :]
# ── Encoder ──
MAX_TOK = 200
if tlen > MAX_TOK:
raise ValueError(f"Text too long: {tlen} tokens > {MAX_TOK}")
x_pad = np.zeros((1, self.hidden_channels, MAX_TOK), dtype=np.float32)
x_pad[:, :, :tlen] = x_emb
enc_out = self.enc_session.run(None, {
"x_emb": x_pad,
"lengths": np.array([tlen], dtype=np.int32),
})
m_p = np.asarray(enc_out[0])[:, :, :tlen]
logs_p = np.asarray(enc_out[1])[:, :, :tlen]
x_enc = np.asarray(enc_out[2])[:, :, :tlen]
x_mask = np.asarray(enc_out[3])[:, :, :tlen]
# ── Duration Predictor ──
logw = self._dp_sess.run(None, {
"x": x_enc.astype(np.float32),
"x_mask": x_mask.astype(np.float32),
})[0]
# ── Duration + Alignment ──
rng = np.random.RandomState(seed + idx)
w = np.exp(logw) * x_mask * (1.0 / speed)
w_ceil = np.ceil(w)
y_lengths = np.clip(w_ceil.sum(axis=(1, 2)), 1, None).astype(np.int64)
y_mask = _sequence_mask(y_lengths, None)[:, np.newaxis, :].astype(x_mask.dtype)
attn_mask = x_mask[:, :, np.newaxis, :] * y_mask[:, :, :, np.newaxis]
attn = _generate_path(w_ceil, attn_mask)
m_p = (attn.squeeze(1) @ m_p.transpose(0, 2, 1)).transpose(0, 2, 1)
logs_p = (attn.squeeze(1) @ logs_p.transpose(0, 2, 1)).transpose(0, 2, 1)
z_p = m_p + rng.randn(*m_p.shape).astype(np.float32) * np.exp(logs_p) * variation
# ── Decoder ──
mel_len = z_p.shape[2]
MAX_MEL = 500
if mel_len > MAX_MEL:
raise ValueError(f"Audio too long: {mel_len} frames > {MAX_MEL}")
if self._dec_is_npu:
zp_np = np.zeros((1, self.inter_channels, MAX_MEL), dtype=np.float32)
ym_np = np.zeros((1, 1, MAX_MEL), dtype=np.float32)
zp_np[:, :, :mel_len] = z_p
ym_np[:, :, :mel_len] = y_mask
dec_out = self.dec_session.run(None, {"z_p": zp_np, "y_mask": ym_np})
waveform = dec_out[0][0, 0, :mel_len * self.hop_length]
else:
dec_out = self.dec_session.run(None, {
"z_p": z_p.astype(np.float32),
"y_mask": y_mask.astype(np.float32),
})
waveform = dec_out[0][0, 0, :mel_len * self.hop_length]
pieces.append(waveform)
waveform = np.clip(np.concatenate(pieces), -1.0, 1.0)
return self.sample_rate, waveform
def save(self, text: str, output: str | Path, **kwargs):
import soundfile as sf
dest = Path(output)
dest.parent.mkdir(parents=True, exist_ok=True)
sr, wav = self.synthesize(text, **kwargs)
sf.write(dest, wav, sr)
return dest