| """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 |
|
|
|
|
| |
| |
| |
|
|
| 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) |
|
|
| |
| |
| |
| |
| |
| 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() |
|
|
|
|
| |
| |
| |
|
|
| 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] |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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) |
|
|