inoryQwQ's picture
三芯片合并:AX620E/AX637 升级 encoder+decoder 全 NPU,新增新一代 SDK;AX650 保持老 SDK
5eee449 verified
Raw
History Blame Contribute Delete
7.72 kB
"""Host-side chain between encoder.axmodel and decoder.axmodel (pure numpy).
Replicates, in numpy, the torch host chain of export/export_onnx.py:host_expand
(which itself mirrors origin/runtime/models.py:546-558):
w = exp(logw) * x_mask * length_scale
w_ceil = ceil(w); T' = max(sum(w_ceil), 1)
attn = generate_path(w_ceil, mask) # pure integer logic
m_p' = attn @ m_p^T ; logs_p' = attn @ logs_p^T
z_p = m_p' + randn * exp(logs_p') * variation (seed-controlled, host)
Also contains the fixed-shape plumbing: encoder T=256 zero padding and decoder
Tp=512 chunking with overlap crossfade (EXPORT_NOTES §5.3 recommends
>=64-frame overlap between chunks; single-chunk T'<=512 is the path validated
by SIMULATE).
"""
from __future__ import annotations
import numpy as np
SAMPLE_RATE = 24000
HOP_LENGTH = 256
HIDDEN_CHANNELS = 192
ENCODER_T = 256
DECODER_TP = 512
DECODER_OVERLAP = 64 # frames of overlap between decoder chunks (EXPORT_NOTES §5.3)
# ---------------------------------------------------------------------------
# Duration rounding + alignment (generate_path)
# ---------------------------------------------------------------------------
def expand_priors(
logw: np.ndarray,
m_p: np.ndarray,
logs_p: np.ndarray,
x_len: int,
length_scale: float,
) -> tuple[np.ndarray, np.ndarray, int]:
"""Duration rounding + generate_path + prior expansion.
Args:
logw: [1, 1, T] log-durations (encoder output, static T)
m_p: [1, C, T] prior means
logs_p: [1, C, T] prior log-stds
x_len: number of valid (unpadded) token frames
length_scale: 1.0 / speed
Returns:
(m_p_e [T', C], logs_p_e [T', C], T')
"""
t_total = logw.shape[-1]
if not 1 <= x_len <= t_total:
raise ValueError(f"x_len {x_len} out of range [1, {t_total}]")
x_mask = (np.arange(t_total) < x_len).astype(np.float32)
w = np.exp(logw[0, 0].astype(np.float32)) * x_mask * np.float32(length_scale)
w_ceil = np.ceil(w).astype(np.int64)
y_len = max(int(w_ceil.sum()), 1)
# generate_path (origin/runtime/commons.py): with the masked durations the
# alignment reduces to pure integer interval logic — frame t of the output
# copies input frame i for cum[i-1] <= t < cum[i]. O(T) loop, no matmul
# of the [T', T] path needed; we still build attn explicitly for the prior
# expansion to stay a literal port (T=256, T'<=~2000: cheap).
cum = np.cumsum(w_ceil)
attn = np.zeros((y_len, t_total), dtype=np.float32)
lo = 0
for i in range(t_total):
hi = min(int(cum[i]), y_len)
if hi > lo:
attn[lo:hi, i] = 1.0
lo = hi
m_p_e = attn @ m_p[0].T.astype(np.float32)
logs_p_e = attn @ logs_p[0].T.astype(np.float32)
return m_p_e, logs_p_e, y_len
def inject_noise(
m_p_e: np.ndarray,
logs_p_e: np.ndarray,
variation: float,
seed: int,
) -> np.ndarray:
"""z_p = m_p' + randn * exp(logs_p') * variation, seed-controlled.
NOTE: numpy PCG64 is used, not torch's MT19937 — a given `seed` is
deterministic and reproducible within this SDK but is NOT bit-identical to
the PyTorch reference (origin/inference.py).
"""
rng = np.random.default_rng(seed)
noise = rng.standard_normal(m_p_e.shape, dtype=np.float32)
z_p = m_p_e + noise * np.exp(logs_p_e.astype(np.float32)) * np.float32(variation)
return z_p.T.copy() # [C, T']
# ---------------------------------------------------------------------------
# Decoder chunk composition (Tp=512 static shape, overlap crossfade)
# ---------------------------------------------------------------------------
def decoder_chunk_starts(t_prime: int, chunk: int = DECODER_TP,
overlap: int = DECODER_OVERLAP) -> list[int]:
"""Frame offsets of decoder chunks covering [0, t_prime) with >=overlap."""
if t_prime <= chunk:
return [0]
stride = chunk - overlap
starts = list(range(0, t_prime - chunk + 1, stride))
last = t_prime - chunk
if starts[-1] < last:
starts.append(last)
return starts
def decode_waveform(
z_p: np.ndarray,
run_decoder,
chunk: int = DECODER_TP,
overlap: int = DECODER_OVERLAP,
) -> np.ndarray:
"""Run the fixed-shape decoder over z_p and stitch chunks.
Args:
z_p: [C, T'] float32 prior with noise injected.
run_decoder: callable taking z_p_chunk [C, chunk] float32 (already
right-padded with zeros) and returning wav [chunk*HOP] float32.
chunk/overlap: see DECODER_TP / DECODER_OVERLAP.
Returns:
wav [T'*HOP] float32 (tail trimmed; chunk overlaps crossfaded).
"""
t_prime = z_p.shape[1]
hop = HOP_LENGTH
starts = decoder_chunk_starts(t_prime, chunk, overlap)
out: np.ndarray | None = None
prev_end_frame = 0
for start in starts:
take = min(chunk, t_prime - start)
z_chunk = np.zeros((z_p.shape[0], chunk), dtype=np.float32)
z_chunk[:, :take] = z_p[:, start:start + take]
wav = np.asarray(run_decoder(z_chunk), dtype=np.float32)
if out is None:
out = wav
else:
ov_frames = prev_end_frame - start
ov = ov_frames * hop
fade_in = np.linspace(0.0, 1.0, ov, endpoint=True, dtype=np.float32)
out = np.concatenate(
[out[:-ov], out[-ov:] * (1.0 - fade_in) + wav[:ov] * fade_in, wav[ov:]]
)
prev_end_frame = start + chunk
assert out is not None
return out[: t_prime * hop]
# ---------------------------------------------------------------------------
# Waveform post-processing (origin/inference.py)
# ---------------------------------------------------------------------------
def edge_fade(waveform: np.ndarray, sample_rate: int = SAMPLE_RATE,
milliseconds: float = 5.0) -> np.ndarray:
frames = min(round(sample_rate * milliseconds / 1000.0), waveform.size // 2)
if frames <= 0:
return waveform
output = waveform.copy()
ramp = np.linspace(0.0, 1.0, frames, endpoint=True, dtype=np.float32)
output[:frames] *= ramp
output[-frames:] *= ramp[::-1]
return output
# ---------------------------------------------------------------------------
# Text chunking (origin/inference.py: split_text / boundary_pause_seconds)
# ---------------------------------------------------------------------------
def split_text(text: str, limit: int = 280) -> list[str]:
import re
normalized = " ".join(text.split())
sentences = [
part.strip()
for part in re.split(r"(?<=[.!?;:])\s+", normalized)
if part.strip()
]
chunks: list[str] = []
for sentence in sentences or [normalized]:
while len(sentence) > limit:
search = sentence[: limit + 1]
punctuation = max(search.rfind(mark) for mark in (",", ";", ":"))
split_at = (
punctuation + 1
if punctuation >= limit // 2
else sentence.rfind(" ", 0, limit + 1)
)
if split_at < limit // 2:
split_at = limit
chunks.append(sentence[:split_at].strip())
sentence = sentence[split_at:].strip()
if sentence:
chunks.append(sentence)
return chunks
def boundary_pause_seconds(chunk: str) -> float:
ending = chunk.rstrip()[-1:] if chunk.strip() else ""
return {
"?": 0.28,
"!": 0.24,
".": 0.22,
";": 0.16,
":": 0.13,
",": 0.09,
}.get(ending, 0.08)
def seconds_to_samples(seconds: float) -> int:
return round(seconds * SAMPLE_RATE)