Download python/inflect_ax_tts/host_chain.py from AXERA-TECH/inflect_micro_v2: direct link, hf CLI and curl.
- Browser
- Download file 7.72 kB
-
https://huggingface.co/AXERA-TECH/inflect_micro_v2/resolve/main/python/inflect_ax_tts/host_chain.py
- Command line
-
hf download hf://AXERA-TECH/inflect_micro_v2/python/inflect_ax_tts/host_chain.py
-
curl -L -o host_chain.py https://huggingface.co/AXERA-TECH/inflect_micro_v2/resolve/main/python/inflect_ax_tts/host_chain.py
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) | |