Initial release: optimized CPU runtime and converted weights of maya2-native
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- NOTICE +10 -0
- README.md +127 -0
- assets/v1/audio_in_bf16.bin +3 -0
- assets/v1/backbone_wo8.data +3 -0
- assets/v1/backbone_wo8.onnx +3 -0
- assets/v1/controls.json +183 -0
- assets/v1/depth_frame_fp16.onnx +3 -0
- assets/v1/depth_frame_fp16.onnx.data +3 -0
- assets/v1/embed_bf16.bin +3 -0
- assets/v1/manifest.json +29 -0
- assets/v1/mimi_decoder.onnx +3 -0
- assets/v1/prompt.npz +3 -0
- assets/v1/tokenizer.json +3 -0
- assets/v1/voices.json +102 -0
- requirements-fast.txt +5 -0
- src/maya_fast/__init__.py +24 -0
- src/maya_fast/__main__.py +48 -0
- src/maya_fast/__pycache__/__init__.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/__main__.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/assets.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/backbone.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/codec.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/control.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/depth.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/embed.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/engine.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/ort_opts.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/prompt.cpython-313.pyc +0 -0
- src/maya_fast/__pycache__/sampling.cpython-313.pyc +0 -0
- src/maya_fast/assets.py +91 -0
- src/maya_fast/backbone.py +157 -0
- src/maya_fast/codec.py +34 -0
- src/maya_fast/control.py +135 -0
- src/maya_fast/depth.py +62 -0
- src/maya_fast/embed.py +25 -0
- src/maya_fast/engine.py +236 -0
- src/maya_fast/ort_opts.py +20 -0
- src/maya_fast/prompt.py +103 -0
- src/maya_fast/sampling.py +34 -0
- tests/__init__.py +0 -0
- tests/fixtures/__init__.py +0 -0
- tests/fixtures/tiny_assets.py +286 -0
- tests/test_audio_utils.py +76 -0
- tests/test_maya_fast_assets.py +51 -0
- tests/test_maya_fast_backbone.py +50 -0
- tests/test_maya_fast_codec.py +45 -0
- tests/test_maya_fast_control.py +123 -0
- tests/test_maya_fast_depth.py +41 -0
- tests/test_maya_fast_engine.py +95 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/v1/backbone_wo8.data filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/v1/depth_frame_fp16.onnx.data filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/v1/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
NOTICE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
maya2-native-fast
|
| 2 |
+
==================
|
| 3 |
+
|
| 4 |
+
Derived from maya-research/maya2-native (Maya Research), Apache-2.0.
|
| 5 |
+
Includes a Mimi decoder converted from kyutai/mimi (CC-BY-4.0).
|
| 6 |
+
Backbone initialised from Qwen3-1.7B-Base (Qwen team), Apache-2.0.
|
| 7 |
+
Speaker conditioning uses speechbrain/spkrec-ecapa-voxceleb (SpeechBrain), Apache-2.0.
|
| 8 |
+
|
| 9 |
+
Apache-2.0 full text: https://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
CC-BY-4.0 full text: https://creativecommons.org/licenses/by/4.0/
|
README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: maya-research/maya2-native
|
| 4 |
+
language:
|
| 5 |
+
- mr
|
| 6 |
+
- hi
|
| 7 |
+
library_name: onnx
|
| 8 |
+
pipeline_tag: text-to-speech
|
| 9 |
+
tags:
|
| 10 |
+
- text-to-speech
|
| 11 |
+
- onnx
|
| 12 |
+
- int8
|
| 13 |
+
- fp16
|
| 14 |
+
- marathi
|
| 15 |
+
- hindi
|
| 16 |
+
- cpu
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# maya2-native-fast
|
| 20 |
+
|
| 21 |
+
Optimized CPU inference for [maya-research/maya2-native](https://huggingface.co/maya-research/maya2-native):
|
| 22 |
+
ONNX graphs, int8 backbone, fp16 depth, a torch-free numpy runtime.
|
| 23 |
+
|
| 24 |
+
**This is not a fine-tune.** The weights are converted from the upstream checkpoint. No training data was used.
|
| 25 |
+
|
| 26 |
+
## What is in the repo
|
| 27 |
+
|
| 28 |
+
| Path | Contents |
|
| 29 |
+
|---|---|
|
| 30 |
+
| `assets/v1/backbone_wo8.onnx` + `.data` | 28-layer Qwen3 backbone, int8 weights (MatMulNBits, block 32), 1.6 GB |
|
| 31 |
+
| `assets/v1/depth_frame_fp16.onnx` + `.data` | Depth sampler, 15 levels, fp16 storage, Gumbel top-k sampling in-graph, 873 MB |
|
| 32 |
+
| `assets/v1/mimi_decoder.onnx` | Mimi decoder, fp32, dynamic frame count, 195 MB |
|
| 33 |
+
| `assets/v1/embed_bf16.bin`, `audio_in_bf16.bin` | Text and audio embedding tables, bf16, memory-mapped |
|
| 34 |
+
| `assets/v1/prompt.npz`, `tokenizer.json`, `voices.json`, `controls.json` | Prompt weights, extended tokenizer, voices, sentence controls |
|
| 35 |
+
| `assets/v1/manifest.json` | sha256 of every file above, runtime shapes. The runtime refuses mismatches. |
|
| 36 |
+
| `src/maya_fast/` | Runtime (numpy, onnxruntime, tokenizers; no torch) |
|
| 37 |
+
| `tools/maya_export/` | Export and evaluation scripts, to rebuild the assets from the upstream checkpoint |
|
| 38 |
+
| `tests/` | Runtime and export tests |
|
| 39 |
+
|
| 40 |
+
## Measured results (Apple M-series Mac, CPU, 8 threads)
|
| 41 |
+
|
| 42 |
+
| Check | Result | Target | Status |
|
| 43 |
+
|---|---|---|---|
|
| 44 |
+
| Depth fp16 vs fp32 reference, teacher-forced, 75 files | top-1 >= 0.9965 every level, KL <= 6e-6 | top-1 >= 0.98, KL <= 0.02 | pass |
|
| 45 |
+
| Depth fp32 vs upstream PyTorch, 20 frames | rel 1.6e-6 | rel <= 1e-4 | pass |
|
| 46 |
+
| Mimi decoder vs upstream | max abs 1.5e-6 | <= 1e-4 | pass |
|
| 47 |
+
| Prompt port vs upstream, 500 prompts | 0 token mismatches, max embed error 7.5e-9 | <= 1e-5 | pass |
|
| 48 |
+
| Backbone int8 vs fp32, teacher-forced | top-1 0.9677, KL 0.0034 | top-1 >= 0.99 | **fail (waived)** |
|
| 49 |
+
| Backbone speed, 8 threads | 48 / 58 / 82 ms per token at 32 / 256 / 768 cached tokens | <= 35 ms at 256 | **fail (waived)** |
|
| 50 |
+
| End-to-end, one Marathi sentence (2.96 s audio), two runs | RTF 4.67 and 4.43 | offline RTF <= 4 | **fail** |
|
| 51 |
+
| Peak RSS, end-to-end | 3.9 GB | <= 3 GB target, 5 GB limit | over target |
|
| 52 |
+
|
| 53 |
+
Known limits:
|
| 54 |
+
- The int8 backbone misses top-1 agreement; expect small differences in generated codes.
|
| 55 |
+
- End-to-end RTF on this machine is above the offline target.
|
| 56 |
+
- Hindi (`--lang hi`) works mechanically; it has not been checked for quality.
|
| 57 |
+
- Not yet checked by listening tests.
|
| 58 |
+
- Real-time playback is not supported.
|
| 59 |
+
|
| 60 |
+
## Install
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
python -m venv .venv
|
| 64 |
+
.venv/bin/pip install -r requirements-fast.txt
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Requires Python 3.10 to 3.13. Peak memory is about 4 GB while a sentence is synthesized.
|
| 68 |
+
|
| 69 |
+
## Use: command line
|
| 70 |
+
|
| 71 |
+
Run from the repo root (so `src` is importable):
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
PYTHONPATH=. .venv/bin/python -m src.maya_fast "नमस्कार, मी तुमची मदत करण्यासाठी इथे आहे." \
|
| 75 |
+
--assets assets/v1 -o out.wav
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
Options:
|
| 79 |
+
- `--voice NAME`: named voice from `assets/v1/voices.json` (case-insensitive). Default `Charvi`.
|
| 80 |
+
- `--lang mr|hi`: language tag. Default `mr`.
|
| 81 |
+
- `--seed N`: random seed. Same seed and threads give the same audio.
|
| 82 |
+
- `--threads N`: ORT threads. Default min(8, cores).
|
| 83 |
+
|
| 84 |
+
## Use: Python
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
from src.maya_fast.engine import MayaFast
|
| 88 |
+
|
| 89 |
+
engine = MayaFast(assets_dir="assets/v1", threads=8)
|
| 90 |
+
wav, metrics = engine.synthesize("नमस्कार, मी तुमची मदत करण्यासाठी इथे आहे.", voice="Charvi", seed=0, lang="mr")
|
| 91 |
+
# wav: float32, 24 kHz mono
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
Write it with `soundfile` or the standard `wave` module.
|
| 95 |
+
|
| 96 |
+
Assets are verified against `manifest.json` at load. A bad or partial download fails with the file name.
|
| 97 |
+
|
| 98 |
+
## Rebuild the assets from upstream (optional)
|
| 99 |
+
|
| 100 |
+
Needs the upstream checkpoint (`maya-research/maya2-native`, `kyutai/mimi`, `speechbrain/spkrec-ecapa-voxceleb`) and an upstream `maya3` install in a second venv (`.venv-tts`, torch 2.8, transformers 5.6.2). The export scripts write to `$MAYA_FAST_WORK/assets/v1`, default `~/.cache/maya_fast`. See `tools/maya_export/`:
|
| 101 |
+
1. `export_prompt_assets.py`, `export_backbone.py --precision wo8`, `export_depth_all.py`, `export_codec.py`
|
| 102 |
+
2. `finalize_assets.py` writes the runtime manifest.
|
| 103 |
+
Run each step under `tools/memguard.py`, which kills the process above a memory limit.
|
| 104 |
+
|
| 105 |
+
## License and attribution
|
| 106 |
+
|
| 107 |
+
- These weights and code are derived from Maya2 Native (Apache-2.0) by Maya Research.
|
| 108 |
+
- The Mimi decoder is derived from Kyutai Mimi (CC-BY-4.0). Keep this attribution.
|
| 109 |
+
- The text backbone is derived from Qwen3-1.7B-Base (Apache-2.0).
|
| 110 |
+
- Speaker conditioning uses SpeechBrain ECAPA-TDNN (Apache-2.0).
|
| 111 |
+
- Full notices in `NOTICE`.
|
| 112 |
+
|
| 113 |
+
The model adds no audio watermark.
|
| 114 |
+
|
| 115 |
+
## Citation
|
| 116 |
+
|
| 117 |
+
Please cite the upstream model:
|
| 118 |
+
|
| 119 |
+
```bibtex
|
| 120 |
+
@misc{maya2native2026,
|
| 121 |
+
title = {Maya 2 Native: Streaming Multilingual Text-to-Speech},
|
| 122 |
+
author = {{Maya Research}},
|
| 123 |
+
year = {2026},
|
| 124 |
+
url = {https://huggingface.co/maya-research/maya2-native},
|
| 125 |
+
note = {Release 1.0}
|
| 126 |
+
}
|
| 127 |
+
```
|
assets/v1/audio_in_bf16.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2670e5583c2d2e6e963a8117136a8d07e0e892d3ffda2620d3f13cdbaddc94d0
|
| 3 |
+
size 268435456
|
assets/v1/backbone_wo8.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ce9ebfb1e911bac4e3bc58b77c6657f75b79f145f89df718f23d352d824dcb01
|
| 3 |
+
size 1602727936
|
assets/v1/backbone_wo8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d0326c5973d9d71708d7c39c3f34851c73addc88bceb46d0ac90c37e9041955d
|
| 3 |
+
size 1121189
|
assets/v1/controls.json
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"text_frames_prior": {
|
| 3 |
+
"as": {
|
| 4 |
+
"a": -1.36,
|
| 5 |
+
"b": 0.7786,
|
| 6 |
+
"resid_p99": 91.6
|
| 7 |
+
},
|
| 8 |
+
"bn": {
|
| 9 |
+
"a": 3.16,
|
| 10 |
+
"b": 0.7887,
|
| 11 |
+
"resid_p99": 76.2
|
| 12 |
+
},
|
| 13 |
+
"en": {
|
| 14 |
+
"a": 0.6,
|
| 15 |
+
"b": 0.7416,
|
| 16 |
+
"resid_p99": 75.0
|
| 17 |
+
},
|
| 18 |
+
"gu": {
|
| 19 |
+
"a": 1.74,
|
| 20 |
+
"b": 0.8834,
|
| 21 |
+
"resid_p99": 83.1
|
| 22 |
+
},
|
| 23 |
+
"hi": {
|
| 24 |
+
"a": 5.65,
|
| 25 |
+
"b": 0.7644,
|
| 26 |
+
"resid_p99": 79.8
|
| 27 |
+
},
|
| 28 |
+
"kn": {
|
| 29 |
+
"a": 1.91,
|
| 30 |
+
"b": 0.7675,
|
| 31 |
+
"resid_p99": 69.5
|
| 32 |
+
},
|
| 33 |
+
"ml": {
|
| 34 |
+
"a": 1.25,
|
| 35 |
+
"b": 0.6788,
|
| 36 |
+
"resid_p99": 105.6
|
| 37 |
+
},
|
| 38 |
+
"mr": {
|
| 39 |
+
"a": 1.12,
|
| 40 |
+
"b": 0.8235,
|
| 41 |
+
"resid_p99": 60.1
|
| 42 |
+
},
|
| 43 |
+
"or": {
|
| 44 |
+
"a": 1.06,
|
| 45 |
+
"b": 0.7936,
|
| 46 |
+
"resid_p99": 68.4
|
| 47 |
+
},
|
| 48 |
+
"pa": {
|
| 49 |
+
"a": 1.86,
|
| 50 |
+
"b": 0.8079,
|
| 51 |
+
"resid_p99": 73.5
|
| 52 |
+
},
|
| 53 |
+
"ta": {
|
| 54 |
+
"a": 2.24,
|
| 55 |
+
"b": 0.6425,
|
| 56 |
+
"resid_p99": 70.8
|
| 57 |
+
},
|
| 58 |
+
"te": {
|
| 59 |
+
"a": 1.2,
|
| 60 |
+
"b": 0.7927,
|
| 61 |
+
"resid_p99": 89.6
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"prior_default": {
|
| 65 |
+
"a": 2.0,
|
| 66 |
+
"b": 0.72,
|
| 67 |
+
"resid_p99": 90.0
|
| 68 |
+
},
|
| 69 |
+
"text": {
|
| 70 |
+
"terminals": "।.!?;।॥",
|
| 71 |
+
"fps": 12.5,
|
| 72 |
+
"frame_s": 0.08,
|
| 73 |
+
"chunk_s": 16.0
|
| 74 |
+
},
|
| 75 |
+
"guard": {
|
| 76 |
+
"enabled": true,
|
| 77 |
+
"dur_k": 2.2,
|
| 78 |
+
"dur_margin_s": 1.2,
|
| 79 |
+
"hard_cap": 1500,
|
| 80 |
+
"dur_prior_coefs": {
|
| 81 |
+
"as": [
|
| 82 |
+
0.0,
|
| 83 |
+
0.07962,
|
| 84 |
+
0.17764,
|
| 85 |
+
0.53121,
|
| 86 |
+
0.62254
|
| 87 |
+
],
|
| 88 |
+
"bn": [
|
| 89 |
+
0.71369,
|
| 90 |
+
0.06604,
|
| 91 |
+
0.04537,
|
| 92 |
+
0.49869,
|
| 93 |
+
0.23288
|
| 94 |
+
],
|
| 95 |
+
"en": [
|
| 96 |
+
2.87006,
|
| 97 |
+
0.01,
|
| 98 |
+
0.04601,
|
| 99 |
+
0.40336,
|
| 100 |
+
0.06541
|
| 101 |
+
],
|
| 102 |
+
"gu": [
|
| 103 |
+
0.54378,
|
| 104 |
+
0.07044,
|
| 105 |
+
0.01,
|
| 106 |
+
0.50786,
|
| 107 |
+
0.24873
|
| 108 |
+
],
|
| 109 |
+
"hi": [
|
| 110 |
+
2.44169,
|
| 111 |
+
0.05329,
|
| 112 |
+
0.02187,
|
| 113 |
+
0.43341,
|
| 114 |
+
0.37052
|
| 115 |
+
],
|
| 116 |
+
"kn": [
|
| 117 |
+
1.71201,
|
| 118 |
+
0.06651,
|
| 119 |
+
0.01,
|
| 120 |
+
0.45228,
|
| 121 |
+
0.5053
|
| 122 |
+
],
|
| 123 |
+
"ml": [
|
| 124 |
+
0.92428,
|
| 125 |
+
0.05706,
|
| 126 |
+
0.11842,
|
| 127 |
+
0.52651,
|
| 128 |
+
0.41106
|
| 129 |
+
],
|
| 130 |
+
"mr": [
|
| 131 |
+
1.44052,
|
| 132 |
+
0.0719,
|
| 133 |
+
0.03206,
|
| 134 |
+
0.30526,
|
| 135 |
+
0.28705
|
| 136 |
+
],
|
| 137 |
+
"or": [
|
| 138 |
+
0.0,
|
| 139 |
+
0.08134,
|
| 140 |
+
0.12578,
|
| 141 |
+
0.43209,
|
| 142 |
+
0.23588
|
| 143 |
+
],
|
| 144 |
+
"pa": [
|
| 145 |
+
0.57673,
|
| 146 |
+
0.07431,
|
| 147 |
+
0.01,
|
| 148 |
+
0.39355,
|
| 149 |
+
0.37283
|
| 150 |
+
],
|
| 151 |
+
"ta": [
|
| 152 |
+
2.05273,
|
| 153 |
+
0.05244,
|
| 154 |
+
0.01,
|
| 155 |
+
0.47524,
|
| 156 |
+
0.45486
|
| 157 |
+
],
|
| 158 |
+
"te": [
|
| 159 |
+
0.45026,
|
| 160 |
+
0.07269,
|
| 161 |
+
0.0576,
|
| 162 |
+
0.46093,
|
| 163 |
+
0.39692
|
| 164 |
+
]
|
| 165 |
+
},
|
| 166 |
+
"dur_prior_default": [
|
| 167 |
+
1.1438125,
|
| 168 |
+
0.06297000000000001,
|
| 169 |
+
0.05539583333333333,
|
| 170 |
+
0.45169916666666676,
|
| 171 |
+
0.35033166666666665
|
| 172 |
+
]
|
| 173 |
+
},
|
| 174 |
+
"sampling": {
|
| 175 |
+
"q_serve": 16,
|
| 176 |
+
"temp": 0.7,
|
| 177 |
+
"top_k": 64,
|
| 178 |
+
"tau": 0.35,
|
| 179 |
+
"hazard": 0.6,
|
| 180 |
+
"cfg_scale": 0.0,
|
| 181 |
+
"aeos_class": 2048
|
| 182 |
+
}
|
| 183 |
+
}
|
assets/v1/depth_frame_fp16.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c49a4db17d5f443371c459d75c4bdbd508abc164ba30caf51f70c36032c1cea4
|
| 3 |
+
size 4507646
|
assets/v1/depth_frame_fp16.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:494c8e441065164eb1aa26100f5c2763b447108a116914036a72e1b124463e61
|
| 3 |
+
size 872850432
|
assets/v1/embed_bf16.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7940ea16f0a59b955ac42a9f5c035930e0aded3e034ca68de18531c45a6ff861
|
| 3 |
+
size 796868608
|
assets/v1/manifest.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 1,
|
| 3 |
+
"files": {
|
| 4 |
+
"tokenizer.json": "26c51f9b772f9084ed6bdeabc44b7c15397c12966bfa3e1c09b025e82f4fdbaf",
|
| 5 |
+
"embed_bf16.bin": "7940ea16f0a59b955ac42a9f5c035930e0aded3e034ca68de18531c45a6ff861",
|
| 6 |
+
"audio_in_bf16.bin": "2670e5583c2d2e6e963a8117136a8d07e0e892d3ffda2620d3f13cdbaddc94d0",
|
| 7 |
+
"prompt.npz": "0d6392e0cba6009a913ba57605641b48b1a1fa32a0040ffd4b7b2f840be8afc5",
|
| 8 |
+
"voices.json": "dac72e40ff1c3863f91ab65d99346965df98967cc40b2a8234b757ae7892b4cf",
|
| 9 |
+
"controls.json": "527f1e3129bf6b16df9ac7e2ec7dc813da6b74a207031cc41d959b6a6337021e",
|
| 10 |
+
"backbone_wo8.onnx": "d0326c5973d9d71708d7c39c3f34851c73addc88bceb46d0ac90c37e9041955d",
|
| 11 |
+
"backbone_wo8.data": "ce9ebfb1e911bac4e3bc58b77c6657f75b79f145f89df718f23d352d824dcb01",
|
| 12 |
+
"depth_frame_fp16.onnx": "c49a4db17d5f443371c459d75c4bdbd508abc164ba30caf51f70c36032c1cea4",
|
| 13 |
+
"depth_frame_fp16.onnx.data": "494c8e441065164eb1aa26100f5c2763b447108a116914036a72e1b124463e61",
|
| 14 |
+
"mimi_decoder.onnx": "a0e6accff030071e49c1e1824b26f3a4868c39d125f11bb8079b4a4bbd9dcb58"
|
| 15 |
+
},
|
| 16 |
+
"shapes": {
|
| 17 |
+
"d_model": 2048,
|
| 18 |
+
"vocab": 194548,
|
| 19 |
+
"audio_rows": 65536,
|
| 20 |
+
"n_q": 16,
|
| 21 |
+
"n_codes": 2048,
|
| 22 |
+
"ecapa_dim": 192,
|
| 23 |
+
"rope_theta": 1000000.0,
|
| 24 |
+
"n_layers": 28,
|
| 25 |
+
"n_heads": 8,
|
| 26 |
+
"head_dim": 128,
|
| 27 |
+
"d_mem": 1024
|
| 28 |
+
}
|
| 29 |
+
}
|
assets/v1/mimi_decoder.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a0e6accff030071e49c1e1824b26f3a4868c39d125f11bb8079b4a4bbd9dcb58
|
| 3 |
+
size 194722647
|
assets/v1/prompt.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0d6392e0cba6009a913ba57605641b48b1a1fa32a0040ffd4b7b2f840be8afc5
|
| 3 |
+
size 4616734
|
assets/v1/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:26c51f9b772f9084ed6bdeabc44b7c15397c12966bfa3e1c09b025e82f4fdbaf
|
| 3 |
+
size 16103051
|
assets/v1/voices.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Ahana": {
|
| 3 |
+
"mode": "named",
|
| 4 |
+
"use_icl": false,
|
| 5 |
+
"use_mem": false
|
| 6 |
+
},
|
| 7 |
+
"Vaidehi": {
|
| 8 |
+
"mode": "named",
|
| 9 |
+
"use_icl": false,
|
| 10 |
+
"use_mem": false
|
| 11 |
+
},
|
| 12 |
+
"Malini": {
|
| 13 |
+
"mode": "named",
|
| 14 |
+
"use_icl": false,
|
| 15 |
+
"use_mem": false
|
| 16 |
+
},
|
| 17 |
+
"Charvi": {
|
| 18 |
+
"mode": "named",
|
| 19 |
+
"use_icl": false,
|
| 20 |
+
"use_mem": false
|
| 21 |
+
},
|
| 22 |
+
"Jahnavi": {
|
| 23 |
+
"mode": "named",
|
| 24 |
+
"use_icl": false,
|
| 25 |
+
"use_mem": false
|
| 26 |
+
},
|
| 27 |
+
"Anirban": {
|
| 28 |
+
"mode": "named",
|
| 29 |
+
"use_icl": false,
|
| 30 |
+
"use_mem": false
|
| 31 |
+
},
|
| 32 |
+
"Nakul": {
|
| 33 |
+
"mode": "named",
|
| 34 |
+
"use_icl": false,
|
| 35 |
+
"use_mem": false
|
| 36 |
+
},
|
| 37 |
+
"Vasant": {
|
| 38 |
+
"mode": "named",
|
| 39 |
+
"use_icl": false,
|
| 40 |
+
"use_mem": false
|
| 41 |
+
},
|
| 42 |
+
"Siddhant": {
|
| 43 |
+
"mode": "named",
|
| 44 |
+
"use_icl": false,
|
| 45 |
+
"use_mem": false
|
| 46 |
+
},
|
| 47 |
+
"Satvik": {
|
| 48 |
+
"mode": "named",
|
| 49 |
+
"use_icl": false,
|
| 50 |
+
"use_mem": false
|
| 51 |
+
},
|
| 52 |
+
"Abhay": {
|
| 53 |
+
"mode": "named",
|
| 54 |
+
"use_icl": false,
|
| 55 |
+
"use_mem": false
|
| 56 |
+
},
|
| 57 |
+
"Nivedita": {
|
| 58 |
+
"mode": "named",
|
| 59 |
+
"use_icl": false,
|
| 60 |
+
"use_mem": false
|
| 61 |
+
},
|
| 62 |
+
"Ishwari": {
|
| 63 |
+
"mode": "named",
|
| 64 |
+
"use_icl": false,
|
| 65 |
+
"use_mem": false
|
| 66 |
+
},
|
| 67 |
+
"Ritvik": {
|
| 68 |
+
"mode": "named",
|
| 69 |
+
"use_icl": false,
|
| 70 |
+
"use_mem": false
|
| 71 |
+
},
|
| 72 |
+
"Vrinda": {
|
| 73 |
+
"mode": "named",
|
| 74 |
+
"use_icl": false,
|
| 75 |
+
"use_mem": false
|
| 76 |
+
},
|
| 77 |
+
"Sreekar": {
|
| 78 |
+
"mode": "named",
|
| 79 |
+
"use_icl": false,
|
| 80 |
+
"use_mem": false
|
| 81 |
+
},
|
| 82 |
+
"Kayal": {
|
| 83 |
+
"mode": "named",
|
| 84 |
+
"use_icl": false,
|
| 85 |
+
"use_mem": false
|
| 86 |
+
},
|
| 87 |
+
"Shaunak": {
|
| 88 |
+
"mode": "named",
|
| 89 |
+
"use_icl": false,
|
| 90 |
+
"use_mem": false
|
| 91 |
+
},
|
| 92 |
+
"Akshat": {
|
| 93 |
+
"mode": "named",
|
| 94 |
+
"use_icl": false,
|
| 95 |
+
"use_mem": false
|
| 96 |
+
},
|
| 97 |
+
"Tanushree": {
|
| 98 |
+
"mode": "named",
|
| 99 |
+
"use_icl": false,
|
| 100 |
+
"use_mem": false
|
| 101 |
+
}
|
| 102 |
+
}
|
requirements-fast.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=2.0
|
| 2 |
+
onnxruntime==1.31.*
|
| 3 |
+
tokenizers>=0.22,<0.23
|
| 4 |
+
psutil
|
| 5 |
+
onnx
|
src/maya_fast/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .engine import MayaFast
|
| 4 |
+
from .assets import PromptAssets, load_manifest, AssetError
|
| 5 |
+
from .backbone import Backbone
|
| 6 |
+
from .depth import Depth
|
| 7 |
+
from .codec import MimiDecoder
|
| 8 |
+
from .prompt import AudioIn, Prompt, build_prompt
|
| 9 |
+
from .control import StopPolicy, sample_cb0
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"MayaFast",
|
| 13 |
+
"PromptAssets",
|
| 14 |
+
"load_manifest",
|
| 15 |
+
"AssetError",
|
| 16 |
+
"Backbone",
|
| 17 |
+
"Depth",
|
| 18 |
+
"MimiDecoder",
|
| 19 |
+
"AudioIn",
|
| 20 |
+
"Prompt",
|
| 21 |
+
"build_prompt",
|
| 22 |
+
"StopPolicy",
|
| 23 |
+
"sample_cb0",
|
| 24 |
+
]
|
src/maya_fast/__main__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import sys
|
| 5 |
+
import wave
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from .engine import MayaFast
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main() -> None:
|
| 13 |
+
parser = argparse.ArgumentParser(description="Maya-Fast TTS CLI")
|
| 14 |
+
parser.add_argument("text", type=str, help="Text to speak")
|
| 15 |
+
parser.add_argument("-o", "--out", type=str, default="out.wav", help="Output WAV path")
|
| 16 |
+
parser.add_argument("--voice", type=str, default="Charvi", help="Voice name (default: Charvi)")
|
| 17 |
+
parser.add_argument("--seed", type=int, default=0, help="Random seed (default: 0)")
|
| 18 |
+
parser.add_argument("--lang", type=str, default="mr", help="Language tag: mr, hi, ... (default: mr)")
|
| 19 |
+
parser.add_argument("--threads", type=int, default=None, help="ORT threads (default: min(8, cores))")
|
| 20 |
+
parser.add_argument("--assets", type=str, default=None, help="Assets directory")
|
| 21 |
+
|
| 22 |
+
args = parser.parse_args()
|
| 23 |
+
|
| 24 |
+
engine = MayaFast(assets_dir=args.assets, threads=args.threads)
|
| 25 |
+
wav, metrics = engine.synthesize(args.text, voice=args.voice, seed=args.seed, lang=args.lang)
|
| 26 |
+
|
| 27 |
+
# Convert float32 [-1.0, 1.0] to 16-bit PCM WAV using stdlib wave
|
| 28 |
+
wav_clipped = np.clip(wav, -1.0, 1.0)
|
| 29 |
+
pcm16 = (wav_clipped * 32767.0).astype(np.int16)
|
| 30 |
+
|
| 31 |
+
out_path = Path(args.out)
|
| 32 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
with wave.open(str(out_path), "wb") as wf:
|
| 34 |
+
wf.setnchannels(1)
|
| 35 |
+
wf.setsampwidth(2)
|
| 36 |
+
wf.setframerate(24000)
|
| 37 |
+
wf.writeframes(pcm16.tobytes())
|
| 38 |
+
|
| 39 |
+
print(
|
| 40 |
+
f"audio_s={metrics['audio_s']:.2f} "
|
| 41 |
+
f"wall_s={metrics['wall_s']:.2f} "
|
| 42 |
+
f"rtf={metrics['rtf']:.2f} "
|
| 43 |
+
f"peak_rss_mb={metrics['peak_rss_mb']:.1f}"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
main()
|
src/maya_fast/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (721 Bytes). View file
|
|
|
src/maya_fast/__pycache__/__main__.cpython-313.pyc
ADDED
|
Binary file (3.08 kB). View file
|
|
|
src/maya_fast/__pycache__/assets.cpython-313.pyc
ADDED
|
Binary file (5.74 kB). View file
|
|
|
src/maya_fast/__pycache__/backbone.cpython-313.pyc
ADDED
|
Binary file (8.59 kB). View file
|
|
|
src/maya_fast/__pycache__/codec.cpython-313.pyc
ADDED
|
Binary file (2.1 kB). View file
|
|
|
src/maya_fast/__pycache__/control.cpython-313.pyc
ADDED
|
Binary file (8.36 kB). View file
|
|
|
src/maya_fast/__pycache__/depth.cpython-313.pyc
ADDED
|
Binary file (2.92 kB). View file
|
|
|
src/maya_fast/__pycache__/embed.cpython-313.pyc
ADDED
|
Binary file (1.94 kB). View file
|
|
|
src/maya_fast/__pycache__/engine.cpython-313.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
src/maya_fast/__pycache__/ort_opts.cpython-313.pyc
ADDED
|
Binary file (1.13 kB). View file
|
|
|
src/maya_fast/__pycache__/prompt.cpython-313.pyc
ADDED
|
Binary file (7.62 kB). View file
|
|
|
src/maya_fast/__pycache__/sampling.cpython-313.pyc
ADDED
|
Binary file (2.07 kB). View file
|
|
|
src/maya_fast/assets.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AssetError(Exception):
|
| 12 |
+
pass
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def default_work_dir() -> Path:
|
| 16 |
+
env = os.environ.get("MAYA_FAST_WORK")
|
| 17 |
+
if env:
|
| 18 |
+
return Path(env)
|
| 19 |
+
return Path.home() / ".cache" / "maya_fast"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def default_assets_dir() -> Path:
|
| 23 |
+
return default_work_dir() / "assets" / "v1"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def compute_file_sha256(path: Path) -> str:
|
| 27 |
+
h = hashlib.sha256()
|
| 28 |
+
with open(path, "rb") as f:
|
| 29 |
+
while chunk := f.read(65536):
|
| 30 |
+
h.update(chunk)
|
| 31 |
+
return h.hexdigest()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def load_manifest(assets_dir: str | Path, verify: bool = True) -> dict[str, Any]:
|
| 35 |
+
assets_dir = Path(assets_dir)
|
| 36 |
+
manifest_file = assets_dir / "manifest.json"
|
| 37 |
+
if not manifest_file.exists():
|
| 38 |
+
raise AssetError(f"manifest.json missing in {assets_dir}")
|
| 39 |
+
|
| 40 |
+
with open(manifest_file, "r", encoding="utf-8") as f:
|
| 41 |
+
try:
|
| 42 |
+
manifest = json.load(f)
|
| 43 |
+
except Exception as e:
|
| 44 |
+
raise AssetError(f"Corrupt manifest.json in {assets_dir}: {e}")
|
| 45 |
+
|
| 46 |
+
if verify:
|
| 47 |
+
files = manifest.get("files", {})
|
| 48 |
+
for name, expected_sha in files.items():
|
| 49 |
+
fpath = assets_dir / name
|
| 50 |
+
if not fpath.exists():
|
| 51 |
+
raise AssetError(f"Required asset file missing: {name}")
|
| 52 |
+
actual_sha = compute_file_sha256(fpath)
|
| 53 |
+
if actual_sha != expected_sha:
|
| 54 |
+
raise AssetError(f"Asset sha256 mismatch for file: {name}")
|
| 55 |
+
|
| 56 |
+
return manifest
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class PromptAssets:
|
| 60 |
+
def __init__(self, assets_dir: str | Path, verify: bool = True) -> None:
|
| 61 |
+
self.assets_dir = Path(assets_dir)
|
| 62 |
+
self.manifest = load_manifest(self.assets_dir, verify=verify)
|
| 63 |
+
self.shapes = self.manifest.get("shapes", {})
|
| 64 |
+
|
| 65 |
+
from .embed import EmbeddingTable
|
| 66 |
+
from .prompt import AudioIn
|
| 67 |
+
|
| 68 |
+
d_model = self.shapes.get("d_model", 2048)
|
| 69 |
+
vocab = self.shapes.get("vocab", 151936)
|
| 70 |
+
n_q = self.shapes.get("n_q", 16)
|
| 71 |
+
n_codes = self.shapes.get("n_codes", 2048)
|
| 72 |
+
self.embed_table = EmbeddingTable(self.assets_dir / "embed_bf16.bin", rows=vocab, dim=d_model)
|
| 73 |
+
audio_table = EmbeddingTable(self.assets_dir / "audio_in_bf16.bin", rows=n_q * n_codes, dim=d_model)
|
| 74 |
+
|
| 75 |
+
# Controls (text_frames_prior, guard, sampling, text)
|
| 76 |
+
with open(self.assets_dir / "controls.json", "r", encoding="utf-8") as f:
|
| 77 |
+
self.controls = json.load(f)
|
| 78 |
+
|
| 79 |
+
# Speaker anchor weights and audio input norm/aeos (fp32; the audio table is bf16 on disk)
|
| 80 |
+
prompt_npz = np.load(self.assets_dir / "prompt.npz")
|
| 81 |
+
self.prompt_params = {k: prompt_npz[k] for k in prompt_npz.files}
|
| 82 |
+
self.audio_in = AudioIn(audio_table, self.prompt_params["audio_in/aeos_in"],
|
| 83 |
+
self.prompt_params["audio_in/norm"], n_q=n_q)
|
| 84 |
+
|
| 85 |
+
# Voices: name -> {mode, use_icl, use_mem}
|
| 86 |
+
with open(self.assets_dir / "voices.json", "r", encoding="utf-8") as f:
|
| 87 |
+
self.voices_info = json.load(f)
|
| 88 |
+
|
| 89 |
+
# Tokenizer (extended upstream tokenizer, fast format)
|
| 90 |
+
from tokenizers import Tokenizer
|
| 91 |
+
self.tokenizer = Tokenizer.from_file(str(self.assets_dir / "tokenizer.json"))
|
src/maya_fast/backbone.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import numpy as np
|
| 6 |
+
import onnxruntime as ort
|
| 7 |
+
|
| 8 |
+
from .ort_opts import session_options
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Backbone:
|
| 12 |
+
def __init__(
|
| 13 |
+
self,
|
| 14 |
+
path: str | Path,
|
| 15 |
+
threads: int | None = None,
|
| 16 |
+
manifest_path: str | Path | None = None,
|
| 17 |
+
config: dict | None = None,
|
| 18 |
+
) -> None:
|
| 19 |
+
self.path = Path(path)
|
| 20 |
+
self.opts = session_options(threads)
|
| 21 |
+
self.session = ort.InferenceSession(str(self.path), self.opts, providers=["CPUExecutionProvider"])
|
| 22 |
+
|
| 23 |
+
# Determine shapes and hyperparameters
|
| 24 |
+
# Default real Qwen3 / Maya2 backbone parameters:
|
| 25 |
+
# n_layers=28, n_heads=8 (kv heads), head_dim=128, rope_theta=10000.0, d_model=2048
|
| 26 |
+
self.n_layers = 28
|
| 27 |
+
self.n_heads = 8
|
| 28 |
+
self.head_dim = 128
|
| 29 |
+
self.rope_theta = 10000.0
|
| 30 |
+
self.d_model = 2048
|
| 31 |
+
|
| 32 |
+
# If manifest or config provided, override:
|
| 33 |
+
if config is not None:
|
| 34 |
+
self._apply_config(config)
|
| 35 |
+
elif manifest_path is not None and Path(manifest_path).exists():
|
| 36 |
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
| 37 |
+
data = json.load(f)
|
| 38 |
+
shapes = data.get("shapes", {})
|
| 39 |
+
self._apply_config(shapes)
|
| 40 |
+
else:
|
| 41 |
+
# Check if manifest.json exists next to model
|
| 42 |
+
maybe_manifest = self.path.parent / "manifest.json"
|
| 43 |
+
if maybe_manifest.exists():
|
| 44 |
+
with open(maybe_manifest, "r", encoding="utf-8") as f:
|
| 45 |
+
data = json.load(f)
|
| 46 |
+
shapes = data.get("shapes", {})
|
| 47 |
+
self._apply_config(shapes)
|
| 48 |
+
else:
|
| 49 |
+
# Discover n_layers from session inputs
|
| 50 |
+
past_k_inputs = [inp.name for inp in self.session.get_inputs() if inp.name.startswith("past_k_")]
|
| 51 |
+
if past_k_inputs:
|
| 52 |
+
self.n_layers = len(past_k_inputs)
|
| 53 |
+
|
| 54 |
+
# Inspect session input shapes if available
|
| 55 |
+
for inp in self.session.get_inputs():
|
| 56 |
+
if inp.name == "past_k_0":
|
| 57 |
+
shape = inp.shape
|
| 58 |
+
# shape: [1, n_heads, P, head_dim]
|
| 59 |
+
if len(shape) == 4:
|
| 60 |
+
if isinstance(shape[1], int):
|
| 61 |
+
self.n_heads = shape[1]
|
| 62 |
+
if isinstance(shape[3], int):
|
| 63 |
+
self.head_dim = shape[3]
|
| 64 |
+
elif inp.name == "embeds":
|
| 65 |
+
if len(inp.shape) == 3 and isinstance(inp.shape[2], int):
|
| 66 |
+
self.d_model = inp.shape[2]
|
| 67 |
+
|
| 68 |
+
self.reset()
|
| 69 |
+
|
| 70 |
+
def _apply_config(self, cfg: dict) -> None:
|
| 71 |
+
if "n_layers" in cfg:
|
| 72 |
+
self.n_layers = int(cfg["n_layers"])
|
| 73 |
+
if "n_heads" in cfg:
|
| 74 |
+
self.n_heads = int(cfg["n_heads"])
|
| 75 |
+
if "head_dim" in cfg:
|
| 76 |
+
self.head_dim = int(cfg["head_dim"])
|
| 77 |
+
if "rope_theta" in cfg:
|
| 78 |
+
self.rope_theta = float(cfg["rope_theta"])
|
| 79 |
+
if "d_model" in cfg:
|
| 80 |
+
self.d_model = int(cfg["d_model"])
|
| 81 |
+
|
| 82 |
+
def reset(self) -> None:
|
| 83 |
+
self.past_len = 0
|
| 84 |
+
self.past_k = [np.zeros((1, self.n_heads, 0, self.head_dim), dtype=np.float32) for _ in range(self.n_layers)]
|
| 85 |
+
self.past_v = [np.zeros((1, self.n_heads, 0, self.head_dim), dtype=np.float32) for _ in range(self.n_layers)]
|
| 86 |
+
|
| 87 |
+
def _compute_rope(self, T: int, past: int) -> tuple[np.ndarray, np.ndarray]:
|
| 88 |
+
# RoPE for positions [past, past + T)
|
| 89 |
+
# dim = head_dim
|
| 90 |
+
pos = np.arange(past, past + T, dtype=np.float32)[:, None] # (T, 1)
|
| 91 |
+
dim_indices = np.arange(0, self.head_dim, 2, dtype=np.float32) # (head_dim/2,)
|
| 92 |
+
inv_freq = 1.0 / (self.rope_theta ** (dim_indices / self.head_dim)) # (head_dim/2,)
|
| 93 |
+
sinusoid = pos * inv_freq[None, :] # (T, head_dim/2)
|
| 94 |
+
# duplicate across second half to match typical rotary implementation: [sinusoid, sinusoid]
|
| 95 |
+
sinusoid = np.concatenate([sinusoid, sinusoid], axis=-1) # (T, head_dim)
|
| 96 |
+
cos = np.cos(sinusoid)[None, :, :] # (1, T, head_dim)
|
| 97 |
+
sin = np.sin(sinusoid)[None, :, :] # (1, T, head_dim)
|
| 98 |
+
return cos.astype(np.float32), sin.astype(np.float32)
|
| 99 |
+
|
| 100 |
+
def _compute_mask(self, T: int, past: int) -> np.ndarray:
|
| 101 |
+
# Additive causal mask (1, 1, T, past+T): query i sees keys j <= past + i
|
| 102 |
+
total_len = past + T
|
| 103 |
+
i = np.arange(T)[:, None]
|
| 104 |
+
j = np.arange(total_len)[None, :]
|
| 105 |
+
mask = np.zeros((T, total_len), dtype=np.float32)
|
| 106 |
+
mask[j > (past + i)] = -np.inf
|
| 107 |
+
return mask[None, None, :, :]
|
| 108 |
+
|
| 109 |
+
def step(self, embeds: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
| 110 |
+
"""
|
| 111 |
+
embeds: (T, d_model) or (1, T, d_model)
|
| 112 |
+
Returns:
|
| 113 |
+
hidden: (d_model,) last position
|
| 114 |
+
cb0_logits: (vocab + 1,) last position
|
| 115 |
+
"""
|
| 116 |
+
if embeds.ndim == 2:
|
| 117 |
+
embeds = embeds[None, :, :]
|
| 118 |
+
elif embeds.ndim != 3:
|
| 119 |
+
raise ValueError(f"Expected embeds to have 2 or 3 dims, got shape {embeds.shape}")
|
| 120 |
+
|
| 121 |
+
embeds = embeds.astype(np.float32)
|
| 122 |
+
T = embeds.shape[1]
|
| 123 |
+
P = self.past_len
|
| 124 |
+
|
| 125 |
+
cos, sin = self._compute_rope(T, P)
|
| 126 |
+
mask = self._compute_mask(T, P)
|
| 127 |
+
|
| 128 |
+
feed = {
|
| 129 |
+
"embeds": embeds,
|
| 130 |
+
"cos": cos,
|
| 131 |
+
"sin": sin,
|
| 132 |
+
"mask": mask,
|
| 133 |
+
}
|
| 134 |
+
for i in range(self.n_layers):
|
| 135 |
+
feed[f"past_k_{i}"] = self.past_k[i]
|
| 136 |
+
feed[f"past_v_{i}"] = self.past_v[i]
|
| 137 |
+
|
| 138 |
+
outputs = self.session.run(None, feed)
|
| 139 |
+
|
| 140 |
+
# Output ordering from session:
|
| 141 |
+
# output[0] = hidden (1, T, d_model)
|
| 142 |
+
# output[1] = cb0_logits (1, T, vocab + 1)
|
| 143 |
+
# output[2..] = present_k_i, present_v_i
|
| 144 |
+
hidden_out = outputs[0]
|
| 145 |
+
cb0_logits_out = outputs[1]
|
| 146 |
+
|
| 147 |
+
idx = 2
|
| 148 |
+
for i in range(self.n_layers):
|
| 149 |
+
self.past_k[i] = outputs[idx]
|
| 150 |
+
self.past_v[i] = outputs[idx + 1]
|
| 151 |
+
idx += 2
|
| 152 |
+
|
| 153 |
+
self.past_len = P + T
|
| 154 |
+
|
| 155 |
+
last_hidden = hidden_out[0, -1, :]
|
| 156 |
+
last_cb0_logits = cb0_logits_out[0, -1, :]
|
| 157 |
+
return last_hidden, last_cb0_logits
|
src/maya_fast/codec.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import onnxruntime as ort
|
| 6 |
+
|
| 7 |
+
from .ort_opts import session_options
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class MimiDecoder:
|
| 11 |
+
def __init__(self, path: str | Path, threads: int | None = None) -> None:
|
| 12 |
+
self.path = Path(path)
|
| 13 |
+
self.opts = session_options(threads)
|
| 14 |
+
self.session = ort.InferenceSession(str(self.path), self.opts, providers=["CPUExecutionProvider"])
|
| 15 |
+
|
| 16 |
+
def decode(self, codes: np.ndarray) -> np.ndarray:
|
| 17 |
+
"""
|
| 18 |
+
Input:
|
| 19 |
+
codes: (16, F) int64
|
| 20 |
+
Returns:
|
| 21 |
+
wav: (F * 1920,) float32
|
| 22 |
+
"""
|
| 23 |
+
codes = np.asarray(codes, dtype=np.int64)
|
| 24 |
+
if codes.ndim == 2:
|
| 25 |
+
codes_input = codes[None, :, :] # (1, 16, F)
|
| 26 |
+
elif codes.ndim == 3:
|
| 27 |
+
codes_input = codes
|
| 28 |
+
else:
|
| 29 |
+
raise ValueError(f"Expected codes to have 2 or 3 dims, got {codes.shape}")
|
| 30 |
+
|
| 31 |
+
outputs = self.session.run(None, {"codes": codes_input})
|
| 32 |
+
wav = outputs[0] # (1, 1, samples)
|
| 33 |
+
wav = np.squeeze(wav)
|
| 34 |
+
return wav.astype(np.float32)
|
src/maya_fast/control.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
# Port of maya3.guards (DurationPrior, text_feats) and scripts/s2_serve_controls.py (expected_frames, split_text).
|
| 8 |
+
# The numbers come from controls.json, written by tools/maya_export/export_prompt_assets.py from upstream.
|
| 9 |
+
PAUSE = re.compile(r"[,;:.!?।॥…\-]")
|
| 10 |
+
PRIOR_DEFAULT = {"a": 2.0, "b": 0.72, "resid_p99": 90.0} # scripts/s2_serve_controls.py
|
| 11 |
+
FRAME_S = 0.08 # maya3.guards.FRAME_S (one frame = 80 ms of audio)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def text_feats(t: str) -> np.ndarray:
|
| 15 |
+
letters = sum(1 for ch in t if ch.isalpha() or (0x900 <= ord(ch) <= 0xDFF))
|
| 16 |
+
latin = sum(1 for ch in t if ch.isascii() and ch.isalpha())
|
| 17 |
+
return np.array([1.0, letters - latin, latin, len(PAUSE.findall(t)), sum(ch.isdigit() for ch in t)])
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def expected_frames(prior: dict, lang: str, nchar: int) -> tuple[float, float]:
|
| 21 |
+
p = prior.get(lang, PRIOR_DEFAULT)
|
| 22 |
+
return p["a"] + p["b"] * nchar, p["resid_p99"]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def min_frames(ef: float) -> int:
|
| 26 |
+
return max(4, int(0.5 * ef))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def expected_seconds(text: str, lang: str, guard: dict) -> float:
|
| 30 |
+
coefs = guard["dur_prior_coefs"]
|
| 31 |
+
coef = np.asarray(coefs.get(lang, guard["dur_prior_default"]), dtype=np.float64)
|
| 32 |
+
return float(max(0.35, text_feats(text) @ coef))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def frame_cap(text: str, lang: str, ef: float, er: float, controls: dict) -> int:
|
| 36 |
+
"""Duration cap as in tools/maya_export/golden_light.py: min(loose, max(0.5*ef + 8, duration-prior cap))."""
|
| 37 |
+
guard = controls["guard"]
|
| 38 |
+
loose = min(int(guard["hard_cap"]), int(2.0 * (ef + er) + 40))
|
| 39 |
+
if not guard["enabled"]:
|
| 40 |
+
return loose
|
| 41 |
+
dur_cap = int((float(guard["dur_k"]) * expected_seconds(text, lang, guard)
|
| 42 |
+
+ float(guard["dur_margin_s"])) / FRAME_S)
|
| 43 |
+
return int(min(loose, max(int(ef * 0.5) + 8, dur_cap)))
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def split_text(text: str, lang: str, prior: dict, chunk_s: float = 12.0, *, fps: float = 12.5,
|
| 47 |
+
terminals: str = "।.!?;।॥") -> list[str]:
|
| 48 |
+
"""Split at sentence terminals into pieces of about chunk_s seconds (prior-estimated).
|
| 49 |
+
Falls back to commas, then whitespace, for run-on text."""
|
| 50 |
+
if chunk_s <= 0:
|
| 51 |
+
return [text]
|
| 52 |
+
target = chunk_s * fps
|
| 53 |
+
pieces = [p.strip() for p in re.split(rf"(?<=[{re.escape(terminals)}])\s+", text) if p.strip()]
|
| 54 |
+
out: list[str] = []
|
| 55 |
+
cur = ""
|
| 56 |
+
for p in pieces:
|
| 57 |
+
cand = (cur + " " + p).strip() if cur else p
|
| 58 |
+
if cur and expected_frames(prior, lang, len(cand))[0] > target:
|
| 59 |
+
out.append(cur)
|
| 60 |
+
cur = p
|
| 61 |
+
else:
|
| 62 |
+
cur = cand
|
| 63 |
+
if cur:
|
| 64 |
+
out.append(cur)
|
| 65 |
+
|
| 66 |
+
final: list[str] = []
|
| 67 |
+
for piece in out:
|
| 68 |
+
if expected_frames(prior, lang, len(piece))[0] <= target * 1.3:
|
| 69 |
+
final.append(piece)
|
| 70 |
+
continue
|
| 71 |
+
words = re.split(r"(?<=[,،])\s+|\s+", piece)
|
| 72 |
+
cur = ""
|
| 73 |
+
for w in words:
|
| 74 |
+
cand = (cur + " " + w).strip() if cur else w
|
| 75 |
+
if cur and expected_frames(prior, lang, len(cand))[0] > target:
|
| 76 |
+
final.append(cur)
|
| 77 |
+
cur = w
|
| 78 |
+
else:
|
| 79 |
+
cur = cand
|
| 80 |
+
if cur:
|
| 81 |
+
final.append(cur)
|
| 82 |
+
return final
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class StopPolicy:
|
| 86 |
+
"""The "controlled" stop rule of ServeSession.speak: argmax, tau and hazard tests, none before min_frames."""
|
| 87 |
+
|
| 88 |
+
def __init__(self, min_frames: int, tau: float = 0.35, hazard: float = 0.6) -> None:
|
| 89 |
+
self.min_frames = int(min_frames)
|
| 90 |
+
self.tau = float(tau)
|
| 91 |
+
self.hazard = float(hazard)
|
| 92 |
+
self.H = 0.0
|
| 93 |
+
|
| 94 |
+
def check(self, t: int, p: np.ndarray, aeos_class: int = 2048) -> str | None:
|
| 95 |
+
"""
|
| 96 |
+
p: (vocab + 1,) probability distribution over cb0 classes.
|
| 97 |
+
Returns: 'aeos_argmax', 'aeos_tau', 'aeos_hazard', or None
|
| 98 |
+
"""
|
| 99 |
+
if t < self.min_frames:
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
p_aeos = float(p[aeos_class])
|
| 103 |
+
self.H = 1.0 - (1.0 - self.H) * (1.0 - p_aeos)
|
| 104 |
+
|
| 105 |
+
if int(np.argmax(p)) == aeos_class:
|
| 106 |
+
return "aeos_argmax"
|
| 107 |
+
if p_aeos >= self.tau:
|
| 108 |
+
return "aeos_tau"
|
| 109 |
+
if self.H >= self.hazard:
|
| 110 |
+
return "aeos_hazard"
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def sample_cb0(p: np.ndarray, noise: np.ndarray, aeos_class: int = 2048) -> int:
|
| 115 |
+
"""
|
| 116 |
+
aeos zeroed and renormalised, then Gumbel-max over the rest (same distribution as upstream multinomial).
|
| 117 |
+
"""
|
| 118 |
+
q = np.array(p, dtype=np.float32, copy=True)
|
| 119 |
+
q[aeos_class] = 0.0
|
| 120 |
+
s = np.sum(q)
|
| 121 |
+
if s > 0:
|
| 122 |
+
q = q / s
|
| 123 |
+
else:
|
| 124 |
+
# Uniform over classes except aeos
|
| 125 |
+
q = np.ones_like(q)
|
| 126 |
+
q[aeos_class] = 0.0
|
| 127 |
+
q = q / np.sum(q)
|
| 128 |
+
|
| 129 |
+
# Gumbel-max on probabilities q: argmax(log(q) + noise)
|
| 130 |
+
# where q == 0, log(q) = -inf
|
| 131 |
+
with np.errstate(divide="ignore"):
|
| 132 |
+
log_q = np.log(q)
|
| 133 |
+
perturbed = log_q + noise
|
| 134 |
+
perturbed[aeos_class] = -np.inf
|
| 135 |
+
return int(np.argmax(perturbed))
|
src/maya_fast/depth.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import onnxruntime as ort
|
| 6 |
+
|
| 7 |
+
from .ort_opts import session_options
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Depth:
|
| 11 |
+
def __init__(self, path: str | Path, threads: int | None = None) -> None:
|
| 12 |
+
self.path = Path(path)
|
| 13 |
+
self.opts = session_options(threads)
|
| 14 |
+
self.session = ort.InferenceSession(str(self.path), self.opts, providers=["CPUExecutionProvider"])
|
| 15 |
+
|
| 16 |
+
def frame(
|
| 17 |
+
self,
|
| 18 |
+
h: np.ndarray,
|
| 19 |
+
cb0: int | np.integer | np.ndarray,
|
| 20 |
+
mem: np.ndarray,
|
| 21 |
+
noise: np.ndarray,
|
| 22 |
+
) -> np.ndarray:
|
| 23 |
+
"""
|
| 24 |
+
Inputs:
|
| 25 |
+
h: (d_model,) or (1, d_model) float32
|
| 26 |
+
cb0: int or (1,) int64
|
| 27 |
+
mem: (1, R, d_mem) float32
|
| 28 |
+
noise: (15, vocab) float32
|
| 29 |
+
Returns:
|
| 30 |
+
codes: (16,) int64
|
| 31 |
+
"""
|
| 32 |
+
if h.ndim == 1:
|
| 33 |
+
h = h[None, :]
|
| 34 |
+
h = h.astype(np.float32)
|
| 35 |
+
|
| 36 |
+
if isinstance(cb0, (int, np.integer)):
|
| 37 |
+
cb0 = np.array([cb0], dtype=np.int64)
|
| 38 |
+
else:
|
| 39 |
+
cb0 = np.asarray(cb0, dtype=np.int64)
|
| 40 |
+
if cb0.ndim == 0:
|
| 41 |
+
cb0 = cb0.reshape(1)
|
| 42 |
+
|
| 43 |
+
mem = np.asarray(mem, dtype=np.float32)
|
| 44 |
+
if mem.ndim == 2:
|
| 45 |
+
mem = mem[None, :, :]
|
| 46 |
+
|
| 47 |
+
noise = np.asarray(noise, dtype=np.float32)
|
| 48 |
+
if noise.ndim == 2: # (15, n_codes) -> (1, 15, n_codes)
|
| 49 |
+
noise = noise[None]
|
| 50 |
+
|
| 51 |
+
feed = {
|
| 52 |
+
"h": h,
|
| 53 |
+
"cb0": cb0,
|
| 54 |
+
"mem": mem,
|
| 55 |
+
"noise": noise,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
outputs = self.session.run(None, feed)
|
| 59 |
+
codes = outputs[0]
|
| 60 |
+
if codes.ndim > 1:
|
| 61 |
+
codes = codes.squeeze()
|
| 62 |
+
return codes.astype(np.int64)
|
src/maya_fast/embed.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class EmbeddingTable:
|
| 8 |
+
"""
|
| 9 |
+
bf16 memory map embedding lookup.
|
| 10 |
+
Lookup(ids) returns float32 (len(ids), dim).
|
| 11 |
+
Exact conversion: (memmap[ids].astype(np.uint32) << 16).view(np.float32)
|
| 12 |
+
"""
|
| 13 |
+
def __init__(self, path: str | Path, rows: int, dim: int) -> None:
|
| 14 |
+
self.path = Path(path)
|
| 15 |
+
self.rows = int(rows)
|
| 16 |
+
self.dim = int(dim)
|
| 17 |
+
# raw little-endian uint16
|
| 18 |
+
self.mmap = np.memmap(self.path, dtype="<u2", mode="r", shape=(self.rows, self.dim))
|
| 19 |
+
|
| 20 |
+
def lookup(self, ids: np.ndarray | list[int]) -> np.ndarray:
|
| 21 |
+
ids_arr = np.asarray(ids, dtype=np.int64)
|
| 22 |
+
bf16_rows = self.mmap[ids_arr] # (..., dim) uint16
|
| 23 |
+
u32 = bf16_rows.astype(np.uint32) << 16
|
| 24 |
+
fp32 = u32.view(np.float32)
|
| 25 |
+
return np.ascontiguousarray(fp32)
|
src/maya_fast/engine.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
import numpy as np
|
| 8 |
+
import psutil
|
| 9 |
+
|
| 10 |
+
from .assets import PromptAssets, default_assets_dir
|
| 11 |
+
from .backbone import Backbone
|
| 12 |
+
from .depth import Depth
|
| 13 |
+
from .codec import MimiDecoder
|
| 14 |
+
from .prompt import AudioIn, build_prompt
|
| 15 |
+
from .sampling import gumbel
|
| 16 |
+
from .control import (
|
| 17 |
+
expected_frames,
|
| 18 |
+
frame_cap,
|
| 19 |
+
min_frames,
|
| 20 |
+
split_text,
|
| 21 |
+
StopPolicy,
|
| 22 |
+
sample_cb0,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Speakable text check: must contain at least one alphanumeric character
|
| 26 |
+
ALPHANUM_PATTERN = re.compile(r"[\w\d]", re.UNICODE)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _softmax_topk(logits: np.ndarray, temp: float = 0.7, top_k: int = 64) -> np.ndarray:
|
| 30 |
+
logits = np.asarray(logits, dtype=np.float32)
|
| 31 |
+
k = min(top_k, logits.shape[-1])
|
| 32 |
+
topk_idx = np.argpartition(logits, -k)[-k:]
|
| 33 |
+
masked = np.full_like(logits, -np.inf)
|
| 34 |
+
masked[topk_idx] = logits[topk_idx] / temp
|
| 35 |
+
max_val = np.max(masked[topk_idx])
|
| 36 |
+
exp_vals = np.exp(masked - max_val)
|
| 37 |
+
exp_vals[masked == -np.inf] = 0.0
|
| 38 |
+
sum_exp = np.sum(exp_vals)
|
| 39 |
+
if sum_exp > 0:
|
| 40 |
+
return exp_vals / sum_exp
|
| 41 |
+
probs = np.zeros_like(logits)
|
| 42 |
+
probs[topk_idx] = 1.0 / len(topk_idx)
|
| 43 |
+
return probs
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class MayaFast:
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
assets_dir: str | Path | None = None,
|
| 50 |
+
threads: int | None = None,
|
| 51 |
+
) -> None:
|
| 52 |
+
if assets_dir is None:
|
| 53 |
+
self.assets_dir = default_assets_dir()
|
| 54 |
+
else:
|
| 55 |
+
self.assets_dir = Path(assets_dir)
|
| 56 |
+
|
| 57 |
+
self.threads = threads
|
| 58 |
+
self.assets = PromptAssets(self.assets_dir, verify=True)
|
| 59 |
+
self.shapes = self.assets.shapes
|
| 60 |
+
self.controls = self.assets.controls
|
| 61 |
+
self.sampling_cfg = self.controls.get("sampling", {})
|
| 62 |
+
|
| 63 |
+
# Load models
|
| 64 |
+
backbone_path = self.assets_dir / "backbone_wo8.onnx"
|
| 65 |
+
self.backbone = Backbone(
|
| 66 |
+
backbone_path,
|
| 67 |
+
threads=self.threads,
|
| 68 |
+
config=self.shapes,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
depth_path = self.assets_dir / "depth_frame_fp16.onnx"
|
| 72 |
+
self.depth = Depth(depth_path, threads=self.threads)
|
| 73 |
+
|
| 74 |
+
mimi_path = self.assets_dir / "mimi_decoder.onnx"
|
| 75 |
+
self.mimi = MimiDecoder(mimi_path, threads=self.threads)
|
| 76 |
+
|
| 77 |
+
self.audio_in = self.assets.audio_in
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def voices(self) -> list[str]:
|
| 81 |
+
return sorted(list(self.assets.voices_info.keys()))
|
| 82 |
+
|
| 83 |
+
def synthesize(
|
| 84 |
+
self,
|
| 85 |
+
text: str,
|
| 86 |
+
voice: str = "Charvi",
|
| 87 |
+
seed: int = 0,
|
| 88 |
+
chunk_s: float | None = None,
|
| 89 |
+
lang: str = "mr",
|
| 90 |
+
) -> tuple[np.ndarray, dict[str, Any]]:
|
| 91 |
+
"""
|
| 92 |
+
Synthesize text into 24 kHz float32 audio.
|
| 93 |
+
Returns:
|
| 94 |
+
(wav, metrics)
|
| 95 |
+
"""
|
| 96 |
+
# Review focus #2: Empty or punctuation-only text: expected ValueError("no speakable text")
|
| 97 |
+
if not text or not ALPHANUM_PATTERN.search(text):
|
| 98 |
+
raise ValueError("no speakable text")
|
| 99 |
+
|
| 100 |
+
if voice not in self.assets.voices_info:
|
| 101 |
+
raise KeyError(f"Unknown voice {voice!r}. Available: {self.voices}")
|
| 102 |
+
|
| 103 |
+
t_wall_start = time.perf_counter()
|
| 104 |
+
prior = self.controls.get("text_frames_prior", {})
|
| 105 |
+
sampling_cfg = self.sampling_cfg
|
| 106 |
+
q_serve = int(sampling_cfg.get("q_serve", 16))
|
| 107 |
+
temp = float(sampling_cfg.get("temp", 0.7))
|
| 108 |
+
top_k = int(sampling_cfg.get("top_k", 64))
|
| 109 |
+
tau = float(sampling_cfg.get("tau", 0.35))
|
| 110 |
+
hazard = float(sampling_cfg.get("hazard", 0.6))
|
| 111 |
+
aeos_class = int(self.shapes.get("n_codes", 2048))
|
| 112 |
+
|
| 113 |
+
# Split text into chunks
|
| 114 |
+
c_s = 12.0 if chunk_s is None else chunk_s
|
| 115 |
+
chunks = split_text(text, lang, prior, chunk_s=c_s)
|
| 116 |
+
|
| 117 |
+
rng = np.random.default_rng(seed)
|
| 118 |
+
|
| 119 |
+
# Speaker memory for Depth: named voices use a null memory (use_mem False)
|
| 120 |
+
d_mem = int(self.shapes.get("d_mem", 1024))
|
| 121 |
+
mem = np.zeros((1, 1, d_mem), dtype=np.float32)
|
| 122 |
+
|
| 123 |
+
gen_codes: list[np.ndarray] = []
|
| 124 |
+
reasons: list[str] = []
|
| 125 |
+
caps: list[int] = []
|
| 126 |
+
max_past_len = 0
|
| 127 |
+
max_chunk_bound = 0
|
| 128 |
+
|
| 129 |
+
backbone_time = 0.0
|
| 130 |
+
depth_time = 0.0
|
| 131 |
+
mimi_time = 0.0
|
| 132 |
+
|
| 133 |
+
for chunk_text in chunks:
|
| 134 |
+
# Review focus #1: Each piece a fresh KV session
|
| 135 |
+
self.backbone.reset()
|
| 136 |
+
|
| 137 |
+
prompt = build_prompt(chunk_text, voice, self.assets, lang=lang)
|
| 138 |
+
|
| 139 |
+
ef, er = expected_frames(prior, lang, len(chunk_text))
|
| 140 |
+
cap = frame_cap(chunk_text, lang, ef, er, self.controls)
|
| 141 |
+
min_f = min_frames(ef)
|
| 142 |
+
caps.append(cap)
|
| 143 |
+
chunk_bound = prompt.n_prompt + cap
|
| 144 |
+
if chunk_bound > max_chunk_bound:
|
| 145 |
+
max_chunk_bound = chunk_bound
|
| 146 |
+
|
| 147 |
+
# Initial backbone step on prompt
|
| 148 |
+
t0 = time.perf_counter()
|
| 149 |
+
h, cb0_logits = self.backbone.step(prompt.embeds)
|
| 150 |
+
backbone_time += time.perf_counter() - t0
|
| 151 |
+
|
| 152 |
+
if self.backbone.past_len > max_past_len:
|
| 153 |
+
max_past_len = self.backbone.past_len
|
| 154 |
+
|
| 155 |
+
stop_policy = StopPolicy(min_frames=min_f, tau=tau, hazard=hazard)
|
| 156 |
+
chunk_frames: list[np.ndarray] = []
|
| 157 |
+
reason = "cap"
|
| 158 |
+
|
| 159 |
+
for t in range(cap):
|
| 160 |
+
# 1. Softmax with top_k mask over cb0_logits
|
| 161 |
+
p = _softmax_topk(cb0_logits, temp=temp, top_k=top_k)
|
| 162 |
+
|
| 163 |
+
# 2. StopPolicy check
|
| 164 |
+
stop_reason = stop_policy.check(t, p, aeos_class=aeos_class)
|
| 165 |
+
if stop_reason is not None:
|
| 166 |
+
reason = stop_reason
|
| 167 |
+
break
|
| 168 |
+
|
| 169 |
+
# 3. Sample cb0 (aeos zeroed, Gumbel-max over the rest)
|
| 170 |
+
cb0_noise = gumbel(rng, (cb0_logits.shape[-1],))
|
| 171 |
+
cb0 = sample_cb0(p, cb0_noise, aeos_class=aeos_class)
|
| 172 |
+
|
| 173 |
+
# 4. Depth frame
|
| 174 |
+
depth_noise = gumbel(rng, (15, int(self.shapes.get("n_codes", 2048))))
|
| 175 |
+
t_d = time.perf_counter()
|
| 176 |
+
frame_codes = self.depth.frame(h, cb0, mem, depth_noise)
|
| 177 |
+
depth_time += time.perf_counter() - t_d
|
| 178 |
+
|
| 179 |
+
chunk_frames.append(frame_codes)
|
| 180 |
+
|
| 181 |
+
# 5. AudioIn feedback
|
| 182 |
+
fb = self.audio_in.frames(frame_codes[None, :], q_use=q_serve)
|
| 183 |
+
|
| 184 |
+
# 6. Next backbone step
|
| 185 |
+
t_b = time.perf_counter()
|
| 186 |
+
h, cb0_logits = self.backbone.step(fb)
|
| 187 |
+
backbone_time += time.perf_counter() - t_b
|
| 188 |
+
|
| 189 |
+
if self.backbone.past_len > max_past_len:
|
| 190 |
+
max_past_len = self.backbone.past_len
|
| 191 |
+
|
| 192 |
+
reasons.append(reason)
|
| 193 |
+
# Note on upstream aeos step:
|
| 194 |
+
# Upstream does one self._step(audio_in.aeos(1)) after the loop if reason.startswith("aeos").
|
| 195 |
+
# However, because each chunk resets the KV session and no further steps read from this KV session,
|
| 196 |
+
# running the final aeos step changes nothing returned and wastes compute. We skip it here.
|
| 197 |
+
|
| 198 |
+
if chunk_frames:
|
| 199 |
+
# chunk_frames: list of (16,)
|
| 200 |
+
chunk_codes_arr = np.stack(chunk_frames, axis=1) # (16, F_chunk)
|
| 201 |
+
gen_codes.append(chunk_codes_arr)
|
| 202 |
+
|
| 203 |
+
if gen_codes:
|
| 204 |
+
all_codes = np.concatenate(gen_codes, axis=1) # (16, Total_F)
|
| 205 |
+
total_frames = all_codes.shape[1]
|
| 206 |
+
t_m = time.perf_counter()
|
| 207 |
+
wav = self.mimi.decode(all_codes)
|
| 208 |
+
mimi_time += time.perf_counter() - t_m
|
| 209 |
+
else:
|
| 210 |
+
total_frames = 0
|
| 211 |
+
wav = np.zeros(0, dtype=np.float32)
|
| 212 |
+
|
| 213 |
+
wall_s = time.perf_counter() - t_wall_start
|
| 214 |
+
audio_s = len(wav) / 24000.0
|
| 215 |
+
rtf = wall_s / max(1e-6, audio_s)
|
| 216 |
+
|
| 217 |
+
# Peak RSS in MB
|
| 218 |
+
process = psutil.Process()
|
| 219 |
+
peak_rss_mb = process.memory_info().rss / (1024 * 1024)
|
| 220 |
+
|
| 221 |
+
metrics = {
|
| 222 |
+
"audio_s": audio_s,
|
| 223 |
+
"wall_s": wall_s,
|
| 224 |
+
"rtf": rtf,
|
| 225 |
+
"frames": total_frames,
|
| 226 |
+
"reasons": reasons,
|
| 227 |
+
"caps": caps,
|
| 228 |
+
"backbone_s": backbone_time,
|
| 229 |
+
"depth_s": depth_time,
|
| 230 |
+
"mimi_s": mimi_time,
|
| 231 |
+
"peak_rss_mb": peak_rss_mb,
|
| 232 |
+
"max_past_len": max_past_len,
|
| 233 |
+
"max_chunk_bound": max_chunk_bound,
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
return wav, metrics
|
src/maya_fast/ort_opts.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import onnxruntime as ort
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def session_options(threads: int | None = None) -> ort.SessionOptions:
|
| 8 |
+
opts = ort.SessionOptions()
|
| 9 |
+
if threads is None:
|
| 10 |
+
cpu_count = os.cpu_count() or 1
|
| 11 |
+
threads = min(8, cpu_count)
|
| 12 |
+
opts.intra_op_num_threads = max(1, threads)
|
| 13 |
+
opts.inter_op_num_threads = 1
|
| 14 |
+
# Global Constraints:
|
| 15 |
+
# intra_op_num_threads = requested threads (default min(8, physical cores)),
|
| 16 |
+
# session.intra_op.allow_spinning=0, enable_cpu_mem_arena=False, ORT_ENABLE_ALL
|
| 17 |
+
opts.add_session_config_entry("session.intra_op.allow_spinning", "0")
|
| 18 |
+
opts.enable_cpu_mem_arena = False
|
| 19 |
+
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 20 |
+
return opts
|
src/maya_fast/prompt.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import dataclasses
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from .assets import PromptAssets
|
| 9 |
+
from .embed import EmbeddingTable
|
| 10 |
+
|
| 11 |
+
# Port of the inference path for named voices (maya3.prompt.NamedSpeakerBuilder with V5Builder.example, the
|
| 12 |
+
# packer's _span and collate_v2 truncation, TTSModelV2.compose_inputs and SpeakerAnchor in eval mode).
|
| 13 |
+
# Checked token for token and value for value against tools/maya_export/golden_light.py by the golden tests.
|
| 14 |
+
|
| 15 |
+
RMS_EPS = float(np.finfo(np.float32).eps) # torch.nn.RMSNorm default eps for float32
|
| 16 |
+
_erf = np.frompyfunc(math.erf, 1, 1)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _rms_norm(x: np.ndarray, weight: np.ndarray) -> np.ndarray:
|
| 20 |
+
x = np.asarray(x, dtype=np.float32)
|
| 21 |
+
ms = np.mean(x * x, axis=-1, keepdims=True)
|
| 22 |
+
return (x * (1.0 / np.sqrt(ms + RMS_EPS)) * weight).astype(np.float32)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _gelu(x: np.ndarray) -> np.ndarray:
|
| 26 |
+
# exact (erf) GELU, torch's default
|
| 27 |
+
return (0.5 * x * (1.0 + _erf(x / math.sqrt(2.0)).astype(np.float32))).astype(np.float32)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class AudioIn:
|
| 31 |
+
"""Audio code frames -> backbone input (port of tts/model/audio_io.py AudioInputEmbed, eval mode)."""
|
| 32 |
+
|
| 33 |
+
def __init__(self, table: EmbeddingTable, aeos_in: np.ndarray, norm_w: np.ndarray, n_q: int = 16) -> None:
|
| 34 |
+
self.table = table
|
| 35 |
+
self.aeos_in = np.asarray(aeos_in, dtype=np.float32)
|
| 36 |
+
self.norm_w = np.asarray(norm_w, dtype=np.float32)
|
| 37 |
+
self.n_q = int(n_q)
|
| 38 |
+
if table.rows % self.n_q:
|
| 39 |
+
raise ValueError("audio table rows must be a multiple of n_q")
|
| 40 |
+
self.n_codes = table.rows // self.n_q
|
| 41 |
+
|
| 42 |
+
def frames(self, codes: np.ndarray, q_use: int = 16) -> np.ndarray:
|
| 43 |
+
"""
|
| 44 |
+
codes: (N, 16) int64 in [0, n_codes)
|
| 45 |
+
Returns: (N, d_model) float32
|
| 46 |
+
"""
|
| 47 |
+
codes = np.asarray(codes, dtype=np.int64)
|
| 48 |
+
if np.any((codes[:, :q_use] < 0) | (codes[:, :q_use] >= self.n_codes)):
|
| 49 |
+
raise ValueError("audio code outside [0, n_codes) reached the input table")
|
| 50 |
+
lvl = np.arange(q_use, dtype=np.int64)[None, :]
|
| 51 |
+
e = self.table.lookup(codes[:, :q_use] + lvl * self.n_codes) # (N, q_use, d)
|
| 52 |
+
return _rms_norm(e.sum(axis=1) / math.sqrt(q_use), self.norm_w)
|
| 53 |
+
|
| 54 |
+
def aeos(self) -> np.ndarray:
|
| 55 |
+
"""Returns: (d_model,) float32"""
|
| 56 |
+
return _rms_norm(self.aeos_in[None, :], self.norm_w)[0]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def speaker_anchor_null(p: dict[str, np.ndarray]) -> np.ndarray:
|
| 60 |
+
"""SpeakerAnchor for a null speaker row: fc2(gelu(fc1(null_vec))). Returns (d_model,) float32."""
|
| 61 |
+
h = _gelu(p["anchor_null"] @ p["anchor_fc1_w"].T + p["anchor_fc1_b"])
|
| 62 |
+
return (h @ p["anchor_fc2_w"].T + p["anchor_fc2_b"]).astype(np.float32)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@dataclasses.dataclass
|
| 66 |
+
class Prompt:
|
| 67 |
+
token_ids: np.ndarray # (n_prompt,) int64, ends with the <|audio|> sentinel
|
| 68 |
+
embeds: np.ndarray # (n_prompt, d_model) float32
|
| 69 |
+
n_prompt: int
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def build_prompt(text: str, voice: str, assets: PromptAssets, lang: str = "mr") -> Prompt:
|
| 73 |
+
"""
|
| 74 |
+
Named voice prompt: <|lang:xx|> "{Name}: " text <|audio|>
|
| 75 |
+
The speaker anchor (null for named voices) is added at the <|audio|> sentinel, the last prompt position.
|
| 76 |
+
"""
|
| 77 |
+
name = _resolve_voice(voice, assets)
|
| 78 |
+
info = assets.voices_info[name]
|
| 79 |
+
if info.get("mode") != "named":
|
| 80 |
+
raise NotImplementedError(f"voice {name!r} uses mode {info.get('mode')!r}; only named voices are supported")
|
| 81 |
+
|
| 82 |
+
rendered = "{" + name + "}: " + text.strip()
|
| 83 |
+
tok = assets.tokenizer
|
| 84 |
+
lang_id = tok.token_to_id(f"<|lang:{lang}|>")
|
| 85 |
+
audio_id = tok.token_to_id("<|audio|>")
|
| 86 |
+
if lang_id is None or audio_id is None:
|
| 87 |
+
raise KeyError(f"tokenizer lacks <|lang:{lang}|> or <|audio|>")
|
| 88 |
+
|
| 89 |
+
ids = [lang_id, *tok.encode(rendered, add_special_tokens=False).ids, audio_id]
|
| 90 |
+
token_ids = np.asarray(ids, dtype=np.int64)
|
| 91 |
+
embeds = assets.embed_table.lookup(token_ids)
|
| 92 |
+
embeds[-1] += speaker_anchor_null(assets.prompt_params)
|
| 93 |
+
return Prompt(token_ids=token_ids, embeds=embeds, n_prompt=len(token_ids))
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _resolve_voice(voice: str, assets: PromptAssets) -> str:
|
| 97 |
+
if voice in assets.voices_info:
|
| 98 |
+
return voice
|
| 99 |
+
low = str(voice).lower() # voices are case-insensitive
|
| 100 |
+
hits = [k for k in assets.voices_info if k.lower() == low]
|
| 101 |
+
if len(hits) == 1:
|
| 102 |
+
return hits[0]
|
| 103 |
+
raise KeyError(f"Unknown voice {voice!r}. Available: {sorted(assets.voices_info)}")
|
src/maya_fast/sampling.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def gumbel(rng: np.random.Generator, shape: tuple[int, ...]) -> np.ndarray:
|
| 7 |
+
"""
|
| 8 |
+
Produce float32 Gumbel noise: -log(-log(U)) with U in [1e-10, 1 - 1e-7].
|
| 9 |
+
"""
|
| 10 |
+
u = rng.uniform(low=1e-10, high=1.0 - 1e-7, size=shape).astype(np.float32)
|
| 11 |
+
return (-np.log(-np.log(u))).astype(np.float32)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def topk_gumbel_argmax(
|
| 15 |
+
logits: np.ndarray,
|
| 16 |
+
temp: float = 0.7,
|
| 17 |
+
top_k: int = 64,
|
| 18 |
+
noise: np.ndarray | None = None,
|
| 19 |
+
) -> int:
|
| 20 |
+
"""
|
| 21 |
+
Numpy reference of the graph's rule:
|
| 22 |
+
argmax(where(rank < top_k, logits / temp + noise, -inf))
|
| 23 |
+
"""
|
| 24 |
+
logits = np.asarray(logits, dtype=np.float32)
|
| 25 |
+
if noise is None:
|
| 26 |
+
noise = np.zeros_like(logits)
|
| 27 |
+
else:
|
| 28 |
+
noise = np.asarray(noise, dtype=np.float32)
|
| 29 |
+
|
| 30 |
+
k = min(top_k, logits.shape[-1])
|
| 31 |
+
topk_idx = np.argpartition(logits, -k)[-k:]
|
| 32 |
+
masked_perturbed = np.full_like(logits, -np.inf)
|
| 33 |
+
masked_perturbed[topk_idx] = logits[topk_idx] / temp + noise[topk_idx]
|
| 34 |
+
return int(np.argmax(masked_perturbed))
|
tests/__init__.py
ADDED
|
File without changes
|
tests/fixtures/__init__.py
ADDED
|
File without changes
|
tests/fixtures/tiny_assets.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import numpy as np
|
| 7 |
+
import onnx
|
| 8 |
+
from onnx import helper, TensorProto
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _sha256(data: bytes) -> str:
|
| 12 |
+
return hashlib.sha256(data).hexdigest()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _make_tiny_backbone_onnx(out_path: Path, d: int = 16, vocab: int = 64, n_layers: int = 2, n_heads: int = 2, head_dim: int = 8) -> None:
|
| 16 |
+
# Inputs:
|
| 17 |
+
# embeds (1, T, 2048) -> here (1, T, d)
|
| 18 |
+
# cos (1, T, head_dim) -> (1, T, 8)
|
| 19 |
+
# sin (1, T, head_dim) -> (1, T, 8)
|
| 20 |
+
# mask (1, 1, T, P+T)
|
| 21 |
+
# past_k_{i}, past_v_{i} ((1, 8, P, 128)) -> ((1, n_heads, P, head_dim))
|
| 22 |
+
# Outputs:
|
| 23 |
+
# hidden (1, T, 2048) -> (1, T, d)
|
| 24 |
+
# cb0_logits (1, T, 2049) -> (1, T, vocab + 1)
|
| 25 |
+
# present_k_{i}, present_v_{i}
|
| 26 |
+
|
| 27 |
+
inputs = [
|
| 28 |
+
helper.make_tensor_value_info("embeds", TensorProto.FLOAT, [1, "T", d]),
|
| 29 |
+
helper.make_tensor_value_info("cos", TensorProto.FLOAT, [1, "T", head_dim]),
|
| 30 |
+
helper.make_tensor_value_info("sin", TensorProto.FLOAT, [1, "T", head_dim]),
|
| 31 |
+
helper.make_tensor_value_info("mask", TensorProto.FLOAT, [1, 1, "T", "total_len"]),
|
| 32 |
+
]
|
| 33 |
+
for i in range(n_layers):
|
| 34 |
+
inputs.append(helper.make_tensor_value_info(f"past_k_{i}", TensorProto.FLOAT, [1, n_heads, "P", head_dim]))
|
| 35 |
+
inputs.append(helper.make_tensor_value_info(f"past_v_{i}", TensorProto.FLOAT, [1, n_heads, "P", head_dim]))
|
| 36 |
+
|
| 37 |
+
outputs = [
|
| 38 |
+
helper.make_tensor_value_info("hidden", TensorProto.FLOAT, [1, "T", d]),
|
| 39 |
+
helper.make_tensor_value_info("cb0_logits", TensorProto.FLOAT, [1, "T", vocab + 1]),
|
| 40 |
+
]
|
| 41 |
+
for i in range(n_layers):
|
| 42 |
+
outputs.append(helper.make_tensor_value_info(f"present_k_{i}", TensorProto.FLOAT, [1, n_heads, "total_len", head_dim]))
|
| 43 |
+
outputs.append(helper.make_tensor_value_info(f"present_v_{i}", TensorProto.FLOAT, [1, n_heads, "total_len", head_dim]))
|
| 44 |
+
|
| 45 |
+
# For tiny mock model:
|
| 46 |
+
# hidden = Identity(embeds)
|
| 47 |
+
# cb0_logits = MatMul(hidden, W_cb0) + B_cb0
|
| 48 |
+
# present_k_i = Concat(past_k_i, k_new, axis=2)
|
| 49 |
+
# We construct a simple valid ONNX graph that outputs the right shapes.
|
| 50 |
+
nodes = []
|
| 51 |
+
initializers = []
|
| 52 |
+
|
| 53 |
+
# W_cb0: (d, vocab + 1)
|
| 54 |
+
np.random.seed(42)
|
| 55 |
+
w_cb0_val = np.random.randn(d, vocab + 1).astype(np.float32) * 0.1
|
| 56 |
+
# ensure aeos (index vocab) has sensible value
|
| 57 |
+
initializers.append(helper.make_tensor("W_cb0", TensorProto.FLOAT, [d, vocab + 1], w_cb0_val.flatten()))
|
| 58 |
+
|
| 59 |
+
nodes.append(helper.make_node("Identity", ["embeds"], ["hidden"]))
|
| 60 |
+
nodes.append(helper.make_node("MatMul", ["hidden", "W_cb0"], ["cb0_logits"]))
|
| 61 |
+
|
| 62 |
+
# For past_k, past_v: generate dummy k_new from embeds:
|
| 63 |
+
# embeds (1, T, d) -> slice or reshape to (1, n_heads, T, head_dim)
|
| 64 |
+
# In our case d = 16 = 2 * 8 = n_heads * head_dim!
|
| 65 |
+
# So we can reshape embeds -> (1, T, n_heads, head_dim) then transpose to (1, n_heads, T, head_dim)
|
| 66 |
+
initializers.append(helper.make_tensor("shape_k", TensorProto.INT64, [4], [1, -1, n_heads, head_dim]))
|
| 67 |
+
nodes.append(helper.make_node("Reshape", ["embeds", "shape_k"], ["reshaped_kv"]))
|
| 68 |
+
nodes.append(helper.make_node("Transpose", ["reshaped_kv"], ["k_new"], perm=[0, 2, 1, 3]))
|
| 69 |
+
|
| 70 |
+
for i in range(n_layers):
|
| 71 |
+
nodes.append(helper.make_node("Concat", [f"past_k_{i}", "k_new"], [f"present_k_{i}"], axis=2))
|
| 72 |
+
nodes.append(helper.make_node("Concat", [f"past_v_{i}", "k_new"], [f"present_v_{i}"], axis=2))
|
| 73 |
+
|
| 74 |
+
graph = helper.make_graph(nodes, "tiny_backbone", inputs, outputs, initializers)
|
| 75 |
+
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)], ir_version=10)
|
| 76 |
+
onnx.save(model, str(out_path))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _make_tiny_depth_onnx(out_path: Path, d: int = 16, vocab: int = 64, d_mem: int = 16) -> None:
|
| 80 |
+
# DepthFrame (batched, as the real sampled graph):
|
| 81 |
+
# Inputs:
|
| 82 |
+
# h (B, 2048) -> (B, d)
|
| 83 |
+
# cb0 (B,) int64
|
| 84 |
+
# mem (B, R, 1024) -> (B, R, d_mem)
|
| 85 |
+
# noise (B, 15, 2048) float32 -> (B, 15, vocab)
|
| 86 |
+
# Output:
|
| 87 |
+
# codes (B, 16) int64
|
| 88 |
+
|
| 89 |
+
inputs = [
|
| 90 |
+
helper.make_tensor_value_info("h", TensorProto.FLOAT, ["B", d]),
|
| 91 |
+
helper.make_tensor_value_info("cb0", TensorProto.INT64, ["B"]),
|
| 92 |
+
helper.make_tensor_value_info("mem", TensorProto.FLOAT, ["B", "R", d_mem]),
|
| 93 |
+
helper.make_tensor_value_info("noise", TensorProto.FLOAT, ["B", 15, vocab]),
|
| 94 |
+
]
|
| 95 |
+
outputs = [
|
| 96 |
+
helper.make_tensor_value_info("codes", TensorProto.INT64, ["B", 16]),
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
# For tiny test: codes[:, 0] = cb0, codes[:, 1..15] = argmax(noise[:, i]) over the vocab.
|
| 100 |
+
initializers = [helper.make_tensor("unsq_axes", TensorProto.INT64, [1], [1])]
|
| 101 |
+
nodes = [
|
| 102 |
+
helper.make_node("ArgMax", ["noise"], ["deeper_codes"], axis=-1, keepdims=0),
|
| 103 |
+
helper.make_node("Unsqueeze", ["cb0", "unsq_axes"], ["cb0_col"]),
|
| 104 |
+
helper.make_node("Concat", ["cb0_col", "deeper_codes"], ["codes"], axis=1),
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
graph = helper.make_graph(nodes, "tiny_depth", inputs, outputs, initializers)
|
| 108 |
+
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)], ir_version=10)
|
| 109 |
+
onnx.save(model, str(out_path))
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _make_tiny_mimi_onnx(out_path: Path) -> None:
|
| 113 |
+
# MimiDecoder:
|
| 114 |
+
# Input: codes (1, 16, F) int64
|
| 115 |
+
# Output: wav (1, 1, F * 1920) float32
|
| 116 |
+
inputs = [
|
| 117 |
+
helper.make_tensor_value_info("codes", TensorProto.INT64, [1, 16, "F"]),
|
| 118 |
+
]
|
| 119 |
+
outputs = [
|
| 120 |
+
helper.make_tensor_value_info("wav", TensorProto.FLOAT, [1, 1, "samples"]),
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
# In ONNX helper:
|
| 124 |
+
# Shape of codes -> shape (3,)
|
| 125 |
+
# Gather index 2 -> F
|
| 126 |
+
# Expand 1920 samples per frame.
|
| 127 |
+
# We can do: Cast(codes, FLOAT), ReduceMean along axis 1 -> (1, 1, F)
|
| 128 |
+
# Then Tile by [1, 1, 1920] -> (1, 1, F * 1920)
|
| 129 |
+
initializers = [
|
| 130 |
+
helper.make_tensor("tile_repeats", TensorProto.INT64, [3], [1, 1, 1920]),
|
| 131 |
+
]
|
| 132 |
+
nodes = [
|
| 133 |
+
helper.make_node("Cast", ["codes"], ["codes_float"], to=TensorProto.FLOAT),
|
| 134 |
+
helper.make_node("ReduceMean", ["codes_float"], ["mean_frames"], axes=[1], keepdims=1),
|
| 135 |
+
helper.make_node("Tile", ["mean_frames", "tile_repeats"], ["wav"]),
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
graph = helper.make_graph(nodes, "tiny_mimi", inputs, outputs, initializers)
|
| 139 |
+
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 17)], ir_version=10)
|
| 140 |
+
onnx.save(model, str(out_path))
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def make_tiny_assets(assets_dir: Path, aeos_bias: float = 0.0) -> Path:
|
| 144 |
+
assets_dir = Path(assets_dir)
|
| 145 |
+
assets_dir.mkdir(parents=True, exist_ok=True)
|
| 146 |
+
|
| 147 |
+
d = 16
|
| 148 |
+
vocab = 64
|
| 149 |
+
n_layers = 2
|
| 150 |
+
n_heads = 2
|
| 151 |
+
head_dim = 8
|
| 152 |
+
d_mem = 16
|
| 153 |
+
n_q = 16
|
| 154 |
+
n_codes = vocab
|
| 155 |
+
ecapa_dim = 16
|
| 156 |
+
|
| 157 |
+
files_sha = {}
|
| 158 |
+
|
| 159 |
+
# 1. embed_bf16.bin
|
| 160 |
+
np.random.seed(123)
|
| 161 |
+
embeds_fp32 = np.random.randn(vocab, d).astype(np.float32)
|
| 162 |
+
# Convert fp32 to bf16 uint16
|
| 163 |
+
embeds_bf16 = (embeds_fp32.view(np.uint32) >> 16).astype(np.uint16)
|
| 164 |
+
embed_bytes = embeds_bf16.tobytes()
|
| 165 |
+
(assets_dir / "embed_bf16.bin").write_bytes(embed_bytes)
|
| 166 |
+
files_sha["embed_bf16.bin"] = _sha256(embed_bytes)
|
| 167 |
+
|
| 168 |
+
# 2. prompt.npz
|
| 169 |
+
# anchor_fc1_w, anchor_fc1_b, anchor_fc2_w, anchor_fc2_b, anchor_null, aeos, plus audio_in/<name>
|
| 170 |
+
# Note: audio_in parameters: table (n_q * n_codes, d), aeos_in (d,), norm.weight (d,)
|
| 171 |
+
prompt_dict = {
|
| 172 |
+
"anchor_fc1_w": np.random.randn(32, ecapa_dim).astype(np.float32),
|
| 173 |
+
"anchor_fc1_b": np.zeros(32, dtype=np.float32),
|
| 174 |
+
"anchor_fc2_w": np.zeros((d, 32), dtype=np.float32), # zero init as upstream
|
| 175 |
+
"anchor_fc2_b": np.zeros(d, dtype=np.float32),
|
| 176 |
+
"anchor_null": np.random.randn(ecapa_dim).astype(np.float32),
|
| 177 |
+
"aeos": np.random.randn(d).astype(np.float32),
|
| 178 |
+
"audio_in/aeos_in": np.random.randn(d).astype(np.float32),
|
| 179 |
+
"audio_in/norm": np.ones(d, dtype=np.float32),
|
| 180 |
+
}
|
| 181 |
+
np.savez(assets_dir / "prompt.npz", **prompt_dict)
|
| 182 |
+
files_sha["prompt.npz"] = _sha256((assets_dir / "prompt.npz").read_bytes())
|
| 183 |
+
|
| 184 |
+
# audio input table, bf16 raw uint16 row-major (same layout as the real export)
|
| 185 |
+
audio_f32 = np.random.randn(n_q * n_codes, d).astype(np.float32)
|
| 186 |
+
audio_bf16 = (audio_f32.view(np.uint32) >> 16).astype("<u2")
|
| 187 |
+
(assets_dir / "audio_in_bf16.bin").write_bytes(audio_bf16.tobytes())
|
| 188 |
+
files_sha["audio_in_bf16.bin"] = _sha256((assets_dir / "audio_in_bf16.bin").read_bytes())
|
| 189 |
+
|
| 190 |
+
# 3. voices.json + voices.npz
|
| 191 |
+
voices_json = {
|
| 192 |
+
"Charvi": {"mode": "named", "use_icl": False, "use_mem": False}
|
| 193 |
+
}
|
| 194 |
+
(assets_dir / "voices.json").write_text(json.dumps(voices_json, indent=2))
|
| 195 |
+
files_sha["voices.json"] = _sha256((assets_dir / "voices.json").read_bytes())
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# 4. controls.json
|
| 199 |
+
controls_json = {
|
| 200 |
+
"text_frames_prior": {
|
| 201 |
+
"mr": {"a": 4.0, "b": 0.5, "resid_p99": 10.0}
|
| 202 |
+
},
|
| 203 |
+
"text": {"terminals": "।.!?;।॥", "fps": 12.5, "frame_s": 0.08, "chunk_s": 16.0},
|
| 204 |
+
"guard": {
|
| 205 |
+
"enabled": True,
|
| 206 |
+
"dur_k": 2.2,
|
| 207 |
+
"dur_margin_s": 1.2,
|
| 208 |
+
"hard_cap": 1500,
|
| 209 |
+
"dur_prior_coefs": {"mr": [1.0, 0.05, 0.03, 0.3, 0.3]},
|
| 210 |
+
"dur_prior_default": [1.0, 0.05, 0.03, 0.3, 0.3]
|
| 211 |
+
},
|
| 212 |
+
"sampling": {
|
| 213 |
+
"q_serve": 16,
|
| 214 |
+
"temp": 0.7,
|
| 215 |
+
"top_k": 64,
|
| 216 |
+
"tau": 0.35,
|
| 217 |
+
"hazard": 0.6
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
(assets_dir / "controls.json").write_text(json.dumps(controls_json, indent=2))
|
| 221 |
+
files_sha["controls.json"] = _sha256((assets_dir / "controls.json").read_bytes())
|
| 222 |
+
|
| 223 |
+
# 5. tokenizer.json (minimal mock tokenizer using tokenizers library)
|
| 224 |
+
from tokenizers import Tokenizer, models, pre_tokenizers
|
| 225 |
+
vocab_dict = {f"t_{i}": i for i in range(vocab - 5)}
|
| 226 |
+
vocab_dict["<|lang:mr|>"] = vocab - 5
|
| 227 |
+
vocab_dict["<|audio|>"] = vocab - 4
|
| 228 |
+
vocab_dict["<|aeos|>"] = vocab - 3
|
| 229 |
+
vocab_dict["[UNK]"] = vocab - 2
|
| 230 |
+
vocab_dict[" "] = vocab - 1
|
| 231 |
+
tok = Tokenizer(models.WordLevel(vocab_dict, unk_token="[UNK]"))
|
| 232 |
+
tok.pre_tokenizer = pre_tokenizers.WhitespaceSplit()
|
| 233 |
+
tok.save(str(assets_dir / "tokenizer.json"))
|
| 234 |
+
files_sha["tokenizer.json"] = _sha256((assets_dir / "tokenizer.json").read_bytes())
|
| 235 |
+
|
| 236 |
+
# 6. ONNX models
|
| 237 |
+
_make_tiny_backbone_onnx(assets_dir / "backbone_wo8.onnx", d=d, vocab=vocab, n_layers=n_layers, n_heads=n_heads, head_dim=head_dim)
|
| 238 |
+
# If aeos_bias is requested, we can adjust W_cb0 in backbone or create an alternate backbone
|
| 239 |
+
if aeos_bias != 0.0:
|
| 240 |
+
# Load and modify W_cb0 last column
|
| 241 |
+
m = onnx.load(str(assets_dir / "backbone_wo8.onnx"))
|
| 242 |
+
for init in m.graph.initializer:
|
| 243 |
+
if init.name == "W_cb0":
|
| 244 |
+
arr = onnx.numpy_helper.to_array(init)
|
| 245 |
+
# bias is applied to the output logits, or we add bias node.
|
| 246 |
+
# In tiny_backbone, cb0_logits = hidden @ W_cb0. If we add a large negative bias to index vocab (aeos):
|
| 247 |
+
# Let's add an explicit bias tensor:
|
| 248 |
+
bias_arr = np.zeros((1, 1, vocab + 1), dtype=np.float32)
|
| 249 |
+
bias_arr[0, 0, vocab] = aeos_bias
|
| 250 |
+
bias_init = helper.make_tensor("bias_cb0", TensorProto.FLOAT, [1, 1, vocab + 1], bias_arr.flatten())
|
| 251 |
+
m.graph.initializer.append(bias_init)
|
| 252 |
+
# change cb0_logits node
|
| 253 |
+
for node in m.graph.node:
|
| 254 |
+
if node.name == "" and node.op_type == "MatMul":
|
| 255 |
+
node.output[0] = "cb0_logits_pre"
|
| 256 |
+
m.graph.node.append(helper.make_node("Add", ["cb0_logits_pre", "bias_cb0"], ["cb0_logits"]))
|
| 257 |
+
break
|
| 258 |
+
onnx.save(m, str(assets_dir / "backbone_wo8.onnx"))
|
| 259 |
+
|
| 260 |
+
files_sha["backbone_wo8.onnx"] = _sha256((assets_dir / "backbone_wo8.onnx").read_bytes())
|
| 261 |
+
|
| 262 |
+
_make_tiny_depth_onnx(assets_dir / "depth_frame_fp16.onnx", d=d, vocab=vocab, d_mem=d_mem)
|
| 263 |
+
files_sha["depth_frame_fp16.onnx"] = _sha256((assets_dir / "depth_frame_fp16.onnx").read_bytes())
|
| 264 |
+
|
| 265 |
+
_make_tiny_mimi_onnx(assets_dir / "mimi_decoder.onnx")
|
| 266 |
+
files_sha["mimi_decoder.onnx"] = _sha256((assets_dir / "mimi_decoder.onnx").read_bytes())
|
| 267 |
+
|
| 268 |
+
# 7. manifest.json
|
| 269 |
+
manifest = {
|
| 270 |
+
"version": 1,
|
| 271 |
+
"files": files_sha,
|
| 272 |
+
"shapes": {
|
| 273 |
+
"d_model": d,
|
| 274 |
+
"vocab": vocab,
|
| 275 |
+
"n_q": n_q,
|
| 276 |
+
"n_codes": n_codes,
|
| 277 |
+
"n_layers": n_layers,
|
| 278 |
+
"n_heads": n_heads,
|
| 279 |
+
"head_dim": head_dim,
|
| 280 |
+
"d_mem": d_mem,
|
| 281 |
+
"rope_theta": 10000.0,
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
(assets_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
| 285 |
+
|
| 286 |
+
return assets_dir
|
tests/test_audio_utils.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
import numpy as np
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import tempfile
|
| 6 |
+
import soundfile as sf
|
| 7 |
+
|
| 8 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 9 |
+
|
| 10 |
+
from src.audio_utils import resample, load_and_resample, condition_audio
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def tone(freq, sr, seconds=1.0, amp=0.5):
|
| 14 |
+
t = np.arange(int(sr * seconds)) / sr
|
| 15 |
+
return (amp * np.sin(2 * np.pi * freq * t)).astype(np.float32)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TestResample(unittest.TestCase):
|
| 19 |
+
def test_removes_content_above_nyquist(self):
|
| 20 |
+
# 10 kHz is above the 8 kHz Nyquist of 16 kHz output; linear interp aliases it to 6 kHz
|
| 21 |
+
out = resample(tone(10000, 48000), 48000, 16000)
|
| 22 |
+
self.assertLess(np.sqrt(np.mean(out[1000:-1000] ** 2)), 0.01)
|
| 23 |
+
|
| 24 |
+
def test_preserves_speech_band_tone(self):
|
| 25 |
+
out = resample(tone(1000, 44100), 44100, 16000)
|
| 26 |
+
self.assertEqual(len(out), 16000)
|
| 27 |
+
self.assertAlmostEqual(np.sqrt(np.mean(out[1000:-1000] ** 2)), 0.5 / np.sqrt(2), delta=0.02)
|
| 28 |
+
|
| 29 |
+
def test_upsample_8k(self):
|
| 30 |
+
out = resample(tone(1000, 8000), 8000, 16000)
|
| 31 |
+
self.assertEqual(len(out), 16000)
|
| 32 |
+
self.assertEqual(out.dtype, np.float32)
|
| 33 |
+
|
| 34 |
+
def test_16k_passthrough(self):
|
| 35 |
+
x = tone(1000, 16000)
|
| 36 |
+
self.assertTrue(np.array_equal(resample(x, 16000, 16000), x))
|
| 37 |
+
|
| 38 |
+
def test_load_and_resample_uses_soxr(self):
|
| 39 |
+
stereo = np.stack([tone(10000, 48000), tone(10000, 48000)], axis=1)
|
| 40 |
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
| 41 |
+
path = f.name
|
| 42 |
+
try:
|
| 43 |
+
sf.write(path, stereo, 48000)
|
| 44 |
+
data, duration = load_and_resample(path)
|
| 45 |
+
finally:
|
| 46 |
+
os.remove(path)
|
| 47 |
+
self.assertEqual(data.ndim, 1)
|
| 48 |
+
self.assertEqual(data.dtype, np.float32)
|
| 49 |
+
self.assertAlmostEqual(duration, 1.0, places=3)
|
| 50 |
+
self.assertLess(np.sqrt(np.mean(data[1000:-1000] ** 2)), 0.01)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class TestConditionAudio(unittest.TestCase):
|
| 54 |
+
def test_removes_dc_normalizes_and_pads(self):
|
| 55 |
+
x = (0.1 * np.sin(2 * np.pi * 440 * np.arange(16000) / 16000) + 0.05).astype(np.float32)
|
| 56 |
+
out = condition_audio(x)
|
| 57 |
+
self.assertEqual(len(out), 16000 + 2 * 3200)
|
| 58 |
+
self.assertTrue(np.all(out[:3200] == 0) and np.all(out[-3200:] == 0))
|
| 59 |
+
self.assertLess(abs(out[3200:-3200].mean()), 1e-3)
|
| 60 |
+
self.assertAlmostEqual(np.abs(out).max(), 0.9, delta=1e-3)
|
| 61 |
+
|
| 62 |
+
def test_all_zero_stays_zero(self):
|
| 63 |
+
out = condition_audio(np.zeros(1600, dtype=np.float32))
|
| 64 |
+
self.assertFalse(np.isnan(out).any())
|
| 65 |
+
self.assertEqual(np.abs(out).max(), 0.0)
|
| 66 |
+
|
| 67 |
+
def test_near_silent_gain_is_capped(self):
|
| 68 |
+
x = (0.001 * np.sin(2 * np.pi * 440 * np.arange(16000) / 16000)).astype(np.float32)
|
| 69 |
+
self.assertAlmostEqual(np.abs(condition_audio(x)).max(), 0.01, delta=1e-4)
|
| 70 |
+
|
| 71 |
+
def test_empty_input_returns_padding_only(self):
|
| 72 |
+
self.assertEqual(len(condition_audio(np.zeros(0, dtype=np.float32))), 6400)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
unittest.main()
|
tests/test_maya_fast_assets.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import tempfile
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from tests.fixtures.tiny_assets import make_tiny_assets
|
| 7 |
+
from src.maya_fast.assets import load_manifest, AssetError, PromptAssets
|
| 8 |
+
from src.maya_fast.embed import EmbeddingTable
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestMayaFastAssets(unittest.TestCase):
|
| 12 |
+
def setUp(self):
|
| 13 |
+
self.temp_dir = tempfile.TemporaryDirectory()
|
| 14 |
+
self.assets_dir = make_tiny_assets(Path(self.temp_dir.name))
|
| 15 |
+
|
| 16 |
+
def tearDown(self):
|
| 17 |
+
self.temp_dir.cleanup()
|
| 18 |
+
|
| 19 |
+
def test_manifest_sha_mismatch_raises(self):
|
| 20 |
+
# corrupt a file
|
| 21 |
+
corrupt_target = self.assets_dir / "embed_bf16.bin"
|
| 22 |
+
with open(corrupt_target, "ab") as f:
|
| 23 |
+
f.write(b"\x00\x00\x00\x00")
|
| 24 |
+
|
| 25 |
+
with self.assertRaises(AssetError) as ctx:
|
| 26 |
+
load_manifest(self.assets_dir, verify=True)
|
| 27 |
+
self.assertIn("embed_bf16.bin", str(ctx.exception))
|
| 28 |
+
|
| 29 |
+
def test_manifest_missing_file_raises(self):
|
| 30 |
+
(self.assets_dir / "prompt.npz").unlink()
|
| 31 |
+
with self.assertRaises(AssetError) as ctx:
|
| 32 |
+
load_manifest(self.assets_dir, verify=True)
|
| 33 |
+
self.assertIn("prompt.npz", str(ctx.exception))
|
| 34 |
+
|
| 35 |
+
def test_bf16_lookup_exact(self):
|
| 36 |
+
# write 3 known float32 values that are exact in bf16, look up -> equal
|
| 37 |
+
with tempfile.TemporaryDirectory() as td:
|
| 38 |
+
p = Path(td) / "test_embed.bin"
|
| 39 |
+
# 1.0, -2.5, 0.125 are exactly representable in bf16
|
| 40 |
+
vals = np.array([[1.0, -2.5, 0.125], [0.0, 4.0, -0.5]], dtype=np.float32)
|
| 41 |
+
u32 = vals.view(np.uint32)
|
| 42 |
+
bf16 = (u32 >> 16).astype(np.uint16)
|
| 43 |
+
p.write_bytes(bf16.tobytes())
|
| 44 |
+
|
| 45 |
+
table = EmbeddingTable(p, rows=2, dim=3)
|
| 46 |
+
res = table.lookup(np.array([0, 1]))
|
| 47 |
+
np.testing.assert_array_equal(res, vals)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
unittest.main()
|
tests/test_maya_fast_backbone.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import tempfile
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from tests.fixtures.tiny_assets import make_tiny_assets
|
| 7 |
+
from src.maya_fast.backbone import Backbone
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TestMayaFastBackbone(unittest.TestCase):
|
| 11 |
+
def setUp(self):
|
| 12 |
+
self.temp_dir = tempfile.TemporaryDirectory()
|
| 13 |
+
self.assets_dir = make_tiny_assets(Path(self.temp_dir.name))
|
| 14 |
+
|
| 15 |
+
def tearDown(self):
|
| 16 |
+
self.temp_dir.cleanup()
|
| 17 |
+
|
| 18 |
+
def test_backbone_step_kv_grows(self):
|
| 19 |
+
# runtime: tiny synthetic 2-layer graph built with onnx.helper in tests/fixtures/tiny_assets.py;
|
| 20 |
+
# past_len goes 5 -> 6 -> 7 and reset() returns it to 0
|
| 21 |
+
backbone_path = self.assets_dir / "backbone_wo8.onnx"
|
| 22 |
+
bb = Backbone(backbone_path, threads=1, manifest_path=self.assets_dir / "manifest.json")
|
| 23 |
+
self.assertEqual(bb.past_len, 0)
|
| 24 |
+
|
| 25 |
+
# Prefill 5 tokens
|
| 26 |
+
# d_model = 16 for tiny
|
| 27 |
+
d = 16
|
| 28 |
+
prompt_embeds = np.random.randn(5, d).astype(np.float32)
|
| 29 |
+
h, cb0 = bb.step(prompt_embeds)
|
| 30 |
+
self.assertEqual(bb.past_len, 5)
|
| 31 |
+
self.assertEqual(h.shape, (d,))
|
| 32 |
+
self.assertEqual(cb0.shape, (65,)) # vocab + 1
|
| 33 |
+
|
| 34 |
+
# Step 6
|
| 35 |
+
tok1 = np.random.randn(1, d).astype(np.float32)
|
| 36 |
+
h, cb0 = bb.step(tok1)
|
| 37 |
+
self.assertEqual(bb.past_len, 6)
|
| 38 |
+
|
| 39 |
+
# Step 7
|
| 40 |
+
tok2 = np.random.randn(1, d).astype(np.float32)
|
| 41 |
+
h, cb0 = bb.step(tok2)
|
| 42 |
+
self.assertEqual(bb.past_len, 7)
|
| 43 |
+
|
| 44 |
+
# Reset
|
| 45 |
+
bb.reset()
|
| 46 |
+
self.assertEqual(bb.past_len, 0)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
unittest.main()
|
tests/test_maya_fast_codec.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import tempfile
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from tests.fixtures.tiny_assets import make_tiny_assets
|
| 7 |
+
from src.maya_fast.assets import default_assets_dir
|
| 8 |
+
from src.maya_fast.codec import MimiDecoder
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestMayaFastCodec(unittest.TestCase):
|
| 12 |
+
def setUp(self):
|
| 13 |
+
self.temp_dir = tempfile.TemporaryDirectory()
|
| 14 |
+
self.assets_dir = make_tiny_assets(Path(self.temp_dir.name))
|
| 15 |
+
|
| 16 |
+
def tearDown(self):
|
| 17 |
+
self.temp_dir.cleanup()
|
| 18 |
+
|
| 19 |
+
def test_codec_length(self):
|
| 20 |
+
# tiny assets: F = 5 -> 9600 samples
|
| 21 |
+
codec_path = self.assets_dir / "mimi_decoder.onnx"
|
| 22 |
+
mimi = MimiDecoder(codec_path, threads=1)
|
| 23 |
+
|
| 24 |
+
F = 5
|
| 25 |
+
codes = np.random.randint(0, 64, size=(16, F), dtype=np.int64)
|
| 26 |
+
wav = mimi.decode(codes)
|
| 27 |
+
|
| 28 |
+
self.assertEqual(wav.shape, (F * 1920,))
|
| 29 |
+
self.assertEqual(wav.dtype, np.float32)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TestMayaFastCodecReal(unittest.TestCase):
|
| 33 |
+
def test_real_decoder_length(self):
|
| 34 |
+
path = default_assets_dir() / "mimi_decoder.onnx"
|
| 35 |
+
if not path.exists():
|
| 36 |
+
self.skipTest(f"real Mimi decoder not found at {path}")
|
| 37 |
+
mimi = MimiDecoder(path, threads=1)
|
| 38 |
+
codes = np.random.default_rng(0).integers(0, 2048, size=(16, 5), dtype=np.int64)
|
| 39 |
+
wav = mimi.decode(codes)
|
| 40 |
+
self.assertEqual(wav.shape, (5 * 1920,))
|
| 41 |
+
self.assertTrue(np.all(np.isfinite(wav)))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
unittest.main()
|
tests/test_maya_fast_control.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import unittest
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from src.maya_fast.assets import PromptAssets, default_assets_dir
|
| 7 |
+
from src.maya_fast.control import (
|
| 8 |
+
expected_frames,
|
| 9 |
+
frame_cap,
|
| 10 |
+
min_frames,
|
| 11 |
+
split_text,
|
| 12 |
+
StopPolicy,
|
| 13 |
+
sample_cb0,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class TestMayaFastControl(unittest.TestCase):
|
| 18 |
+
def test_controls_basic(self):
|
| 19 |
+
prior = {"mr": {"a": 4.0, "b": 0.5, "resid_p99": 10.0}}
|
| 20 |
+
ef, er = expected_frames(prior, "mr", 20)
|
| 21 |
+
self.assertEqual(ef, 14.0)
|
| 22 |
+
self.assertEqual(er, 10.0)
|
| 23 |
+
|
| 24 |
+
controls = {
|
| 25 |
+
"guard": {
|
| 26 |
+
"enabled": True,
|
| 27 |
+
"dur_k": 2.2,
|
| 28 |
+
"dur_margin_s": 1.2,
|
| 29 |
+
"hard_cap": 1500,
|
| 30 |
+
"dur_prior_coefs": {"mr": [1.0, 0.05, 0.03, 0.3, 0.3]},
|
| 31 |
+
"dur_prior_default": [1.0, 0.05, 0.03, 0.3, 0.3],
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
cap = frame_cap("नमस्कार", "mr", ef, er, controls)
|
| 35 |
+
self.assertGreater(cap, 0)
|
| 36 |
+
self.assertLessEqual(cap, 1500)
|
| 37 |
+
|
| 38 |
+
mf = min_frames(ef)
|
| 39 |
+
self.assertGreaterEqual(mf, 4)
|
| 40 |
+
|
| 41 |
+
# test split_text
|
| 42 |
+
text = "हा पहिला भाग आहे. हा दुसरा भाग आहे."
|
| 43 |
+
chunks = split_text(text, "mr", prior, chunk_s=1.0)
|
| 44 |
+
self.assertGreater(len(chunks), 1)
|
| 45 |
+
|
| 46 |
+
def test_stop_policy_reasons(self):
|
| 47 |
+
# test_stop_policy_reasons: hand-built p vectors trigger each reason at the right t; nothing fires before min_frames
|
| 48 |
+
policy = StopPolicy(min_frames=5, tau=0.35, hazard=0.6)
|
| 49 |
+
aeos_class = 2048
|
| 50 |
+
p = np.zeros(2049, dtype=np.float32)
|
| 51 |
+
|
| 52 |
+
# Before min_frames, none should fire even if aeos is 1.0
|
| 53 |
+
p[aeos_class] = 1.0
|
| 54 |
+
for t in range(5):
|
| 55 |
+
self.assertIsNone(policy.check(t, p))
|
| 56 |
+
|
| 57 |
+
# At t = 5, argmax aeos triggers aeos_argmax
|
| 58 |
+
self.assertEqual(policy.check(5, p), "aeos_argmax")
|
| 59 |
+
|
| 60 |
+
# Test tau trigger when argmax is another token
|
| 61 |
+
policy = StopPolicy(min_frames=5, tau=0.35, hazard=0.6)
|
| 62 |
+
p = np.zeros(2049, dtype=np.float32)
|
| 63 |
+
p[0] = 0.5
|
| 64 |
+
p[aeos_class] = 0.4 # >= 0.35 but argmax is 0
|
| 65 |
+
self.assertEqual(policy.check(5, p), "aeos_tau")
|
| 66 |
+
|
| 67 |
+
# Test hazard trigger: accumulated hazard 1 - prod(1 - p_aeos) >= 0.6
|
| 68 |
+
# If p_aeos = 0.2 at each step after min_frames:
|
| 69 |
+
# t=5: 1 - 0.8 = 0.2
|
| 70 |
+
# t=6: 1 - 0.8^2 = 0.36
|
| 71 |
+
# t=7: 1 - 0.8^3 = 0.488
|
| 72 |
+
# t=8: 1 - 0.8^4 = 0.5904
|
| 73 |
+
# t=9: 1 - 0.8^5 = 0.67232 >= 0.6 -> aeos_hazard!
|
| 74 |
+
policy = StopPolicy(min_frames=5, tau=0.35, hazard=0.6)
|
| 75 |
+
p = np.zeros(2049, dtype=np.float32)
|
| 76 |
+
p[0] = 0.7
|
| 77 |
+
p[aeos_class] = 0.2
|
| 78 |
+
for t in range(5, 9):
|
| 79 |
+
self.assertIsNone(policy.check(t, p))
|
| 80 |
+
self.assertEqual(policy.check(9, p), "aeos_hazard")
|
| 81 |
+
|
| 82 |
+
def test_sample_cb0(self):
|
| 83 |
+
# aeos zeroed, Gumbel-max over the rest
|
| 84 |
+
aeos_class = 64
|
| 85 |
+
p = np.zeros(65, dtype=np.float32)
|
| 86 |
+
p[aeos_class] = 0.99
|
| 87 |
+
p[10] = 0.01
|
| 88 |
+
|
| 89 |
+
noise = np.zeros(65, dtype=np.float32)
|
| 90 |
+
# Even though aeos had 0.99, sample_cb0 masks aeos out, so token 10 must be chosen
|
| 91 |
+
chosen = sample_cb0(p, noise, aeos_class=aeos_class)
|
| 92 |
+
self.assertEqual(chosen, 10)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
GOLDEN = Path.home() / ".cache" / "maya_fast" / "golden"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class TestMayaFastControlGolden(unittest.TestCase):
|
| 99 |
+
"""Sentence controls (ef, er, cap, minimum, split_text) must equal the upstream values in light.json."""
|
| 100 |
+
|
| 101 |
+
@classmethod
|
| 102 |
+
def setUpClass(cls):
|
| 103 |
+
assets_dir = default_assets_dir()
|
| 104 |
+
if not (GOLDEN / "light.json").exists() or not (assets_dir / "manifest.json").exists():
|
| 105 |
+
raise unittest.SkipTest(f"goldens at {GOLDEN} or assets at {assets_dir} not found")
|
| 106 |
+
with open(GOLDEN / "light.json", "r", encoding="utf-8") as f:
|
| 107 |
+
cls.gj = json.load(f)
|
| 108 |
+
cls.controls = PromptAssets(assets_dir).controls
|
| 109 |
+
|
| 110 |
+
def test_controls_match_golden(self):
|
| 111 |
+
prior = self.controls["text_frames_prior"]
|
| 112 |
+
for s_id, item in self.gj["sentences"].items():
|
| 113 |
+
text = item["text"]
|
| 114 |
+
ef, er = expected_frames(prior, "mr", len(text))
|
| 115 |
+
self.assertAlmostEqual(ef, item["ef"], places=9, msg=s_id)
|
| 116 |
+
self.assertAlmostEqual(er, item["er"], places=9, msg=s_id)
|
| 117 |
+
self.assertEqual(frame_cap(text, "mr", ef, er, self.controls), item["cap"], s_id)
|
| 118 |
+
self.assertEqual(min_frames(ef), item["minimum"], s_id)
|
| 119 |
+
self.assertEqual(split_text(text, "mr", prior, chunk_s=16.0), item["split_text"], s_id)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
unittest.main()
|
tests/test_maya_fast_depth.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import tempfile
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from tests.fixtures.tiny_assets import make_tiny_assets
|
| 7 |
+
from src.maya_fast.depth import Depth
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TestMayaFastDepth(unittest.TestCase):
|
| 11 |
+
def setUp(self):
|
| 12 |
+
self.temp_dir = tempfile.TemporaryDirectory()
|
| 13 |
+
self.assets_dir = make_tiny_assets(Path(self.temp_dir.name))
|
| 14 |
+
|
| 15 |
+
def tearDown(self):
|
| 16 |
+
self.temp_dir.cleanup()
|
| 17 |
+
|
| 18 |
+
def test_depth_runtime_shapes(self):
|
| 19 |
+
# tiny assets: frame() returns 16 codes in [0, 2048), codes[0] == cb0
|
| 20 |
+
depth_path = self.assets_dir / "depth_frame_fp16.onnx"
|
| 21 |
+
depth = Depth(depth_path, threads=1)
|
| 22 |
+
|
| 23 |
+
d = 16
|
| 24 |
+
vocab = 64
|
| 25 |
+
d_mem = 16
|
| 26 |
+
|
| 27 |
+
h = np.random.randn(d).astype(np.float32)
|
| 28 |
+
cb0 = 17
|
| 29 |
+
mem = np.random.randn(1, 1, d_mem).astype(np.float32)
|
| 30 |
+
noise = np.random.randn(15, vocab).astype(np.float32)
|
| 31 |
+
|
| 32 |
+
codes = depth.frame(h, cb0, mem, noise)
|
| 33 |
+
|
| 34 |
+
self.assertEqual(codes.shape, (16,))
|
| 35 |
+
self.assertEqual(codes.dtype, np.int64)
|
| 36 |
+
self.assertEqual(codes[0], cb0)
|
| 37 |
+
self.assertTrue(np.all((codes >= 0) & (codes < 2048)))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
unittest.main()
|
tests/test_maya_fast_engine.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ast
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import tempfile
|
| 6 |
+
import unittest
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from tests.fixtures.tiny_assets import make_tiny_assets
|
| 10 |
+
from src.maya_fast.engine import MayaFast
|
| 11 |
+
from src.maya_fast.control import split_text
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestMayaFastEngine(unittest.TestCase):
|
| 15 |
+
def setUp(self):
|
| 16 |
+
self.temp_dir = tempfile.TemporaryDirectory()
|
| 17 |
+
self.assets_dir = make_tiny_assets(Path(self.temp_dir.name))
|
| 18 |
+
|
| 19 |
+
def tearDown(self):
|
| 20 |
+
self.temp_dir.cleanup()
|
| 21 |
+
|
| 22 |
+
def test_synthesize_returns_audio(self):
|
| 23 |
+
# test_synthesize_returns_audio (length = frames * 1920)
|
| 24 |
+
engine = MayaFast(assets_dir=self.assets_dir, threads=1)
|
| 25 |
+
wav, metrics = engine.synthesize("t_1 t_2", voice="Charvi", seed=42)
|
| 26 |
+
self.assertEqual(len(wav), metrics["frames"] * 1920)
|
| 27 |
+
self.assertEqual(wav.dtype, np.float32)
|
| 28 |
+
self.assertIn("audio_s", metrics)
|
| 29 |
+
self.assertIn("wall_s", metrics)
|
| 30 |
+
self.assertIn("rtf", metrics)
|
| 31 |
+
self.assertIn("reasons", metrics)
|
| 32 |
+
|
| 33 |
+
def test_same_seed_same_audio(self):
|
| 34 |
+
engine = MayaFast(assets_dir=self.assets_dir, threads=1)
|
| 35 |
+
wav1, _ = engine.synthesize("t_1 t_2", voice="Charvi", seed=123)
|
| 36 |
+
wav2, _ = engine.synthesize("t_1 t_2", voice="Charvi", seed=123)
|
| 37 |
+
np.testing.assert_array_equal(wav1, wav2)
|
| 38 |
+
|
| 39 |
+
def test_empty_text_raises(self):
|
| 40 |
+
engine = MayaFast(assets_dir=self.assets_dir, threads=1)
|
| 41 |
+
with self.assertRaises(ValueError):
|
| 42 |
+
engine.synthesize("", voice="Charvi")
|
| 43 |
+
with self.assertRaises(ValueError):
|
| 44 |
+
engine.synthesize("।,.", voice="Charvi")
|
| 45 |
+
|
| 46 |
+
def test_cap_stops_generation(self):
|
| 47 |
+
# tiny assets built with an aeos logit bias of -100 -> reasons == ["cap"], frames == cap
|
| 48 |
+
with tempfile.TemporaryDirectory() as td:
|
| 49 |
+
biased_assets_dir = make_tiny_assets(Path(td), aeos_bias=-100.0)
|
| 50 |
+
engine = MayaFast(assets_dir=biased_assets_dir, threads=1)
|
| 51 |
+
wav, metrics = engine.synthesize("t_1", voice="Charvi", seed=0)
|
| 52 |
+
self.assertEqual(metrics["reasons"], ["cap"])
|
| 53 |
+
|
| 54 |
+
def test_long_text_is_split_and_kv_bounded(self):
|
| 55 |
+
# 3-sentence text -> len(reasons) == len(split_text(...)), and Backbone.past_len never exceeds the largest piece's prompt + cap
|
| 56 |
+
text = "t_1 t_2. t_1 t_3. t_2 t_3."
|
| 57 |
+
engine = MayaFast(assets_dir=self.assets_dir, threads=1)
|
| 58 |
+
prior = engine.controls.get("text_frames_prior", {})
|
| 59 |
+
expected_chunks = split_text(text, "mr", prior, chunk_s=0.5)
|
| 60 |
+
|
| 61 |
+
wav, metrics = engine.synthesize(text, voice="Charvi", seed=0, chunk_s=0.5)
|
| 62 |
+
self.assertEqual(len(metrics["reasons"]), len(expected_chunks))
|
| 63 |
+
self.assertLessEqual(metrics["max_past_len"], metrics["max_chunk_bound"])
|
| 64 |
+
|
| 65 |
+
def test_thread_count_does_not_change_output(self):
|
| 66 |
+
# threads 1 and 16 -> identical wav
|
| 67 |
+
engine1 = MayaFast(assets_dir=self.assets_dir, threads=1)
|
| 68 |
+
engine2 = MayaFast(assets_dir=self.assets_dir, threads=16)
|
| 69 |
+
wav1, _ = engine1.synthesize("t_1 t_2", voice="Charvi", seed=7)
|
| 70 |
+
wav2, _ = engine2.synthesize("t_1 t_2", voice="Charvi", seed=7)
|
| 71 |
+
np.testing.assert_array_equal(wav1, wav2)
|
| 72 |
+
|
| 73 |
+
def test_unknown_voice_lists_voices(self):
|
| 74 |
+
engine = MayaFast(assets_dir=self.assets_dir, threads=1)
|
| 75 |
+
with self.assertRaises(KeyError) as ctx:
|
| 76 |
+
engine.synthesize("t_1", voice="UnknownVoice")
|
| 77 |
+
self.assertIn("Charvi", str(ctx.exception))
|
| 78 |
+
|
| 79 |
+
def test_runtime_has_no_torch(self):
|
| 80 |
+
# ast scan of every file under src/maya_fast/: no torch import
|
| 81 |
+
pkg_dir = Path(__file__).resolve().parent.parent / "src" / "maya_fast"
|
| 82 |
+
for py_file in pkg_dir.glob("**/*.py"):
|
| 83 |
+
with open(py_file, "r", encoding="utf-8") as f:
|
| 84 |
+
tree = ast.parse(f.read(), filename=str(py_file))
|
| 85 |
+
for node in ast.walk(tree):
|
| 86 |
+
if isinstance(node, ast.Import):
|
| 87 |
+
for alias in node.names:
|
| 88 |
+
self.assertNotEqual(alias.name.split(".")[0], "torch", f"torch imported in {py_file}")
|
| 89 |
+
elif isinstance(node, ast.ImportFrom):
|
| 90 |
+
if node.module:
|
| 91 |
+
self.assertNotEqual(node.module.split(".")[0], "torch", f"torch imported in {py_file}")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
unittest.main()
|