Inflect-Micro-v2-zh / python /infer_board.py
inoryQwQ's picture
default noise_scale=0.0 (stable long-sequence decoding)
f39fc30 verified
Raw
History Blame
9.32 kB
"""AX board inference: acoustic (CPU/ONNX) + BigVGAN (NPU/axmodel).
Run on the AX board with:
python3 infer_board.py --text "你好" --acoustic acoustic_female.onnx \
--vocoder bigvgan_base.axmodel --output out.wav
"""
import argparse
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cn_frontend import text_to_sequence
MAX_TEXT = 256
MAX_MEL = 1024
VOC_CHUNK = 512 # BigVGAN 静态输入帧数(5.46s@24kHz)
VOC_OVERLAP = 48 # 分块重叠帧(0.5s),交叉淡化消除边界爆音
MAX_SENT_TOKENS = 250 # 单句 token 上限(acoustic 上限 256,留 6 余量)
MEL_GATE_LO = -3.5 # mel 弱帧门限:低于此压到 floor(消除静音段底噪)
MEL_GATE_HI = -2.2 # 过渡区上界,高于此帧保持不动
LENGTH_SCALE = 0.7 # 语速(音节时长校正,接近自然节奏)
TAIL_STRETCH_OLD = 20 # 句尾拉伸:最后 N 帧
TAIL_STRETCH_NEW = 45 # 拉伸到 N 帧(解决句尾音节过短/尾字被吞)
def load_acoustic(path):
"""Load acoustic ONNX (CPU, fp32)."""
import onnxruntime as ort
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
return ort.InferenceSession(path, so, providers=["CPUExecutionProvider"])
def load_vocoder(path):
"""Load BigVGAN axmodel (NPU via axengine)."""
if path.endswith(".onnx"):
import onnxruntime as ort
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
return ort.InferenceSession(path, so, providers=["CPUExecutionProvider"])
import axengine as axe
try:
return axe.InferenceSession(path, providers=["AxEngineExecutionProvider"])
except Exception:
return axe.InferenceSession(
path, providers=["RemoteAXExecutionProvider"],
provider_options={"host": os.environ.get("AX_BOARD", "127.0.0.1"),
"port": "18500"})
def split_sentences(text, max_tokens=MAX_SENT_TOKENS):
"""Split long text into sentences that fit the acoustic model."""
ids = text_to_sequence(text)
tokens = [0]
for pid in ids:
tokens.append(pid)
tokens.append(0)
if len(tokens) <= max_tokens:
return [text]
# split on punctuation, then on hard limit
import re
parts = re.split(r"([,。!?;:、,.!?;:])", text)
sents, cur = [], ""
for p in parts:
cand = cur + p
cand_tokens = len([0]) + 2 * len(text_to_sequence(cand))
if len(text_to_sequence(cand)) * 2 + 1 > max_tokens and cur:
sents.append(cur)
cur = p
else:
cur = cand
if cur:
sents.append(cur)
# hard fallback: cut by characters
out = []
for s in sents:
while len(text_to_sequence(s)) * 2 + 1 > max_tokens:
cut = max(1, (max_tokens - 1) // 2)
out.append(s[:cut])
s = s[cut:]
if s:
out.append(s)
return [s for s in out if s.strip()]
def text_to_inputs(text, noise_scale, seed):
np.random.seed(seed)
ids = text_to_sequence(text)
tokens = [0]
for pid in ids:
tokens.append(pid)
tokens.append(0)
n = min(len(tokens), MAX_TEXT)
x = np.zeros((1, MAX_TEXT), dtype=np.int64)
x[0, :n] = tokens[:n]
x_lengths = np.array([n], dtype=np.int64)
noise_z = np.random.randn(1, 192, MAX_MEL).astype(np.float32)
if noise_scale != 0.3:
noise_z = noise_z * (noise_scale / 0.3)
return x, x_lengths, noise_z
def acoustic_to_mel(acoustic, x, x_lengths, noise_z):
mel, y_lengths = acoustic.run(None, {
"x": x, "x_lengths": x_lengths, "noise_z": noise_z,
})
return mel[:, :, :int(y_lengths[0])]
def mel_soft_gate(mel, thr_lo=MEL_GATE_LO, thr_hi=MEL_GATE_HI, floor=-11.5):
"""压平预测 mel 的弱帧(静音/停顿)以消除渲染底噪。
只调整帧能量 < thr_lo 的帧(向 floor 收敛)和过渡区,
强语音帧(>= thr_hi)完全保持。"""
out = mel.copy()
fe = out.max(axis=1)
for i in range(mel.shape[2]):
f = fe[0, i]
if f < thr_lo:
target = floor
elif f < thr_hi:
t = (f - thr_lo) / (thr_hi - thr_lo)
target = floor * (1 - t) + f * t
else:
continue
out[:, :, i] = out[:, :, i] - f + target
return out
def mel_sharpen(mel, alpha=0.5, k=5):
"""Spectral contrast boost along the mel-band axis: m + alpha*(m - smooth(m))."""
if alpha <= 0 or k <= 1:
return mel
m = mel[0] if mel.ndim == 3 else mel
pad = k // 2
mp = np.pad(m, ((pad, pad), (0, 0)), mode="reflect")
smooth = np.zeros_like(m)
for i in range(k):
smooth += mp[i:i + m.shape[0]]
smooth /= k
sharp = m + alpha * (m - smooth)
return (sharp if mel.ndim == 2 else sharp[None]).astype(np.float32)
def tail_stretch(mel, n_old=TAIL_STRETCH_OLD, n_new=TAIL_STRETCH_NEW):
"""句尾 mel 拉伸:最后 n_old 帧线性插值到 n_new 帧。
模型对句尾音节 duration 预测偏短(如 11 帧 vs 参考 43 帧),
拉伸后尾字清晰完整。"""
T = mel.shape[2]
if T <= n_old:
return mel
tail = mel[:, :, -n_old:]
xo = np.linspace(0, 1, n_old)
xn = np.linspace(0, 1, n_new)
nt = np.stack([np.interp(xn, xo, tail[0, c]) for c in range(mel.shape[1])])
return np.concatenate([mel[:, :, :-n_old], nt[None].astype(np.float32)], axis=2)
def vocoder_chunked(vocoder, mel):
"""Run BigVGAN on arbitrary-length mel via overlapped chunks + crossfade."""
mel = mel.astype(np.float32)
T = mel.shape[2]
if T <= VOC_CHUNK:
m = np.pad(mel, ((0, 0), (0, 0), (0, VOC_CHUNK - T)),
constant_values=-11.5)
w = vocoder.run(None, {"mel": m})[0][0, 0]
return w[:T * 256] # strip trailing silence padding
hop = VOC_CHUNK - VOC_OVERLAP
starts = list(range(0, T, hop))
frame = 256
wav = np.zeros(T * frame)
for i, s in enumerate(starts):
v = min(VOC_CHUNK, T - s)
block = mel[:, :, s:s + VOC_CHUNK]
if block.shape[2] < VOC_CHUNK:
block = np.pad(block, ((0, 0), (0, 0), (0, VOC_CHUNK - block.shape[2])),
constant_values=-11.5)
c = vocoder.run(None, {"mel": block})[0][0, 0]
c = c[:v * frame]
base = s * frame
ov = 0
if i > 0:
ov = min(VOC_OVERLAP * frame, base, len(c))
fade = np.linspace(0, 1, ov)
wav[base - ov:base] = wav[base - ov:base] * (1 - fade) + c[:ov] * fade
wav[base + ov:base + len(c)] = c[ov:]
return wav
def compress_pauses(wav, sr=24000, min_pause_ms=90, target_ms=60):
"""Compress long silences to reduce choppy rhythm."""
win, hop = int(sr * 0.01), int(sr * 0.005)
n = (len(wav) - win) // hop
e = np.array([np.sqrt(np.mean(wav[i*hop:i*hop+win]**2)) for i in range(n)])
thr = max(e.max() * 0.12, 0.008)
sil = e < thr
# find silence runs
runs = []
i = 0
while i < len(sil):
if sil[i]:
j = i
while j < len(sil) and sil[j]:
j += 1
runs.append((i, j))
i = j
else:
i += 1
keep = np.ones(len(wav), dtype=bool)
for s, e_ in runs:
dur_ms = (e_ - s) * hop / sr * 1000
if dur_ms > min_pause_ms:
s0, s1 = s * hop, min(e_ * hop + win, len(wav))
target = int(target_ms / 1000 * sr)
# keep first `target` samples of the pause
if s1 - s0 > target:
keep[s0 + target:s1] = False
out = wav[keep]
return out
def synthesize(acoustic, vocoder, text, noise_scale=0.0, seed=0):
sents = split_sentences(text)
pieces = []
for s in sents:
x, x_lengths, noise_z = text_to_inputs(s, noise_scale, seed)
mel = acoustic_to_mel(acoustic, x, x_lengths, noise_z)
mel = mel_sharpen(mel)
mel = mel_soft_gate(mel)
mel = tail_stretch(mel)
pieces.append(vocoder_chunked(vocoder, mel))
if len(pieces) == 1:
return pieces[0], pieces[0].shape[0] // 256
# concat sentences with a short pause
gap = np.zeros(int(24000 * 0.2))
wav = pieces[0]
for p in pieces[1:]:
wav = np.concatenate([wav, gap, p])
return wav, wav.shape[0] // 256
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--text", required=True)
parser.add_argument("--acoustic", default="export/acoustic_female.onnx")
parser.add_argument("--vocoder", default="export/axmodel/bigvgan_base.axmodel")
parser.add_argument("--output", default="board_out.wav")
parser.add_argument("--noise_scale", type=float, default=0.0)
args = parser.parse_args()
import soundfile as sf
acoustic = load_acoustic(args.acoustic)
vocoder = load_vocoder(args.vocoder)
wav, T = synthesize(acoustic, vocoder, args.text,
noise_scale=args.noise_scale)
wav = wav / (np.abs(wav).max() + 1e-8) * 0.95
sf.write(args.output, wav, 24000)
print(f"saved: {args.output} ({len(wav)/24000:.2f}s, mel_frames={T})")
if __name__ == "__main__":
main()