Spaces:
Running on Zero
Running on Zero
Polish unified Audex demo
Browse filesAdd unified 30B-A3B and 2B inference, streaming text and speech generation, reasoning controls, curated examples, local setup scripts, and ZeroGPU runtime support.
- .gitattributes +3 -0
- .gitignore +6 -0
- README.md +21 -16
- app.py +1451 -152
- examples/korean_speech.wav +3 -0
- examples/question_1059.mp3 +3 -0
- reasoning_utils.py +83 -0
- requirements.txt +1 -1
- run_local.sh +50 -0
- setup_local.sh +23 -0
- tts_player.py +199 -0
- tts_utils.py +314 -0
- web/PCMPlayerWorklet.js +55 -0
.gitattributes
CHANGED
|
@@ -35,5 +35,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
examples/mlk_speech.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
examples/sample_speech.wav filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 38 |
wheels/causal_conv1d-1.6.2.post1-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 39 |
wheels/mamba_ssm-2.3.2.post1-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
examples/mlk_speech.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
examples/sample_speech.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
wheels/*.whl filter=lfs diff=lfs merge=lfs -text
|
| 39 |
wheels/causal_conv1d-1.6.2.post1-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 40 |
wheels/mamba_ssm-2.3.2.post1-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/korean_speech.wav filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
examples/question_1059.mp3 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.cache/
|
| 2 |
+
.local/
|
| 3 |
+
.venv/
|
| 4 |
+
__pycache__/
|
| 5 |
+
_wheelout/
|
| 6 |
+
*.pyc
|
README.md
CHANGED
|
@@ -1,32 +1,37 @@
|
|
| 1 |
---
|
| 2 |
-
title: Nemotron
|
| 3 |
emoji: 🎧
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
| 8 |
app_file: app.py
|
| 9 |
-
short_description:
|
| 10 |
python_version: "3.10"
|
| 11 |
startup_duration_timeout: 1h
|
| 12 |
---
|
| 13 |
|
| 14 |
-
# Nemotron-Labs-Audex
|
| 15 |
|
| 16 |
-
A Gradio demo for
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
-
require the compiled CUDA fast path from `mamba-ssm` (`selective_scan_cuda`) and
|
| 25 |
-
`causal-conv1d`; the pure-torch fallback produces degenerate output. This Space
|
| 26 |
-
builds both extensions from source against the runtime's exact toolchain
|
| 27 |
-
(torch 2.11.0+cu130, CUDA 13.0, Python 3.10, sm_120) on first boot and caches
|
| 28 |
-
the resulting wheels back into the repo (`wheels/`) for fast subsequent boots.
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Nemotron-Labs-Audex
|
| 3 |
emoji: 🎧
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Unified audio-text intelligence
|
| 10 |
python_version: "3.10"
|
| 11 |
startup_duration_timeout: 1h
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Nemotron-Labs-Audex
|
| 15 |
|
| 16 |
+
A Gradio demo for
|
| 17 |
+
[`nvidia/Nemotron-Labs-Audex-30B-A3B`](https://huggingface.co/nvidia/Nemotron-Labs-Audex-30B-A3B)
|
| 18 |
+
and [`nvidia/Nemotron-Labs-Audex-2B`](https://huggingface.co/nvidia/Nemotron-Labs-Audex-2B).
|
| 19 |
+
The model selector defaults to the 30B-A3B and can switch to 2B model.
|
| 20 |
|
| 21 |
+
The demo includes **audio understanding, speech recognition, speech translation, text reasoning, text-to-speech, and speech-to-speech**.
|
| 22 |
|
| 23 |
+
Reasoning has no separate token cap by default for either model. `Max new tokens` sets a total cap shared by reasoning and the final answer.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
## Run locally
|
| 26 |
|
| 27 |
+
```bash
|
| 28 |
+
git clone https://huggingface.co/spaces/L0SG/Nemotron-Labs-Audex
|
| 29 |
+
cd Nemotron-Labs-Audex
|
| 30 |
+
bash setup_local.sh
|
| 31 |
+
bash run_local.sh
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
`setup_local.sh` creates an isolated `.venv` and installs PyTorch when needed.
|
| 35 |
+
Use `AUDEX_TORCH_PACKAGE` or standard pip index environment variables to select a platform-specific PyTorch build.
|
| 36 |
+
|
| 37 |
+
`run_local.sh` detects the selected GPU architecture and creates a matching local CUDA-extension cache.
|
app.py
CHANGED
|
@@ -1,41 +1,233 @@
|
|
| 1 |
"""
|
| 2 |
-
Nemotron-Labs-Audex
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
/ sm_120 toolchain, cache the resulting wheels back into this Space repo, and
|
| 10 |
-
reuse them on subsequent boots.
|
| 11 |
"""
|
| 12 |
import os
|
| 13 |
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 14 |
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 15 |
|
| 16 |
import glob
|
|
|
|
|
|
|
|
|
|
| 17 |
import subprocess
|
| 18 |
import sys
|
| 19 |
import time
|
|
|
|
|
|
|
|
|
|
| 20 |
from pathlib import Path
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Import spaces FIRST (before torch / any CUDA-touching import) so its
|
| 23 |
# torch.cuda.* monkey-patch is installed. The kernel build below runs in
|
| 24 |
# subprocesses, so it does not initialize CUDA in this process.
|
| 25 |
import spaces # noqa: E402
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
APP_DIR = Path(__file__).resolve().parent
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
-
def _run(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
print(f"[build] $ {' '.join(cmd)}", flush=True)
|
| 40 |
p = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=timeout)
|
| 41 |
if p.stdout:
|
|
@@ -50,45 +242,128 @@ def _run(cmd, env=None, timeout=3000):
|
|
| 50 |
return p
|
| 51 |
|
| 52 |
|
| 53 |
-
def
|
| 54 |
try:
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
-
def _pip(*args):
|
| 62 |
-
_run([sys.executable, "-m", "pip"
|
| 63 |
|
| 64 |
|
| 65 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
env = dict(os.environ)
|
| 67 |
env["MAMBA_FORCE_BUILD"] = "TRUE"
|
| 68 |
env["CAUSAL_CONV1D_FORCE_BUILD"] = "TRUE"
|
| 69 |
-
env["TORCH_CUDA_ARCH_LIST"] =
|
| 70 |
env["MAX_JOBS"] = env.get("MAX_JOBS", "4")
|
| 71 |
-
|
| 72 |
-
cuda_home = env.get("CUDA_HOME") or "/cuda-image/usr/local/cuda-13.0"
|
| 73 |
env["CUDA_HOME"] = cuda_home
|
| 74 |
env["PATH"] = f"{cuda_home}/bin:" + env.get("PATH", "")
|
| 75 |
return env
|
| 76 |
|
| 77 |
|
| 78 |
-
def ensure_kernels():
|
| 79 |
"""Install compiled causal-conv1d + mamba-ssm. Use cached wheels if present,
|
| 80 |
otherwise build from source and cache the wheels back into the repo."""
|
| 81 |
-
if
|
| 82 |
print("[build] kernels already importable", flush=True)
|
| 83 |
return
|
| 84 |
|
| 85 |
-
WHEELS_DIR.mkdir(exist_ok=True)
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
print(f"[build] installing cached wheels: {cached}", flush=True)
|
| 89 |
try:
|
| 90 |
_pip("install", "--no-deps", "--no-build-isolation", *cached)
|
| 91 |
-
if
|
| 92 |
print("[build] cached wheels installed OK", flush=True)
|
| 93 |
return
|
| 94 |
print("[build] cached wheels imported incompletely; rebuilding", flush=True)
|
|
@@ -96,25 +371,47 @@ def ensure_kernels():
|
|
| 96 |
print(f"[build] cached wheel install failed ({e!r}); rebuilding", flush=True)
|
| 97 |
|
| 98 |
env = _build_env()
|
| 99 |
-
|
| 100 |
-
out_dir.mkdir(exist_ok=True)
|
| 101 |
|
| 102 |
# Build wheels (no-build-isolation => uses the preinstalled torch).
|
| 103 |
t0 = time.time()
|
| 104 |
print("[build] building causal-conv1d + mamba-ssm from source (this can take ~20 min)", flush=True)
|
| 105 |
-
_pip(
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# mamba-ssm needs causal-conv1d importable during its own build; install it first.
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
print(f"[build] source build finished in {time.time()-t0:.0f}s", flush=True)
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
_pip("install", "--no-deps", *all_wheels)
|
| 116 |
|
| 117 |
-
if not (
|
| 118 |
raise RuntimeError("kernel build completed but imports still fail")
|
| 119 |
print("[build] kernels built + installed OK", flush=True)
|
| 120 |
|
|
@@ -125,6 +422,10 @@ def ensure_kernels():
|
|
| 125 |
if not dst.exists():
|
| 126 |
import shutil
|
| 127 |
shutil.copy(w, dst)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
from huggingface_hub import HfApi
|
| 129 |
tok = os.environ.get("HF_TOKEN")
|
| 130 |
if tok:
|
|
@@ -147,8 +448,14 @@ ensure_kernels()
|
|
| 147 |
# ---- Now safe to bring in torch / model ----
|
| 148 |
import torch
|
| 149 |
import gradio as gr
|
|
|
|
| 150 |
from huggingface_hub import snapshot_download
|
| 151 |
-
from transformers import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
from audio_utils import (
|
| 154 |
IM_END_TOKEN,
|
|
@@ -160,41 +467,37 @@ from audio_utils import (
|
|
| 160 |
resolve_audio_preprocessor_path,
|
| 161 |
split_thinking,
|
| 162 |
)
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
token=os.environ.get("HF_TOKEN"),
|
| 173 |
)
|
| 174 |
-
MODEL_PATH = os.path.join(LOCAL_DIR, SUBFOLDER)
|
| 175 |
-
print(f"[load] checkpoint at {MODEL_PATH}", flush=True)
|
| 176 |
|
| 177 |
-
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
|
| 178 |
-
config = AutoConfig.from_pretrained(MODEL_PATH, trust_remote_code=True)
|
| 179 |
-
feature_extractor = AutoFeatureExtractor.from_pretrained(
|
| 180 |
-
resolve_audio_preprocessor_path(MODEL_PATH, config)
|
| 181 |
-
)
|
| 182 |
-
print("[load] tokenizer/config/feature_extractor loaded", flush=True)
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
)
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
| 198 |
|
| 199 |
|
| 200 |
def _find_fast_path_flag() -> bool | None:
|
|
@@ -204,16 +507,168 @@ def _find_fast_path_flag() -> bool | None:
|
|
| 204 |
return None
|
| 205 |
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
|
| 219 |
def _probe_audio_duration(audio: str | None) -> float:
|
|
@@ -248,7 +703,138 @@ def _estimate(
|
|
| 248 |
return MAX_GPU_DURATION_SECONDS
|
| 249 |
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
def _generate(
|
|
|
|
| 252 |
audio: str,
|
| 253 |
prompt: str,
|
| 254 |
reasoning: bool,
|
|
@@ -256,10 +842,11 @@ def _generate(
|
|
| 256 |
temperature: float,
|
| 257 |
top_p: float,
|
| 258 |
greedy: bool,
|
| 259 |
-
|
|
|
|
| 260 |
started_at = time.perf_counter()
|
| 261 |
-
|
| 262 |
-
|
| 263 |
|
| 264 |
wav, sr = load_audio(audio, target_sr=SAMPLE_RATE)
|
| 265 |
audio_duration = wav.shape[-1] / sr
|
|
@@ -270,24 +857,41 @@ def _generate(
|
|
| 270 |
)
|
| 271 |
if max_new_tokens > MAX_NEW_TOKENS:
|
| 272 |
raise gr.Error(f"This demo supports up to {MAX_NEW_TOKENS} output tokens.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
input_features = extract_whisper_features(
|
| 275 |
-
feature_extractor,
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
)
|
| 278 |
-
num_embeddings = input_features.shape[0] * int(getattr(config, "sound_embedding_size", 750))
|
| 279 |
formatted = build_prompt_template(prompt.strip(), reasoning=bool(reasoning),
|
| 280 |
prompt_repitition="none")
|
| 281 |
expanded = expand_sound_placeholder(formatted, num_embeddings)
|
| 282 |
-
tok = tokenizer(expanded, return_tensors="pt", add_special_tokens=False)
|
| 283 |
input_ids = tok.input_ids.to("cuda")
|
| 284 |
attention_mask = (tok.attention_mask if "attention_mask" in tok
|
| 285 |
else build_attention_mask(input_ids)).to("cuda")
|
| 286 |
input_features = input_features.to("cuda")
|
| 287 |
|
| 288 |
-
eos_token_id = tokenizer.convert_tokens_to_ids(IM_END_TOKEN)
|
| 289 |
-
if eos_token_id is None or eos_token_id == tokenizer.unk_token_id:
|
| 290 |
-
eos_token_id = getattr(config, "eos_token_id", None)
|
| 291 |
|
| 292 |
temperature = max(float(temperature), 1e-4)
|
| 293 |
top_p = float(top_p)
|
|
@@ -295,31 +899,36 @@ def _generate(
|
|
| 295 |
gen_kwargs = dict(
|
| 296 |
do_sample=do_sample,
|
| 297 |
eos_token_id=eos_token_id,
|
| 298 |
-
pad_token_id=tokenizer.pad_token_id
|
|
|
|
| 299 |
max_new_tokens=int(max_new_tokens),
|
|
|
|
| 300 |
)
|
|
|
|
| 301 |
if do_sample:
|
| 302 |
gen_kwargs["temperature"] = temperature
|
| 303 |
if top_p > 0.0:
|
| 304 |
gen_kwargs["top_p"] = top_p
|
| 305 |
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
| 317 |
print(
|
| 318 |
f"[gpu] audio_seconds={audio_duration:.1f} greedy={greedy} "
|
| 319 |
f"elapsed_seconds={time.perf_counter() - started_at:.1f}",
|
| 320 |
flush=True,
|
| 321 |
)
|
| 322 |
-
return thinking, prediction
|
| 323 |
|
| 324 |
|
| 325 |
@spaces.GPU(duration=_estimate, size="xlarge")
|
|
@@ -336,7 +945,7 @@ def run(
|
|
| 336 |
|
| 337 |
Predefined ASR and translation tasks use the model's recommended greedy
|
| 338 |
decoding. Other tasks and custom instructions use the sampling controls.
|
| 339 |
-
|
| 340 |
|
| 341 |
Returns:
|
| 342 |
The final answer and, when enabled, the reasoning trace.
|
|
@@ -345,9 +954,14 @@ def run(
|
|
| 345 |
return "Please provide an audio input.", ""
|
| 346 |
|
| 347 |
custom_instruction = custom_prompt.strip() if custom_prompt else ""
|
| 348 |
-
prompt = custom_instruction or
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
audio,
|
| 352 |
prompt,
|
| 353 |
reasoning,
|
|
@@ -355,77 +969,762 @@ def run(
|
|
| 355 |
temperature=float(temperature),
|
| 356 |
top_p=float(top_p),
|
| 357 |
greedy=greedy,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
return answer, thinking
|
| 360 |
|
| 361 |
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
| 365 |
gr.Markdown(
|
| 366 |
-
"# 🎧 Nemotron-Labs-Audex
|
| 367 |
-
"
|
| 368 |
-
|
| 369 |
-
"
|
| 370 |
-
|
| 371 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
)
|
| 373 |
with gr.Row():
|
| 374 |
with gr.Column():
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
type="filepath",
|
| 377 |
-
label="Input audio (up to
|
| 378 |
sources=["upload", "microphone"],
|
| 379 |
)
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
)
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
lines=2,
|
| 389 |
)
|
| 390 |
-
|
|
|
|
|
|
|
| 391 |
with gr.Accordion("Advanced options", open=False):
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
16,
|
| 395 |
MAX_NEW_TOKENS,
|
| 396 |
-
value=
|
| 397 |
step=16,
|
| 398 |
label="Max new tokens",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
)
|
| 400 |
-
temperature = gr.Slider(0.1, 1.5, value=0.7, step=0.05, label="Temperature")
|
| 401 |
-
top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p")
|
| 402 |
gr.Markdown(
|
| 403 |
-
"
|
| 404 |
-
"
|
|
|
|
|
|
|
| 405 |
)
|
| 406 |
with gr.Column():
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
inputs=[
|
| 413 |
-
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
)
|
|
|
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
gr.Examples(
|
| 418 |
examples=[
|
| 419 |
-
[
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
],
|
| 423 |
-
inputs=[audio_in, task, custom_prompt, reasoning, max_new_tokens, temperature, top_p],
|
| 424 |
-
outputs=[answer_out, thinking_out],
|
| 425 |
-
fn=run,
|
| 426 |
cache_examples=False,
|
| 427 |
-
run_on_click=True,
|
| 428 |
)
|
| 429 |
|
| 430 |
if __name__ == "__main__":
|
| 431 |
-
demo.queue(max_size=8).launch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Nemotron-Labs-Audex — unified audio and text demo for the 30B-A3B and 2B models.
|
| 3 |
+
|
| 4 |
+
The 30B-A3B model uses a Mamba2-Transformer Hybrid MoE backbone (30B total,
|
| 5 |
+
3B active). Its correct numerics require the compiled CUDA fast path from
|
| 6 |
+
`mamba-ssm` and `causal-conv1d`; the pure-PyTorch fallback produces degenerate
|
| 7 |
+
or repeated tokens. The Space reuses its cached Blackwell wheels, while local
|
| 8 |
+
runtimes can build and cache wheels for their CUDA architecture.
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
import os
|
| 11 |
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 12 |
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 13 |
|
| 14 |
import glob
|
| 15 |
+
import hashlib
|
| 16 |
+
import importlib.util
|
| 17 |
+
import re
|
| 18 |
import subprocess
|
| 19 |
import sys
|
| 20 |
import time
|
| 21 |
+
from collections.abc import Iterator
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
from importlib.metadata import PackageNotFoundError, version
|
| 24 |
from pathlib import Path
|
| 25 |
+
from threading import Event, Thread
|
| 26 |
+
from types import ModuleType
|
| 27 |
|
| 28 |
# Import spaces FIRST (before torch / any CUDA-touching import) so its
|
| 29 |
# torch.cuda.* monkey-patch is installed. The kernel build below runs in
|
| 30 |
# subprocesses, so it does not initialize CUDA in this process.
|
| 31 |
import spaces # noqa: E402
|
| 32 |
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class AudexRuntime:
|
| 36 |
+
model: object
|
| 37 |
+
tokenizer: object
|
| 38 |
+
config: object
|
| 39 |
+
feature_extractor: object
|
| 40 |
+
speech_decoder: object
|
| 41 |
+
cache_implementation: str | None = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _max_reasoning_budget(max_new_tokens: int) -> int:
|
| 45 |
+
budget = max(16, (int(max_new_tokens) - 16) * 10 // 11)
|
| 46 |
+
return max(16, budget // 16 * 16)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# Runtime and build configuration
|
| 50 |
APP_DIR = Path(__file__).resolve().parent
|
| 51 |
+
IS_ZERO_GPU = os.environ.get("SPACES_ZERO_GPU") == "1"
|
| 52 |
+
DEFAULT_WHEELS_DIR = APP_DIR / ("wheels" if IS_ZERO_GPU else ".local/wheels")
|
| 53 |
+
DEFAULT_BUILD_DIR = APP_DIR / ("_wheelout" if IS_ZERO_GPU else ".local/build")
|
| 54 |
+
WHEELS_DIR = Path(os.environ.get("AUDEX_WHEELS_DIR", str(DEFAULT_WHEELS_DIR)))
|
| 55 |
+
BUILD_DIR = Path(os.environ.get("AUDEX_BUILD_DIR", str(DEFAULT_BUILD_DIR)))
|
| 56 |
+
REPO_ID = os.environ.get("SPACE_ID", "L0SG/Nemotron-Labs-Audex")
|
| 57 |
+
CAUSAL_CONV1D_VERSION = "1.6.2.post1"
|
| 58 |
+
MAMBA_SSM_VERSION = "2.3.1"
|
| 59 |
+
CAUSAL_CONV1D_SPEC = f"causal-conv1d=={CAUSAL_CONV1D_VERSION}"
|
| 60 |
+
MAMBA_SSM_SPEC = f"mamba-ssm=={MAMBA_SSM_VERSION}"
|
| 61 |
+
TORCH_ARCH = os.environ.get("AUDEX_TORCH_ARCH", "12.0" if IS_ZERO_GPU else "")
|
| 62 |
+
|
| 63 |
+
# Model configuration
|
| 64 |
+
MODEL_30B_ID = "nvidia/Nemotron-Labs-Audex-30B-A3B"
|
| 65 |
+
MODEL_2B_ID = "nvidia/Nemotron-Labs-Audex-2B"
|
| 66 |
+
MODEL_SUBFOLDER = "checkpoint_folder_full"
|
| 67 |
+
DECODER_SUBFOLDER = "audex_causal_speech_decoder"
|
| 68 |
+
DEFAULT_MODEL_NAME = "Nemotron-Labs-Audex-30B-A3B"
|
| 69 |
+
MODEL_2B_NAME = "Nemotron-Labs-Audex-2B"
|
| 70 |
+
|
| 71 |
+
# Shared generation configuration
|
| 72 |
+
SAMPLE_RATE = 16000
|
| 73 |
+
MAX_AUDIO_DURATION_SECONDS = float(os.environ.get("AUDEX_MAX_AUDIO_SECONDS", "120"))
|
| 74 |
+
MAX_NEW_TOKENS = int(os.environ.get("AUDEX_MAX_NEW_TOKENS", "2048"))
|
| 75 |
+
DEFAULT_MAX_NEW_TOKENS = min(
|
| 76 |
+
int(os.environ.get("AUDEX_DEFAULT_MAX_NEW_TOKENS", "256")),
|
| 77 |
+
MAX_NEW_TOKENS,
|
| 78 |
+
)
|
| 79 |
+
DEFAULT_REASONING_BUDGET = max(
|
| 80 |
+
0,
|
| 81 |
+
min(
|
| 82 |
+
int(os.environ.get("AUDEX_DEFAULT_REASONING_BUDGET", "0")),
|
| 83 |
+
_max_reasoning_budget(DEFAULT_MAX_NEW_TOKENS),
|
| 84 |
+
),
|
| 85 |
+
)
|
| 86 |
+
STREAM_CHUNK_SIZE = max(1, int(os.environ.get("AUDEX_STREAM_CHUNK_SIZE", "8")))
|
| 87 |
+
MAX_GPU_DURATION_SECONDS = 60
|
| 88 |
+
MAX_TOKEN_WARNING = (
|
| 89 |
+
"⚠️ Maximum new-token limit reached; this output is incomplete. "
|
| 90 |
+
"Increase Max new tokens and run again."
|
| 91 |
+
)
|
| 92 |
+
TEXT_VOCAB_SIZE = 131072
|
| 93 |
+
TEXT_SEED = 100
|
| 94 |
+
TEXT_SYSTEM_PROMPT = (
|
| 95 |
+
"You are a helpful and harmless assistant.\n\n"
|
| 96 |
+
"You are not allowed to use any tools."
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Task names and prompts
|
| 100 |
+
TEXT_TASK = "Text reasoning"
|
| 101 |
+
TTS_TASK = "Text to speech (TTS)"
|
| 102 |
+
S2S_TASK = "Speech to speech (S2S)"
|
| 103 |
+
LEGACY_TASK_PROMPTS = {
|
| 104 |
+
"Describe the audio": "Describe the audio in detail.",
|
| 105 |
+
"Transcribe (ASR)": "Transcribe the speech in the input audio.",
|
| 106 |
+
"Translate speech to English": "Translate the spoken content in the audio to English.",
|
| 107 |
+
"Answer a question about the audio": "Where is the communication likely taking place?",
|
| 108 |
+
}
|
| 109 |
+
LEGACY_GREEDY_TASKS = {"Transcribe (ASR)", "Translate speech to English"}
|
| 110 |
+
MMLU_PRO_EXAMPLE_PROMPT = (
|
| 111 |
+
"Question:\n"
|
| 112 |
+
"Which organelle is primarily responsible for ATP production in eukaryotic cells?\n\n"
|
| 113 |
+
"Answer Choices:\n"
|
| 114 |
+
"(A) Golgi apparatus\n"
|
| 115 |
+
"(B) Mitochondrion\n"
|
| 116 |
+
"(C) Lysosome\n"
|
| 117 |
+
"(D) Endoplasmic reticulum\n\n"
|
| 118 |
+
"Conclude your response with the sentence `The answer is \\boxed{{X}}.`, "
|
| 119 |
+
"in which X is the correct capital letter of your choice."
|
| 120 |
+
)
|
| 121 |
+
S2S_RESPONSE_PROMPT = (
|
| 122 |
+
"SYSTEM & FORMATTING INSTRUCTION: You are Audex, created by NVIDIA based on "
|
| 123 |
+
"the Nemotron-Cascade-2 architecture. You may output your reasoning, followed "
|
| 124 |
+
"by your final response. You may format your reasoning block however you like. "
|
| 125 |
+
"However, your final response must strictly follow these rules: "
|
| 126 |
+
"* Write in plain, unformatted prose like a book or newspaper article. "
|
| 127 |
+
"* Do not use markdown, bullet points, lists, or headers. "
|
| 128 |
+
"* Standard numbers, abbreviations, and symbols are acceptable. "
|
| 129 |
+
"* [CRITICAL] You must press enter after every single sentence, placing each "
|
| 130 |
+
"sentence on its own separate line."
|
| 131 |
+
)
|
| 132 |
+
S2S_TASK_INSTRUCTION = (
|
| 133 |
+
"Use the response prompt above only to control the style of your final answer. "
|
| 134 |
+
"Do not quote, explain, or analyze the response prompt. "
|
| 135 |
+
"Answer the user request in the spoken transcript below. "
|
| 136 |
+
"The final answer must be one short, self-contained sentence of at most "
|
| 137 |
+
"20 words while preserving every requested name, number, and conclusion."
|
| 138 |
+
)
|
| 139 |
+
S2S_UI_PROMPT = f"{S2S_RESPONSE_PROMPT}\n\n{S2S_TASK_INSTRUCTION}"
|
| 140 |
+
TTS_EXAMPLE_TEXT = (
|
| 141 |
+
"Artificial intelligence is helping people understand and create sound in new ways."
|
| 142 |
+
)
|
| 143 |
|
| 144 |
+
# Task-specific limits and feature switches
|
| 145 |
+
TTS_MAX_NEW_TOKENS = min(
|
| 146 |
+
int(os.environ.get("AUDEX_TTS_MAX_NEW_TOKENS", "192")),
|
| 147 |
+
MAX_NEW_TOKENS,
|
| 148 |
+
)
|
| 149 |
+
S2S_TEXT_MAX_NEW_TOKENS = int(os.environ.get("AUDEX_S2S_TEXT_MAX_NEW_TOKENS", "2048"))
|
| 150 |
+
S2S_REASONING_BUDGET = int(os.environ.get("AUDEX_S2S_REASONING_BUDGET", "0"))
|
| 151 |
+
S2S_GPU_DURATION_SECONDS = int(os.environ.get("AUDEX_S2S_GPU_DURATION_SECONDS", "120"))
|
| 152 |
+
S2S_SPOKEN_MAX_WORDS = 20
|
| 153 |
+
S2S_TTS_MAX_NEW_TOKENS = int(os.environ.get("AUDEX_S2S_TTS_MAX_NEW_TOKENS", "2400"))
|
| 154 |
+
S2S_TTS_SEGMENT_SILENCE_SECONDS = 0.2
|
| 155 |
+
TTS_STREAMING_PLAYER_ENABLED = (
|
| 156 |
+
os.environ.get("AUDEX_TTS_STREAMING_PLAYER", "false").lower() == "true"
|
| 157 |
+
)
|
| 158 |
+
TASK_SPECS = {
|
| 159 |
+
"Speech recognition (ASR)": {
|
| 160 |
+
"modality": "audio",
|
| 161 |
+
"template": "<sound>\nTranscribe the speech in the input audio.",
|
| 162 |
+
"reasoning": False,
|
| 163 |
+
"temperature": 1.0,
|
| 164 |
+
"top_p": 1.0,
|
| 165 |
+
"greedy": True,
|
| 166 |
+
},
|
| 167 |
+
"Speech translation (AST)": {
|
| 168 |
+
"modality": "audio",
|
| 169 |
+
"template": "<sound>\nTranslate the spoken content in the audio to English.",
|
| 170 |
+
"reasoning": False,
|
| 171 |
+
"temperature": 1.0,
|
| 172 |
+
"top_p": 1.0,
|
| 173 |
+
"greedy": True,
|
| 174 |
+
},
|
| 175 |
+
"Audio description": {
|
| 176 |
+
"modality": "audio",
|
| 177 |
+
"template": "Describe the audio in detail.\n<sound>",
|
| 178 |
+
"reasoning": False,
|
| 179 |
+
"temperature": 0.7,
|
| 180 |
+
"top_p": 0.9,
|
| 181 |
+
"greedy": False,
|
| 182 |
+
},
|
| 183 |
+
"Audio question answering": {
|
| 184 |
+
"modality": "audio",
|
| 185 |
+
"template": "Where is the communication likely taking place?\n<sound>",
|
| 186 |
+
"reasoning": False,
|
| 187 |
+
"temperature": 0.7,
|
| 188 |
+
"top_p": 0.9,
|
| 189 |
+
"greedy": False,
|
| 190 |
+
},
|
| 191 |
+
TEXT_TASK: {
|
| 192 |
+
"modality": "text",
|
| 193 |
+
"template": MMLU_PRO_EXAMPLE_PROMPT,
|
| 194 |
+
"reasoning": True,
|
| 195 |
+
"temperature": 1.0,
|
| 196 |
+
"top_p": 0.95,
|
| 197 |
+
"greedy": False,
|
| 198 |
+
},
|
| 199 |
+
TTS_TASK: {
|
| 200 |
+
"modality": "tts",
|
| 201 |
+
"template": TTS_EXAMPLE_TEXT,
|
| 202 |
+
"reasoning": False,
|
| 203 |
+
"guidance_scale": 2.0,
|
| 204 |
+
"temperature": 0.8,
|
| 205 |
+
"top_p": 1.0,
|
| 206 |
+
"greedy": False,
|
| 207 |
+
},
|
| 208 |
+
S2S_TASK: {
|
| 209 |
+
"modality": "s2s",
|
| 210 |
+
"template": S2S_UI_PROMPT,
|
| 211 |
+
"reasoning": True,
|
| 212 |
+
"reasoning_budget": min(
|
| 213 |
+
S2S_REASONING_BUDGET,
|
| 214 |
+
_max_reasoning_budget(S2S_TEXT_MAX_NEW_TOKENS),
|
| 215 |
+
),
|
| 216 |
+
"guidance_scale": 1.5,
|
| 217 |
+
"temperature": 1.0,
|
| 218 |
+
"top_p": 0.95,
|
| 219 |
+
"greedy": False,
|
| 220 |
+
"max_new_tokens": S2S_TEXT_MAX_NEW_TOKENS,
|
| 221 |
+
"max_new_tokens_limit": S2S_TEXT_MAX_NEW_TOKENS,
|
| 222 |
+
},
|
| 223 |
+
}
|
| 224 |
|
| 225 |
|
| 226 |
+
def _run(
|
| 227 |
+
cmd: list[str],
|
| 228 |
+
env: dict[str, str] | None = None,
|
| 229 |
+
timeout: int = 3000,
|
| 230 |
+
) -> subprocess.CompletedProcess[str]:
|
| 231 |
print(f"[build] $ {' '.join(cmd)}", flush=True)
|
| 232 |
p = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=timeout)
|
| 233 |
if p.stdout:
|
|
|
|
| 242 |
return p
|
| 243 |
|
| 244 |
|
| 245 |
+
def _installed_version(package: str) -> str | None:
|
| 246 |
try:
|
| 247 |
+
return version(package)
|
| 248 |
+
except PackageNotFoundError:
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _kernels_ready() -> bool:
|
| 253 |
+
if (
|
| 254 |
+
_installed_version("causal-conv1d") != CAUSAL_CONV1D_VERSION
|
| 255 |
+
or _installed_version("mamba-ssm") != MAMBA_SSM_VERSION
|
| 256 |
+
):
|
| 257 |
return False
|
| 258 |
+
try:
|
| 259 |
+
from causal_conv1d import causal_conv1d_fn, causal_conv1d_update
|
| 260 |
+
from mamba_ssm.ops.triton.selective_state_update import selective_state_update
|
| 261 |
+
from mamba_ssm.ops.triton.ssd_combined import (
|
| 262 |
+
mamba_chunk_scan_combined,
|
| 263 |
+
mamba_split_conv1d_scan_combined,
|
| 264 |
+
)
|
| 265 |
+
except ImportError:
|
| 266 |
+
return False
|
| 267 |
+
return all(
|
| 268 |
+
kernel is not None
|
| 269 |
+
for kernel in (
|
| 270 |
+
causal_conv1d_fn,
|
| 271 |
+
causal_conv1d_update,
|
| 272 |
+
selective_state_update,
|
| 273 |
+
mamba_chunk_scan_combined,
|
| 274 |
+
mamba_split_conv1d_scan_combined,
|
| 275 |
+
)
|
| 276 |
+
)
|
| 277 |
|
| 278 |
|
| 279 |
+
def _pip(*args: str, env: dict[str, str] | None = None) -> None:
|
| 280 |
+
_run([sys.executable, "-m", "pip", *args], env=env)
|
| 281 |
|
| 282 |
|
| 283 |
+
def _detect_torch_arch() -> str:
|
| 284 |
+
if TORCH_ARCH:
|
| 285 |
+
return TORCH_ARCH
|
| 286 |
+
result = subprocess.run(
|
| 287 |
+
[
|
| 288 |
+
sys.executable,
|
| 289 |
+
"-c",
|
| 290 |
+
(
|
| 291 |
+
"import torch; "
|
| 292 |
+
"assert torch.cuda.is_available(), 'CUDA is not available'; "
|
| 293 |
+
"major, minor = torch.cuda.get_device_capability(); "
|
| 294 |
+
"print(f'{major}.{minor}')"
|
| 295 |
+
),
|
| 296 |
+
],
|
| 297 |
+
capture_output=True,
|
| 298 |
+
text=True,
|
| 299 |
+
)
|
| 300 |
+
if result.returncode != 0:
|
| 301 |
+
raise RuntimeError(
|
| 302 |
+
"Unable to detect the GPU architecture. Set AUDEX_TORCH_ARCH "
|
| 303 |
+
f"explicitly. Details: {result.stderr.strip()}"
|
| 304 |
+
)
|
| 305 |
+
return result.stdout.strip()
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def _detect_cuda_home() -> str:
|
| 309 |
+
if cuda_home := os.environ.get("CUDA_HOME"):
|
| 310 |
+
if (Path(cuda_home) / "bin/nvcc").is_file():
|
| 311 |
+
return cuda_home
|
| 312 |
+
raise RuntimeError(f"CUDA_HOME does not contain bin/nvcc: {cuda_home}")
|
| 313 |
+
if IS_ZERO_GPU:
|
| 314 |
+
return "/cuda-image/usr/local/cuda-13.0"
|
| 315 |
+
|
| 316 |
+
result = subprocess.run(
|
| 317 |
+
[
|
| 318 |
+
sys.executable,
|
| 319 |
+
"-c",
|
| 320 |
+
"from torch.utils.cpp_extension import CUDA_HOME; print(CUDA_HOME or '')",
|
| 321 |
+
],
|
| 322 |
+
capture_output=True,
|
| 323 |
+
text=True,
|
| 324 |
+
)
|
| 325 |
+
candidates = [result.stdout.strip(), "/usr/local/cuda"]
|
| 326 |
+
for candidate in candidates:
|
| 327 |
+
if candidate and (Path(candidate) / "bin/nvcc").is_file():
|
| 328 |
+
return candidate
|
| 329 |
+
raise RuntimeError(
|
| 330 |
+
"A CUDA toolkit with nvcc is required to build the 30B Mamba kernels. "
|
| 331 |
+
"Install the toolkit or set CUDA_HOME."
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def _build_env() -> dict[str, str]:
|
| 336 |
env = dict(os.environ)
|
| 337 |
env["MAMBA_FORCE_BUILD"] = "TRUE"
|
| 338 |
env["CAUSAL_CONV1D_FORCE_BUILD"] = "TRUE"
|
| 339 |
+
env["TORCH_CUDA_ARCH_LIST"] = _detect_torch_arch()
|
| 340 |
env["MAX_JOBS"] = env.get("MAX_JOBS", "4")
|
| 341 |
+
cuda_home = _detect_cuda_home()
|
|
|
|
| 342 |
env["CUDA_HOME"] = cuda_home
|
| 343 |
env["PATH"] = f"{cuda_home}/bin:" + env.get("PATH", "")
|
| 344 |
return env
|
| 345 |
|
| 346 |
|
| 347 |
+
def ensure_kernels() -> None:
|
| 348 |
"""Install compiled causal-conv1d + mamba-ssm. Use cached wheels if present,
|
| 349 |
otherwise build from source and cache the wheels back into the repo."""
|
| 350 |
+
if _kernels_ready():
|
| 351 |
print("[build] kernels already importable", flush=True)
|
| 352 |
return
|
| 353 |
|
| 354 |
+
WHEELS_DIR.mkdir(parents=True, exist_ok=True)
|
| 355 |
+
cached_causal = sorted(
|
| 356 |
+
glob.glob(str(WHEELS_DIR / f"causal_conv1d-{CAUSAL_CONV1D_VERSION}-*.whl"))
|
| 357 |
+
)
|
| 358 |
+
cached_mamba = sorted(
|
| 359 |
+
glob.glob(str(WHEELS_DIR / f"mamba_ssm-{MAMBA_SSM_VERSION}-*.whl"))
|
| 360 |
+
)
|
| 361 |
+
if cached_causal and cached_mamba:
|
| 362 |
+
cached = [cached_causal[-1], cached_mamba[-1]]
|
| 363 |
print(f"[build] installing cached wheels: {cached}", flush=True)
|
| 364 |
try:
|
| 365 |
_pip("install", "--no-deps", "--no-build-isolation", *cached)
|
| 366 |
+
if _kernels_ready():
|
| 367 |
print("[build] cached wheels installed OK", flush=True)
|
| 368 |
return
|
| 369 |
print("[build] cached wheels imported incompletely; rebuilding", flush=True)
|
|
|
|
| 371 |
print(f"[build] cached wheel install failed ({e!r}); rebuilding", flush=True)
|
| 372 |
|
| 373 |
env = _build_env()
|
| 374 |
+
BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 375 |
|
| 376 |
# Build wheels (no-build-isolation => uses the preinstalled torch).
|
| 377 |
t0 = time.time()
|
| 378 |
print("[build] building causal-conv1d + mamba-ssm from source (this can take ~20 min)", flush=True)
|
| 379 |
+
_pip(
|
| 380 |
+
"wheel",
|
| 381 |
+
"--no-build-isolation",
|
| 382 |
+
"--no-deps",
|
| 383 |
+
"-w",
|
| 384 |
+
str(BUILD_DIR),
|
| 385 |
+
CAUSAL_CONV1D_SPEC,
|
| 386 |
+
env=env,
|
| 387 |
+
)
|
| 388 |
# mamba-ssm needs causal-conv1d importable during its own build; install it first.
|
| 389 |
+
built_causal = sorted(
|
| 390 |
+
glob.glob(str(BUILD_DIR / f"causal_conv1d-{CAUSAL_CONV1D_VERSION}-*.whl"))
|
| 391 |
+
)
|
| 392 |
+
if not built_causal:
|
| 393 |
+
raise RuntimeError(f"build produced no wheel for {CAUSAL_CONV1D_SPEC}")
|
| 394 |
+
_pip("install", "--no-deps", built_causal[-1])
|
| 395 |
+
_pip(
|
| 396 |
+
"wheel",
|
| 397 |
+
"--no-build-isolation",
|
| 398 |
+
"--no-deps",
|
| 399 |
+
"-w",
|
| 400 |
+
str(BUILD_DIR),
|
| 401 |
+
MAMBA_SSM_SPEC,
|
| 402 |
+
env=env,
|
| 403 |
+
)
|
| 404 |
print(f"[build] source build finished in {time.time()-t0:.0f}s", flush=True)
|
| 405 |
|
| 406 |
+
built_mamba = sorted(
|
| 407 |
+
glob.glob(str(BUILD_DIR / f"mamba_ssm-{MAMBA_SSM_VERSION}-*.whl"))
|
| 408 |
+
)
|
| 409 |
+
if not built_mamba:
|
| 410 |
+
raise RuntimeError(f"build produced no wheel for {MAMBA_SSM_SPEC}")
|
| 411 |
+
all_wheels = [built_causal[-1], built_mamba[-1]]
|
| 412 |
_pip("install", "--no-deps", *all_wheels)
|
| 413 |
|
| 414 |
+
if not _kernels_ready():
|
| 415 |
raise RuntimeError("kernel build completed but imports still fail")
|
| 416 |
print("[build] kernels built + installed OK", flush=True)
|
| 417 |
|
|
|
|
| 422 |
if not dst.exists():
|
| 423 |
import shutil
|
| 424 |
shutil.copy(w, dst)
|
| 425 |
+
if not IS_ZERO_GPU:
|
| 426 |
+
print(f"[build] kernels cached locally in {WHEELS_DIR}", flush=True)
|
| 427 |
+
return
|
| 428 |
+
|
| 429 |
from huggingface_hub import HfApi
|
| 430 |
tok = os.environ.get("HF_TOKEN")
|
| 431 |
if tok:
|
|
|
|
| 448 |
# ---- Now safe to bring in torch / model ----
|
| 449 |
import torch
|
| 450 |
import gradio as gr
|
| 451 |
+
import numpy as np
|
| 452 |
from huggingface_hub import snapshot_download
|
| 453 |
+
from transformers import (
|
| 454 |
+
AutoConfig,
|
| 455 |
+
AutoFeatureExtractor,
|
| 456 |
+
AutoModelForCausalLM,
|
| 457 |
+
AutoTokenizer,
|
| 458 |
+
)
|
| 459 |
|
| 460 |
from audio_utils import (
|
| 461 |
IM_END_TOKEN,
|
|
|
|
| 467 |
resolve_audio_preprocessor_path,
|
| 468 |
split_thinking,
|
| 469 |
)
|
| 470 |
+
from reasoning_utils import ReasoningBudgetLogitsProcessor
|
| 471 |
+
from tts_player import TTS_PLAYER_CSS, TTS_PLAYER_JS, TTS_PLAYER_TEMPLATE, player_value
|
| 472 |
+
from tts_utils import (
|
| 473 |
+
EventStoppingCriteria,
|
| 474 |
+
TokenIdStreamer,
|
| 475 |
+
encode_pcm_chunk,
|
| 476 |
+
load_speech_decoder,
|
| 477 |
+
stream_tts,
|
| 478 |
+
write_wav,
|
|
|
|
| 479 |
)
|
|
|
|
|
|
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
|
| 482 |
+
def _load_native_module(model_path: str) -> ModuleType:
|
| 483 |
+
module_path = Path(model_path).resolve() / "modeling_nemotron_h_audio_native.py"
|
| 484 |
+
if not module_path.is_file():
|
| 485 |
+
raise FileNotFoundError(
|
| 486 |
+
f"Native Audex model adapter not found in checkpoint: {module_path}"
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
path_hash = hashlib.sha256(str(module_path).encode()).hexdigest()[:12]
|
| 490 |
+
module_name = f"audex_native_{path_hash}"
|
| 491 |
+
if module_name in sys.modules:
|
| 492 |
+
return sys.modules[module_name]
|
| 493 |
|
| 494 |
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
| 495 |
+
if spec is None or spec.loader is None:
|
| 496 |
+
raise ImportError(f"Unable to load native Audex model adapter: {module_path}")
|
| 497 |
+
module = importlib.util.module_from_spec(spec)
|
| 498 |
+
sys.modules[module_name] = module
|
| 499 |
+
spec.loader.exec_module(module)
|
| 500 |
+
return module
|
| 501 |
|
| 502 |
|
| 503 |
def _find_fast_path_flag() -> bool | None:
|
|
|
|
| 507 |
return None
|
| 508 |
|
| 509 |
|
| 510 |
+
def _resolve_model_paths(
|
| 511 |
+
model_id: str,
|
| 512 |
+
model_name: str,
|
| 513 |
+
model_path_env: str,
|
| 514 |
+
decoder_path_env: str,
|
| 515 |
+
) -> tuple[str, str]:
|
| 516 |
+
model_path_override = os.environ.get(model_path_env)
|
| 517 |
+
if model_path_override:
|
| 518 |
+
model_path = Path(model_path_override).expanduser().resolve()
|
| 519 |
+
if not model_path.is_dir():
|
| 520 |
+
raise FileNotFoundError(f"{model_path_env} does not exist: {model_path}")
|
| 521 |
+
model_root = model_path.parent
|
| 522 |
+
print(f"[load] using local {model_name} checkpoint at {model_path}", flush=True)
|
| 523 |
+
else:
|
| 524 |
+
print(f"[load] downloading {model_name} checkpoint…", flush=True)
|
| 525 |
+
model_root = Path(
|
| 526 |
+
snapshot_download(
|
| 527 |
+
model_id,
|
| 528 |
+
allow_patterns=[
|
| 529 |
+
f"{MODEL_SUBFOLDER}/*",
|
| 530 |
+
f"{DECODER_SUBFOLDER}/*",
|
| 531 |
+
],
|
| 532 |
+
token=os.environ.get("HF_TOKEN"),
|
| 533 |
+
)
|
| 534 |
+
)
|
| 535 |
+
model_path = model_root / MODEL_SUBFOLDER
|
| 536 |
+
|
| 537 |
+
decoder_path = (
|
| 538 |
+
Path(
|
| 539 |
+
os.environ.get(
|
| 540 |
+
decoder_path_env,
|
| 541 |
+
str(model_root / DECODER_SUBFOLDER),
|
| 542 |
+
)
|
| 543 |
+
)
|
| 544 |
+
.expanduser()
|
| 545 |
+
.resolve()
|
| 546 |
+
)
|
| 547 |
+
print(f"[load] {model_name} checkpoint at {model_path}", flush=True)
|
| 548 |
+
print(f"[load] {model_name} speech decoder at {decoder_path}", flush=True)
|
| 549 |
+
return str(model_path), str(decoder_path)
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def _load_30b_runtime() -> AudexRuntime:
|
| 553 |
+
model_path, decoder_path = _resolve_model_paths(
|
| 554 |
+
MODEL_30B_ID,
|
| 555 |
+
DEFAULT_MODEL_NAME,
|
| 556 |
+
"AUDEX_MODEL_PATH",
|
| 557 |
+
"AUDEX_DECODER_PATH",
|
| 558 |
+
)
|
| 559 |
+
native_module = _load_native_module(model_path)
|
| 560 |
+
config_class = native_module.NemotronHAudexConfig
|
| 561 |
+
model_class = native_module.NemotronHAudexForConditionalGeneration
|
| 562 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 563 |
+
chat_template_path = Path(model_path) / "chat_template.jinja"
|
| 564 |
+
if chat_template_path.is_file():
|
| 565 |
+
tokenizer.chat_template = chat_template_path.read_text()
|
| 566 |
+
config = config_class.from_pretrained(model_path)
|
| 567 |
+
feature_extractor = AutoFeatureExtractor.from_pretrained(
|
| 568 |
+
resolve_audio_preprocessor_path(model_path, config)
|
| 569 |
+
)
|
| 570 |
+
print(f"[load] {DEFAULT_MODEL_NAME} tokenizer/config/feature extractor loaded", flush=True)
|
| 571 |
+
|
| 572 |
+
model_load_kwargs = {
|
| 573 |
+
"pretrained_model_name_or_path": model_path,
|
| 574 |
+
"dtype": torch.bfloat16,
|
| 575 |
+
"low_cpu_mem_usage": True,
|
| 576 |
+
"output_loading_info": True,
|
| 577 |
+
}
|
| 578 |
+
if not IS_ZERO_GPU:
|
| 579 |
+
model_load_kwargs["device_map"] = {"": "cuda:0"}
|
| 580 |
+
model, loading_info = model_class.from_pretrained(**model_load_kwargs)
|
| 581 |
+
invalid_keys = {
|
| 582 |
+
key: loading_info[key]
|
| 583 |
+
for key in ("missing_keys", "unexpected_keys", "mismatched_keys")
|
| 584 |
+
if loading_info[key]
|
| 585 |
+
}
|
| 586 |
+
if invalid_keys:
|
| 587 |
+
raise RuntimeError(f"Native checkpoint loading was incomplete: {invalid_keys}")
|
| 588 |
+
model = model.eval()
|
| 589 |
+
if IS_ZERO_GPU:
|
| 590 |
+
model = model.to("cuda")
|
| 591 |
+
runtime_name = "packed by ZeroGPU" if IS_ZERO_GPU else "local CUDA"
|
| 592 |
+
print(f"[load] {DEFAULT_MODEL_NAME} loaded on {runtime_name}", flush=True)
|
| 593 |
+
|
| 594 |
+
if not _find_fast_path_flag():
|
| 595 |
+
raise RuntimeError(
|
| 596 |
+
"Transformers loaded without the native Mamba2-Transformer Hybrid fast path."
|
| 597 |
+
)
|
| 598 |
+
print("[load] native Mamba2-Transformer Hybrid fast path active", flush=True)
|
| 599 |
+
speech_decoder = load_speech_decoder(decoder_path)
|
| 600 |
+
print(f"[load] {DEFAULT_MODEL_NAME} speech decoder loaded", flush=True)
|
| 601 |
+
return AudexRuntime(
|
| 602 |
+
model=model,
|
| 603 |
+
tokenizer=tokenizer,
|
| 604 |
+
config=config,
|
| 605 |
+
feature_extractor=feature_extractor,
|
| 606 |
+
speech_decoder=speech_decoder,
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
def _load_2b_runtime() -> AudexRuntime:
|
| 611 |
+
model_path, decoder_path = _resolve_model_paths(
|
| 612 |
+
MODEL_2B_ID,
|
| 613 |
+
MODEL_2B_NAME,
|
| 614 |
+
"AUDEX_2B_MODEL_PATH",
|
| 615 |
+
"AUDEX_2B_DECODER_PATH",
|
| 616 |
+
)
|
| 617 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 618 |
+
chat_template_path = Path(model_path) / "chat_template.jinja"
|
| 619 |
+
if chat_template_path.is_file():
|
| 620 |
+
tokenizer.chat_template = chat_template_path.read_text()
|
| 621 |
+
config = AutoConfig.from_pretrained(model_path, trust_remote_code=True)
|
| 622 |
+
feature_extractor = AutoFeatureExtractor.from_pretrained(
|
| 623 |
+
resolve_audio_preprocessor_path(model_path, config)
|
| 624 |
+
)
|
| 625 |
+
model_load_kwargs = {
|
| 626 |
+
"pretrained_model_name_or_path": model_path,
|
| 627 |
+
"trust_remote_code": True,
|
| 628 |
+
"dtype": torch.bfloat16,
|
| 629 |
+
"low_cpu_mem_usage": True,
|
| 630 |
+
}
|
| 631 |
+
if not IS_ZERO_GPU:
|
| 632 |
+
model_load_kwargs["device_map"] = {"": "cuda:0"}
|
| 633 |
+
model = AutoModelForCausalLM.from_pretrained(**model_load_kwargs).eval()
|
| 634 |
+
if IS_ZERO_GPU:
|
| 635 |
+
model = model.to("cuda")
|
| 636 |
+
runtime_name = "packed by ZeroGPU" if IS_ZERO_GPU else "local CUDA"
|
| 637 |
+
print(f"[load] {MODEL_2B_NAME} loaded on {runtime_name}", flush=True)
|
| 638 |
+
speech_decoder = load_speech_decoder(decoder_path)
|
| 639 |
+
print(f"[load] {MODEL_2B_NAME} speech decoder loaded", flush=True)
|
| 640 |
+
return AudexRuntime(
|
| 641 |
+
model=model,
|
| 642 |
+
tokenizer=tokenizer,
|
| 643 |
+
config=config,
|
| 644 |
+
feature_extractor=feature_extractor,
|
| 645 |
+
speech_decoder=speech_decoder,
|
| 646 |
+
cache_implementation="static",
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
model_runtimes = {
|
| 651 |
+
DEFAULT_MODEL_NAME: _load_30b_runtime(),
|
| 652 |
+
MODEL_2B_NAME: _load_2b_runtime(),
|
| 653 |
}
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
def _blocked_output_token_ids(runtime: AudexRuntime) -> list[int]:
|
| 657 |
+
sound_token_ids = {
|
| 658 |
+
int(getattr(runtime.config, name))
|
| 659 |
+
for name in ("sound_token_id", "sound_start_token_id", "sound_end_token_id")
|
| 660 |
+
if getattr(runtime.config, name, None) is not None
|
| 661 |
+
}
|
| 662 |
+
return sorted(
|
| 663 |
+
set(range(TEXT_VOCAB_SIZE, int(runtime.config.vocab_size))) | sound_token_ids
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
def _get_runtime(model_name: str) -> AudexRuntime:
|
| 668 |
+
try:
|
| 669 |
+
return model_runtimes[model_name]
|
| 670 |
+
except KeyError as error:
|
| 671 |
+
raise gr.Error(f"Unknown model: {model_name}") from error
|
| 672 |
|
| 673 |
|
| 674 |
def _probe_audio_duration(audio: str | None) -> float:
|
|
|
|
| 703 |
return MAX_GPU_DURATION_SECONDS
|
| 704 |
|
| 705 |
|
| 706 |
+
def _stream_fields(response: str, reasoning: bool) -> tuple[str, str]:
|
| 707 |
+
visible = response
|
| 708 |
+
for marker in (IM_END_TOKEN, "<|end_of_text|>", "</s>"):
|
| 709 |
+
visible = visible.split(marker, 1)[0]
|
| 710 |
+
|
| 711 |
+
if not reasoning:
|
| 712 |
+
_, answer = split_thinking(visible)
|
| 713 |
+
return answer, ""
|
| 714 |
+
if "</think>" not in visible:
|
| 715 |
+
return "", visible.removeprefix("<think>").strip()
|
| 716 |
+
|
| 717 |
+
thinking, answer = visible.rsplit("</think>", 1)
|
| 718 |
+
return answer.strip(), thinking.removeprefix("<think>").strip()
|
| 719 |
+
|
| 720 |
+
|
| 721 |
+
def _stream_model_generate(
|
| 722 |
+
runtime: AudexRuntime,
|
| 723 |
+
generation_kwargs: dict[str, object],
|
| 724 |
+
reasoning: bool,
|
| 725 |
+
reasoning_budget: int | None = None,
|
| 726 |
+
) -> Iterator[tuple[str, str]]:
|
| 727 |
+
model_kwargs = dict(generation_kwargs)
|
| 728 |
+
eos_token_id = model_kwargs.get("eos_token_id")
|
| 729 |
+
eos_token_ids = {eos_token_id} if isinstance(eos_token_id, int) else set(eos_token_id or [])
|
| 730 |
+
logits_processors = list(model_kwargs.pop("logits_processor", []))
|
| 731 |
+
if reasoning and reasoning_budget is not None and reasoning_budget > 0:
|
| 732 |
+
prompt_length = int(model_kwargs["input_ids"].shape[-1])
|
| 733 |
+
logits_processors.append(
|
| 734 |
+
ReasoningBudgetLogitsProcessor(
|
| 735 |
+
runtime.tokenizer,
|
| 736 |
+
prompt_length=prompt_length,
|
| 737 |
+
reasoning_budget=int(reasoning_budget),
|
| 738 |
+
)
|
| 739 |
+
)
|
| 740 |
+
streamer = TokenIdStreamer()
|
| 741 |
+
cancel_event = Event()
|
| 742 |
+
generation_error: list[BaseException] = []
|
| 743 |
+
generated_tokens = 0
|
| 744 |
+
buffered_token_ids: list[int] = []
|
| 745 |
+
response = ""
|
| 746 |
+
last_fields: tuple[str, str] | None = None
|
| 747 |
+
generation_finished = False
|
| 748 |
+
|
| 749 |
+
def generate() -> None:
|
| 750 |
+
try:
|
| 751 |
+
stopping_criteria = list(model_kwargs.pop("stopping_criteria", []))
|
| 752 |
+
cache_kwargs = (
|
| 753 |
+
{
|
| 754 |
+
"cache_implementation": runtime.cache_implementation,
|
| 755 |
+
"disable_compile": True,
|
| 756 |
+
}
|
| 757 |
+
if runtime.cache_implementation
|
| 758 |
+
else {}
|
| 759 |
+
)
|
| 760 |
+
with torch.inference_mode():
|
| 761 |
+
runtime.model.generate(
|
| 762 |
+
**model_kwargs,
|
| 763 |
+
**cache_kwargs,
|
| 764 |
+
logits_processor=logits_processors,
|
| 765 |
+
stopping_criteria=[
|
| 766 |
+
*stopping_criteria,
|
| 767 |
+
EventStoppingCriteria(cancel_event),
|
| 768 |
+
],
|
| 769 |
+
streamer=streamer,
|
| 770 |
+
use_cache=True,
|
| 771 |
+
)
|
| 772 |
+
except BaseException as error:
|
| 773 |
+
generation_error.append(error)
|
| 774 |
+
streamer.fail(error)
|
| 775 |
+
|
| 776 |
+
def flush() -> tuple[str, str]:
|
| 777 |
+
nonlocal response
|
| 778 |
+
response += runtime.tokenizer.decode(
|
| 779 |
+
buffered_token_ids,
|
| 780 |
+
skip_special_tokens=False,
|
| 781 |
+
clean_up_tokenization_spaces=False,
|
| 782 |
+
)
|
| 783 |
+
buffered_token_ids.clear()
|
| 784 |
+
return _stream_fields(response, reasoning)
|
| 785 |
+
|
| 786 |
+
thread = Thread(target=generate, daemon=True)
|
| 787 |
+
thread.start()
|
| 788 |
+
try:
|
| 789 |
+
for token_id in streamer:
|
| 790 |
+
generated_tokens += 1
|
| 791 |
+
buffered_token_ids.append(token_id)
|
| 792 |
+
if token_id in eos_token_ids:
|
| 793 |
+
generation_finished = True
|
| 794 |
+
if len(buffered_token_ids) < STREAM_CHUNK_SIZE and not generation_finished:
|
| 795 |
+
continue
|
| 796 |
+
|
| 797 |
+
fields = flush()
|
| 798 |
+
if fields != last_fields:
|
| 799 |
+
yield fields
|
| 800 |
+
last_fields = fields
|
| 801 |
+
if generation_finished:
|
| 802 |
+
break
|
| 803 |
+
|
| 804 |
+
if generation_error:
|
| 805 |
+
raise generation_error[0]
|
| 806 |
+
if buffered_token_ids:
|
| 807 |
+
fields = flush()
|
| 808 |
+
if fields != last_fields:
|
| 809 |
+
yield fields
|
| 810 |
+
last_fields = fields
|
| 811 |
+
|
| 812 |
+
hit_max_tokens = (
|
| 813 |
+
not generation_finished
|
| 814 |
+
and generated_tokens >= int(model_kwargs["max_new_tokens"])
|
| 815 |
+
)
|
| 816 |
+
final_fields = _stream_fields(response, reasoning)
|
| 817 |
+
if reasoning and generation_finished and "</think>" not in response:
|
| 818 |
+
final_fields = _stream_fields(response, reasoning=False)
|
| 819 |
+
if hit_max_tokens:
|
| 820 |
+
answer, thinking = final_fields
|
| 821 |
+
answer = f"{answer}\n\n{MAX_TOKEN_WARNING}".strip()
|
| 822 |
+
final_fields = answer, thinking
|
| 823 |
+
if final_fields != last_fields:
|
| 824 |
+
yield final_fields
|
| 825 |
+
print(
|
| 826 |
+
f"[gpu] generated_tokens={generated_tokens} "
|
| 827 |
+
f"reasoning_closed={'</think>' in response} "
|
| 828 |
+
f"hit_max_tokens={hit_max_tokens}",
|
| 829 |
+
flush=True,
|
| 830 |
+
)
|
| 831 |
+
finally:
|
| 832 |
+
cancel_event.set()
|
| 833 |
+
thread.join(timeout=10)
|
| 834 |
+
|
| 835 |
+
|
| 836 |
def _generate(
|
| 837 |
+
runtime: AudexRuntime,
|
| 838 |
audio: str,
|
| 839 |
prompt: str,
|
| 840 |
reasoning: bool,
|
|
|
|
| 842 |
temperature: float,
|
| 843 |
top_p: float,
|
| 844 |
greedy: bool,
|
| 845 |
+
reasoning_budget: int | None = None,
|
| 846 |
+
) -> Iterator[tuple[str, str]]:
|
| 847 |
started_at = time.perf_counter()
|
| 848 |
+
if runtime is model_runtimes[DEFAULT_MODEL_NAME]:
|
| 849 |
+
print(f"[gpu] is_fast_path_available={_find_fast_path_flag()}", flush=True)
|
| 850 |
|
| 851 |
wav, sr = load_audio(audio, target_sr=SAMPLE_RATE)
|
| 852 |
audio_duration = wav.shape[-1] / sr
|
|
|
|
| 857 |
)
|
| 858 |
if max_new_tokens > MAX_NEW_TOKENS:
|
| 859 |
raise gr.Error(f"This demo supports up to {MAX_NEW_TOKENS} output tokens.")
|
| 860 |
+
if (
|
| 861 |
+
reasoning
|
| 862 |
+
and reasoning_budget is not None
|
| 863 |
+
and reasoning_budget > 0
|
| 864 |
+
and (
|
| 865 |
+
reasoning_budget >= max_new_tokens
|
| 866 |
+
or reasoning_budget > _max_reasoning_budget(max_new_tokens)
|
| 867 |
+
)
|
| 868 |
+
):
|
| 869 |
+
raise gr.Error(
|
| 870 |
+
"Reasoning budget is too high for max new tokens after reserving "
|
| 871 |
+
"the newline grace window and final answer."
|
| 872 |
+
)
|
| 873 |
|
| 874 |
input_features = extract_whisper_features(
|
| 875 |
+
runtime.feature_extractor,
|
| 876 |
+
wav,
|
| 877 |
+
sample_rate=sr,
|
| 878 |
+
clip_duration=float(getattr(runtime.config, "sound_clip_duration", 30.0)),
|
| 879 |
+
)
|
| 880 |
+
num_embeddings = input_features.shape[0] * int(
|
| 881 |
+
getattr(runtime.config, "sound_embedding_size", 750)
|
| 882 |
)
|
|
|
|
| 883 |
formatted = build_prompt_template(prompt.strip(), reasoning=bool(reasoning),
|
| 884 |
prompt_repitition="none")
|
| 885 |
expanded = expand_sound_placeholder(formatted, num_embeddings)
|
| 886 |
+
tok = runtime.tokenizer(expanded, return_tensors="pt", add_special_tokens=False)
|
| 887 |
input_ids = tok.input_ids.to("cuda")
|
| 888 |
attention_mask = (tok.attention_mask if "attention_mask" in tok
|
| 889 |
else build_attention_mask(input_ids)).to("cuda")
|
| 890 |
input_features = input_features.to("cuda")
|
| 891 |
|
| 892 |
+
eos_token_id = runtime.tokenizer.convert_tokens_to_ids(IM_END_TOKEN)
|
| 893 |
+
if eos_token_id is None or eos_token_id == runtime.tokenizer.unk_token_id:
|
| 894 |
+
eos_token_id = getattr(runtime.config, "eos_token_id", None)
|
| 895 |
|
| 896 |
temperature = max(float(temperature), 1e-4)
|
| 897 |
top_p = float(top_p)
|
|
|
|
| 899 |
gen_kwargs = dict(
|
| 900 |
do_sample=do_sample,
|
| 901 |
eos_token_id=eos_token_id,
|
| 902 |
+
pad_token_id=runtime.tokenizer.pad_token_id
|
| 903 |
+
or getattr(runtime.config, "pad_token_id", 0),
|
| 904 |
max_new_tokens=int(max_new_tokens),
|
| 905 |
+
suppress_tokens=_blocked_output_token_ids(runtime),
|
| 906 |
)
|
| 907 |
+
# The official top-k value is 0, which means no top-k filter in Transformers.
|
| 908 |
if do_sample:
|
| 909 |
gen_kwargs["temperature"] = temperature
|
| 910 |
if top_p > 0.0:
|
| 911 |
gen_kwargs["top_p"] = top_p
|
| 912 |
|
| 913 |
+
torch.manual_seed(0)
|
| 914 |
+
torch.cuda.manual_seed_all(0)
|
| 915 |
+
generation_kwargs = {
|
| 916 |
+
**gen_kwargs,
|
| 917 |
+
"input_ids": input_ids,
|
| 918 |
+
"attention_mask": attention_mask,
|
| 919 |
+
"input_features": input_features,
|
| 920 |
+
}
|
| 921 |
+
yield from _stream_model_generate(
|
| 922 |
+
runtime,
|
| 923 |
+
generation_kwargs,
|
| 924 |
+
reasoning,
|
| 925 |
+
reasoning_budget,
|
| 926 |
+
)
|
| 927 |
print(
|
| 928 |
f"[gpu] audio_seconds={audio_duration:.1f} greedy={greedy} "
|
| 929 |
f"elapsed_seconds={time.perf_counter() - started_at:.1f}",
|
| 930 |
flush=True,
|
| 931 |
)
|
|
|
|
| 932 |
|
| 933 |
|
| 934 |
@spaces.GPU(duration=_estimate, size="xlarge")
|
|
|
|
| 945 |
|
| 946 |
Predefined ASR and translation tasks use the model's recommended greedy
|
| 947 |
decoding. Other tasks and custom instructions use the sampling controls.
|
| 948 |
+
Input duration and output length are limited by the current runtime.
|
| 949 |
|
| 950 |
Returns:
|
| 951 |
The final answer and, when enabled, the reasoning trace.
|
|
|
|
| 954 |
return "Please provide an audio input.", ""
|
| 955 |
|
| 956 |
custom_instruction = custom_prompt.strip() if custom_prompt else ""
|
| 957 |
+
prompt = custom_instruction or LEGACY_TASK_PROMPTS.get(
|
| 958 |
+
task,
|
| 959 |
+
LEGACY_TASK_PROMPTS["Describe the audio"],
|
| 960 |
+
)
|
| 961 |
+
greedy = not custom_instruction and task in LEGACY_GREEDY_TASKS
|
| 962 |
+
answer, thinking = "", ""
|
| 963 |
+
for answer, thinking in _generate(
|
| 964 |
+
model_runtimes[DEFAULT_MODEL_NAME],
|
| 965 |
audio,
|
| 966 |
prompt,
|
| 967 |
reasoning,
|
|
|
|
| 969 |
temperature=float(temperature),
|
| 970 |
top_p=float(top_p),
|
| 971 |
greedy=greedy,
|
| 972 |
+
):
|
| 973 |
+
pass
|
| 974 |
+
return answer, thinking
|
| 975 |
+
|
| 976 |
+
|
| 977 |
+
def _estimate_text(
|
| 978 |
+
prompt: str,
|
| 979 |
+
reasoning: bool,
|
| 980 |
+
max_new_tokens: int,
|
| 981 |
+
temperature: float,
|
| 982 |
+
top_p: float,
|
| 983 |
+
*args: object,
|
| 984 |
+
**kwargs: object,
|
| 985 |
+
) -> int:
|
| 986 |
+
if not prompt.strip() or int(max_new_tokens) > MAX_NEW_TOKENS:
|
| 987 |
+
return 10
|
| 988 |
+
return MAX_GPU_DURATION_SECONDS
|
| 989 |
+
|
| 990 |
+
|
| 991 |
+
def _generate_text(
|
| 992 |
+
runtime: AudexRuntime,
|
| 993 |
+
prompt: str,
|
| 994 |
+
reasoning: bool,
|
| 995 |
+
max_new_tokens: int,
|
| 996 |
+
temperature: float,
|
| 997 |
+
top_p: float,
|
| 998 |
+
reasoning_budget: int | None = None,
|
| 999 |
+
seed: int = TEXT_SEED,
|
| 1000 |
+
) -> Iterator[tuple[str, str]]:
|
| 1001 |
+
"""Run text-only inference with the official Audex chat template."""
|
| 1002 |
+
if not prompt.strip():
|
| 1003 |
+
yield "Please provide a text prompt.", ""
|
| 1004 |
+
return
|
| 1005 |
+
if max_new_tokens > MAX_NEW_TOKENS:
|
| 1006 |
+
raise gr.Error(f"This demo supports up to {MAX_NEW_TOKENS} output tokens.")
|
| 1007 |
+
if (
|
| 1008 |
+
reasoning
|
| 1009 |
+
and reasoning_budget is not None
|
| 1010 |
+
and reasoning_budget > 0
|
| 1011 |
+
and (
|
| 1012 |
+
reasoning_budget >= max_new_tokens
|
| 1013 |
+
or reasoning_budget > _max_reasoning_budget(max_new_tokens)
|
| 1014 |
+
)
|
| 1015 |
+
):
|
| 1016 |
+
raise gr.Error(
|
| 1017 |
+
"Reasoning budget is too high for max new tokens after reserving "
|
| 1018 |
+
"the newline grace window and final answer."
|
| 1019 |
+
)
|
| 1020 |
+
|
| 1021 |
+
formatted = runtime.tokenizer.apply_chat_template(
|
| 1022 |
+
[
|
| 1023 |
+
{"role": "system", "content": TEXT_SYSTEM_PROMPT},
|
| 1024 |
+
{"role": "user", "content": prompt.strip()},
|
| 1025 |
+
],
|
| 1026 |
+
tokenize=False,
|
| 1027 |
+
add_generation_prompt=True,
|
| 1028 |
+
enable_thinking=reasoning,
|
| 1029 |
+
)
|
| 1030 |
+
tok = runtime.tokenizer(formatted, return_tensors="pt", add_special_tokens=False)
|
| 1031 |
+
input_ids = tok.input_ids.to("cuda")
|
| 1032 |
+
attention_mask = (
|
| 1033 |
+
tok.attention_mask if "attention_mask" in tok else build_attention_mask(input_ids)
|
| 1034 |
+
).to("cuda")
|
| 1035 |
+
|
| 1036 |
+
temperature = max(float(temperature), 1e-4)
|
| 1037 |
+
top_p = float(top_p)
|
| 1038 |
+
do_sample = (temperature != 1.0) or (0.0 < top_p < 1.0)
|
| 1039 |
+
gen_kwargs = {
|
| 1040 |
+
"do_sample": do_sample,
|
| 1041 |
+
"eos_token_id": runtime.tokenizer.convert_tokens_to_ids(IM_END_TOKEN),
|
| 1042 |
+
"pad_token_id": runtime.tokenizer.pad_token_id
|
| 1043 |
+
or getattr(runtime.config, "pad_token_id", 0),
|
| 1044 |
+
"max_new_tokens": int(max_new_tokens),
|
| 1045 |
+
"suppress_tokens": _blocked_output_token_ids(runtime),
|
| 1046 |
+
}
|
| 1047 |
+
if do_sample:
|
| 1048 |
+
gen_kwargs["temperature"] = temperature
|
| 1049 |
+
if top_p > 0.0:
|
| 1050 |
+
gen_kwargs["top_p"] = top_p
|
| 1051 |
+
|
| 1052 |
+
started_at = time.perf_counter()
|
| 1053 |
+
torch.manual_seed(seed)
|
| 1054 |
+
torch.cuda.manual_seed_all(seed)
|
| 1055 |
+
generation_kwargs = {
|
| 1056 |
+
**gen_kwargs,
|
| 1057 |
+
"input_ids": input_ids,
|
| 1058 |
+
"attention_mask": attention_mask,
|
| 1059 |
+
}
|
| 1060 |
+
yield from _stream_model_generate(
|
| 1061 |
+
runtime,
|
| 1062 |
+
generation_kwargs,
|
| 1063 |
+
reasoning,
|
| 1064 |
+
reasoning_budget,
|
| 1065 |
)
|
| 1066 |
+
print(
|
| 1067 |
+
f"[gpu] text_input_tokens={input_ids.shape[-1]} "
|
| 1068 |
+
f"elapsed_seconds={time.perf_counter() - started_at:.1f}",
|
| 1069 |
+
flush=True,
|
| 1070 |
+
)
|
| 1071 |
+
|
| 1072 |
+
|
| 1073 |
+
def _generate_tts(
|
| 1074 |
+
runtime: AudexRuntime,
|
| 1075 |
+
text: str,
|
| 1076 |
+
max_new_tokens: int,
|
| 1077 |
+
temperature: float,
|
| 1078 |
+
top_p: float,
|
| 1079 |
+
*,
|
| 1080 |
+
top_k: int = 0,
|
| 1081 |
+
guidance_scale: float = 2.0,
|
| 1082 |
+
token_limit: int | None = None,
|
| 1083 |
+
segment_sentences: bool = False,
|
| 1084 |
+
) -> Iterator[tuple[str, str | None, dict[str, object]]]:
|
| 1085 |
+
text = text.strip()
|
| 1086 |
+
if not text:
|
| 1087 |
+
yield "Please provide text to synthesize.", None, player_value(0, reset=True)
|
| 1088 |
+
return
|
| 1089 |
+
max_tokens = TTS_MAX_NEW_TOKENS if token_limit is None else token_limit
|
| 1090 |
+
if max_new_tokens > max_tokens:
|
| 1091 |
+
raise gr.Error(f"TTS supports up to {max_tokens} speech tokens.")
|
| 1092 |
+
|
| 1093 |
+
segments = _split_sentence_segments(text) if segment_sentences else [text]
|
| 1094 |
+
if not segments:
|
| 1095 |
+
raise gr.Error("Text response had no final answer for TTS.")
|
| 1096 |
+
|
| 1097 |
+
sequence = 0
|
| 1098 |
+
chunks: list[np.ndarray] = []
|
| 1099 |
+
first_token_seconds: float | None = None
|
| 1100 |
+
total_tokens = 0
|
| 1101 |
+
request_tag = hashlib.sha256(text.encode()).hexdigest()[:8]
|
| 1102 |
+
started_at = time.perf_counter()
|
| 1103 |
+
torch.cuda.reset_peak_memory_stats()
|
| 1104 |
+
print(f"[gpu] tts_request={request_tag} started", flush=True)
|
| 1105 |
+
initial_player = (
|
| 1106 |
+
player_value(sequence, reset=True) if TTS_STREAMING_PLAYER_ENABLED else gr.skip()
|
| 1107 |
+
)
|
| 1108 |
+
yield "Generating speech tokens…", None, initial_player
|
| 1109 |
+
|
| 1110 |
+
try:
|
| 1111 |
+
for segment_index, segment in enumerate(segments, start=1):
|
| 1112 |
+
segment_tokens = 0
|
| 1113 |
+
for event in stream_tts(
|
| 1114 |
+
model=runtime.model,
|
| 1115 |
+
tokenizer=runtime.tokenizer,
|
| 1116 |
+
decoder=runtime.speech_decoder,
|
| 1117 |
+
text=segment,
|
| 1118 |
+
max_new_tokens=int(max_new_tokens),
|
| 1119 |
+
temperature=float(temperature),
|
| 1120 |
+
top_p=float(top_p),
|
| 1121 |
+
top_k=top_k,
|
| 1122 |
+
guidance_scale=guidance_scale,
|
| 1123 |
+
):
|
| 1124 |
+
segment_tokens = event.token_count
|
| 1125 |
+
if event.token_count and first_token_seconds is None:
|
| 1126 |
+
first_token_seconds = time.perf_counter() - started_at
|
| 1127 |
+
if event.pcm is not None:
|
| 1128 |
+
chunks.append(event.pcm)
|
| 1129 |
+
sequence += 1
|
| 1130 |
+
player_update = (
|
| 1131 |
+
player_value(
|
| 1132 |
+
sequence,
|
| 1133 |
+
pcm=encode_pcm_chunk(event.pcm),
|
| 1134 |
+
token_count=total_tokens + event.token_count,
|
| 1135 |
+
)
|
| 1136 |
+
if TTS_STREAMING_PLAYER_ENABLED
|
| 1137 |
+
else gr.skip()
|
| 1138 |
+
)
|
| 1139 |
+
yield (
|
| 1140 |
+
f"Generating speech segment {segment_index}/{len(segments)} "
|
| 1141 |
+
f"· {total_tokens + event.token_count} tokens",
|
| 1142 |
+
None,
|
| 1143 |
+
player_update,
|
| 1144 |
+
)
|
| 1145 |
+
|
| 1146 |
+
total_tokens += segment_tokens
|
| 1147 |
+
if segment_index < len(segments):
|
| 1148 |
+
chunks.append(
|
| 1149 |
+
np.zeros(
|
| 1150 |
+
round(SAMPLE_RATE * S2S_TTS_SEGMENT_SILENCE_SECONDS),
|
| 1151 |
+
dtype=np.float32,
|
| 1152 |
+
)
|
| 1153 |
+
)
|
| 1154 |
+
|
| 1155 |
+
if not chunks:
|
| 1156 |
+
raise RuntimeError("TTS completed without producing audio")
|
| 1157 |
+
waveform = np.concatenate(chunks)
|
| 1158 |
+
wav_path = write_wav(waveform)
|
| 1159 |
+
sequence += 1
|
| 1160 |
+
elapsed_seconds = time.perf_counter() - started_at
|
| 1161 |
+
peak_gib = torch.cuda.max_memory_allocated() / (1024**3)
|
| 1162 |
+
token_rate = total_tokens / max(elapsed_seconds, 1e-6)
|
| 1163 |
+
print(
|
| 1164 |
+
f"[gpu] tts_request={request_tag} tts_tokens={total_tokens} "
|
| 1165 |
+
f"ttfc_seconds={first_token_seconds:.2f} "
|
| 1166 |
+
f"tokens_per_second={token_rate:.2f} audio_seconds={waveform.size / SAMPLE_RATE:.2f} "
|
| 1167 |
+
f"elapsed_seconds={elapsed_seconds:.2f} peak_memory_gib={peak_gib:.2f}",
|
| 1168 |
+
flush=True,
|
| 1169 |
+
)
|
| 1170 |
+
yield (
|
| 1171 |
+
f"Complete · {waveform.size / SAMPLE_RATE:.1f}s audio",
|
| 1172 |
+
wav_path,
|
| 1173 |
+
(
|
| 1174 |
+
player_value(sequence, token_count=total_tokens, done=True)
|
| 1175 |
+
if TTS_STREAMING_PLAYER_ENABLED
|
| 1176 |
+
else gr.skip()
|
| 1177 |
+
),
|
| 1178 |
+
)
|
| 1179 |
+
except GeneratorExit:
|
| 1180 |
+
print(f"[gpu] tts_request={request_tag} cancelled", flush=True)
|
| 1181 |
+
raise
|
| 1182 |
+
|
| 1183 |
+
|
| 1184 |
+
def _split_sentence_segments(text: str) -> list[str]:
|
| 1185 |
+
return [
|
| 1186 |
+
part.strip()
|
| 1187 |
+
for line in text.splitlines()
|
| 1188 |
+
for part in re.findall(r"[^.!?]+[.!?]+[\"')\]]*|[^.!?]+$", line.strip())
|
| 1189 |
+
if part.strip()
|
| 1190 |
+
]
|
| 1191 |
+
|
| 1192 |
+
|
| 1193 |
+
def _clean_transcription(text: str) -> str:
|
| 1194 |
+
first_quote = text.find("'")
|
| 1195 |
+
last_quote = text.rfind("'")
|
| 1196 |
+
if first_quote != -1 and last_quote > first_quote:
|
| 1197 |
+
return text[first_quote + 1 : last_quote].strip()
|
| 1198 |
+
return text.strip()
|
| 1199 |
+
|
| 1200 |
+
|
| 1201 |
+
def _compose_s2s_input(response_prompt: str, transcript: str) -> str:
|
| 1202 |
+
return (
|
| 1203 |
+
f"{response_prompt.strip()}\n\n"
|
| 1204 |
+
f"<spoken_transcript>\n{transcript.strip()}\n</spoken_transcript>"
|
| 1205 |
+
)
|
| 1206 |
+
|
| 1207 |
+
|
| 1208 |
+
def _limit_spoken_answer(text: str) -> str:
|
| 1209 |
+
segments = _split_sentence_segments(text)
|
| 1210 |
+
sentence = segments[-1] if segments else text.strip()
|
| 1211 |
+
words = sentence.split()
|
| 1212 |
+
if len(words) <= S2S_SPOKEN_MAX_WORDS:
|
| 1213 |
+
return sentence
|
| 1214 |
+
return " ".join(words[:S2S_SPOKEN_MAX_WORDS]).rstrip(",;:") + "."
|
| 1215 |
+
|
| 1216 |
+
|
| 1217 |
+
def _generate_s2s(
|
| 1218 |
+
runtime: AudexRuntime,
|
| 1219 |
+
audio: str,
|
| 1220 |
+
response_prompt: str,
|
| 1221 |
+
reasoning: bool,
|
| 1222 |
+
reasoning_budget: int,
|
| 1223 |
+
max_new_tokens: int,
|
| 1224 |
+
temperature: float,
|
| 1225 |
+
top_p: float,
|
| 1226 |
+
guidance_scale: float,
|
| 1227 |
+
) -> Iterator[tuple[object, object, object, object]]:
|
| 1228 |
+
yield "Transcribing input speech…", "", gr.skip(), gr.skip()
|
| 1229 |
+
transcript = ""
|
| 1230 |
+
for transcription, _ in _generate(
|
| 1231 |
+
runtime,
|
| 1232 |
+
audio,
|
| 1233 |
+
"Transcribe the input speech.",
|
| 1234 |
+
False,
|
| 1235 |
+
min(256, MAX_NEW_TOKENS),
|
| 1236 |
+
temperature=1.0,
|
| 1237 |
+
top_p=1.0,
|
| 1238 |
+
greedy=True,
|
| 1239 |
+
):
|
| 1240 |
+
transcript = _clean_transcription(transcription)
|
| 1241 |
+
yield f"Transcript: {transcript}", "", gr.skip(), gr.skip()
|
| 1242 |
+
if not transcript:
|
| 1243 |
+
raise gr.Error("Speech-to-speech transcription produced no text.")
|
| 1244 |
+
|
| 1245 |
+
text_input = _compose_s2s_input(response_prompt, transcript)
|
| 1246 |
+
candidate_answer = ""
|
| 1247 |
+
thinking = ""
|
| 1248 |
+
for current_answer, thinking in _generate_text(
|
| 1249 |
+
runtime,
|
| 1250 |
+
text_input,
|
| 1251 |
+
reasoning,
|
| 1252 |
+
int(max_new_tokens),
|
| 1253 |
+
temperature,
|
| 1254 |
+
top_p,
|
| 1255 |
+
reasoning_budget=reasoning_budget,
|
| 1256 |
+
):
|
| 1257 |
+
candidate_answer = current_answer or candidate_answer
|
| 1258 |
+
yield "", thinking, gr.skip(), gr.skip()
|
| 1259 |
+
if MAX_TOKEN_WARNING in candidate_answer:
|
| 1260 |
+
yield candidate_answer, thinking, gr.skip(), gr.skip()
|
| 1261 |
+
return
|
| 1262 |
+
if not candidate_answer and not thinking:
|
| 1263 |
+
raise gr.Error("Speech-to-speech response generation produced no text.")
|
| 1264 |
+
|
| 1265 |
+
answer = _limit_spoken_answer(candidate_answer)
|
| 1266 |
+
if not answer:
|
| 1267 |
+
raise gr.Error("Speech-to-speech response generation produced no final answer.")
|
| 1268 |
+
yield answer, thinking, gr.skip(), gr.skip()
|
| 1269 |
+
|
| 1270 |
+
for _, wav_path, player in _generate_tts(
|
| 1271 |
+
runtime,
|
| 1272 |
+
answer,
|
| 1273 |
+
S2S_TTS_MAX_NEW_TOKENS,
|
| 1274 |
+
0.1,
|
| 1275 |
+
1.0,
|
| 1276 |
+
top_k=80,
|
| 1277 |
+
guidance_scale=guidance_scale,
|
| 1278 |
+
token_limit=S2S_TTS_MAX_NEW_TOKENS,
|
| 1279 |
+
segment_sentences=True,
|
| 1280 |
+
):
|
| 1281 |
+
yield answer, thinking, wav_path or gr.skip(), player
|
| 1282 |
+
|
| 1283 |
+
|
| 1284 |
+
@spaces.GPU(duration=_estimate_text, size="xlarge")
|
| 1285 |
+
def run_text(
|
| 1286 |
+
prompt: str,
|
| 1287 |
+
reasoning: bool,
|
| 1288 |
+
max_new_tokens: int,
|
| 1289 |
+
temperature: float,
|
| 1290 |
+
top_p: float,
|
| 1291 |
+
) -> tuple[str, str]:
|
| 1292 |
+
"""Run the legacy text-only API endpoint."""
|
| 1293 |
+
answer, thinking = "", ""
|
| 1294 |
+
for answer, thinking in _generate_text(
|
| 1295 |
+
model_runtimes[DEFAULT_MODEL_NAME],
|
| 1296 |
+
prompt,
|
| 1297 |
+
reasoning,
|
| 1298 |
+
max_new_tokens,
|
| 1299 |
+
temperature,
|
| 1300 |
+
top_p,
|
| 1301 |
+
):
|
| 1302 |
+
pass
|
| 1303 |
return answer, thinking
|
| 1304 |
|
| 1305 |
|
| 1306 |
+
def _estimate_unified(
|
| 1307 |
+
model_name: str,
|
| 1308 |
+
task: str,
|
| 1309 |
+
audio: str | None,
|
| 1310 |
+
prompt: str,
|
| 1311 |
+
reasoning: bool,
|
| 1312 |
+
reasoning_budget: int,
|
| 1313 |
+
max_new_tokens: int,
|
| 1314 |
+
temperature: float,
|
| 1315 |
+
top_p: float,
|
| 1316 |
+
guidance_scale: float,
|
| 1317 |
+
*args: object,
|
| 1318 |
+
**kwargs: object,
|
| 1319 |
+
) -> int:
|
| 1320 |
+
if task == S2S_TASK:
|
| 1321 |
+
return S2S_GPU_DURATION_SECONDS
|
| 1322 |
+
if task in {TEXT_TASK, TTS_TASK}:
|
| 1323 |
+
if task == TTS_TASK and int(max_new_tokens) > TTS_MAX_NEW_TOKENS:
|
| 1324 |
+
return 10
|
| 1325 |
+
return _estimate_text(
|
| 1326 |
+
prompt,
|
| 1327 |
+
reasoning,
|
| 1328 |
+
max_new_tokens,
|
| 1329 |
+
temperature,
|
| 1330 |
+
top_p,
|
| 1331 |
+
)
|
| 1332 |
+
return _estimate(
|
| 1333 |
+
audio,
|
| 1334 |
+
task,
|
| 1335 |
+
prompt,
|
| 1336 |
+
reasoning,
|
| 1337 |
+
max_new_tokens,
|
| 1338 |
+
temperature,
|
| 1339 |
+
top_p,
|
| 1340 |
+
)
|
| 1341 |
+
|
| 1342 |
+
|
| 1343 |
+
@spaces.GPU(duration=_estimate_unified, size="xlarge")
|
| 1344 |
+
def run_unified(
|
| 1345 |
+
model_name: str,
|
| 1346 |
+
task: str,
|
| 1347 |
+
audio: str | None,
|
| 1348 |
+
prompt: str,
|
| 1349 |
+
reasoning: bool,
|
| 1350 |
+
reasoning_budget: int,
|
| 1351 |
+
max_new_tokens: int,
|
| 1352 |
+
temperature: float,
|
| 1353 |
+
top_p: float,
|
| 1354 |
+
guidance_scale: float,
|
| 1355 |
+
) -> Iterator[tuple[object, object, object, object]]:
|
| 1356 |
+
"""Run a selected Audex model for audio, text, or speech generation."""
|
| 1357 |
+
yield "", "", gr.skip(), gr.skip()
|
| 1358 |
+
runtime = _get_runtime(model_name)
|
| 1359 |
+
settings = TASK_SPECS[task]
|
| 1360 |
+
if settings["modality"] == "s2s":
|
| 1361 |
+
if audio is None:
|
| 1362 |
+
yield "Please provide an audio input.", "", gr.skip(), gr.skip()
|
| 1363 |
+
return
|
| 1364 |
+
yield from _generate_s2s(
|
| 1365 |
+
runtime,
|
| 1366 |
+
audio,
|
| 1367 |
+
prompt,
|
| 1368 |
+
reasoning,
|
| 1369 |
+
int(reasoning_budget),
|
| 1370 |
+
int(max_new_tokens),
|
| 1371 |
+
temperature,
|
| 1372 |
+
top_p,
|
| 1373 |
+
float(guidance_scale),
|
| 1374 |
+
)
|
| 1375 |
+
return
|
| 1376 |
+
if settings["modality"] == "tts":
|
| 1377 |
+
for status, wav_path, player in _generate_tts(
|
| 1378 |
+
runtime,
|
| 1379 |
+
prompt,
|
| 1380 |
+
int(max_new_tokens),
|
| 1381 |
+
temperature,
|
| 1382 |
+
top_p,
|
| 1383 |
+
guidance_scale=float(guidance_scale),
|
| 1384 |
+
):
|
| 1385 |
+
yield status, "", wav_path or gr.skip(), player
|
| 1386 |
+
return
|
| 1387 |
+
if settings["modality"] == "text":
|
| 1388 |
+
for answer, thinking in _generate_text(
|
| 1389 |
+
runtime,
|
| 1390 |
+
prompt,
|
| 1391 |
+
reasoning,
|
| 1392 |
+
max_new_tokens,
|
| 1393 |
+
temperature,
|
| 1394 |
+
top_p,
|
| 1395 |
+
reasoning_budget=int(reasoning_budget),
|
| 1396 |
+
):
|
| 1397 |
+
yield answer, thinking, gr.skip(), gr.skip()
|
| 1398 |
+
return
|
| 1399 |
+
if audio is None:
|
| 1400 |
+
yield "Please provide an audio input.", "", gr.skip(), gr.skip()
|
| 1401 |
+
return
|
| 1402 |
+
|
| 1403 |
+
instruction = (prompt.strip() or str(settings["template"])).replace("<sound>", "").strip()
|
| 1404 |
+
for answer, thinking in _generate(
|
| 1405 |
+
runtime,
|
| 1406 |
+
audio,
|
| 1407 |
+
instruction,
|
| 1408 |
+
reasoning,
|
| 1409 |
+
int(max_new_tokens),
|
| 1410 |
+
temperature=float(temperature),
|
| 1411 |
+
top_p=float(top_p),
|
| 1412 |
+
greedy=bool(settings["greedy"]),
|
| 1413 |
+
reasoning_budget=int(reasoning_budget),
|
| 1414 |
+
):
|
| 1415 |
+
yield answer, thinking, gr.skip(), gr.skip()
|
| 1416 |
+
|
| 1417 |
+
|
| 1418 |
+
def unified_task_defaults(
|
| 1419 |
+
task: str,
|
| 1420 |
+
) -> tuple[dict[str, object], ...]:
|
| 1421 |
+
settings = TASK_SPECS[task]
|
| 1422 |
+
is_audio = settings["modality"] in {"audio", "s2s"}
|
| 1423 |
+
is_tts = settings["modality"] == "tts"
|
| 1424 |
+
has_speech_output = settings["modality"] in {"tts", "s2s"}
|
| 1425 |
+
reasoning = bool(settings["reasoning"])
|
| 1426 |
+
default_max_new_tokens = int(settings.get("max_new_tokens", DEFAULT_MAX_NEW_TOKENS))
|
| 1427 |
+
max_new_tokens_limit = int(settings.get("max_new_tokens_limit", MAX_NEW_TOKENS))
|
| 1428 |
+
reasoning_budget_limit = _max_reasoning_budget(default_max_new_tokens)
|
| 1429 |
+
return (
|
| 1430 |
+
gr.update(visible=is_audio),
|
| 1431 |
+
gr.update(
|
| 1432 |
+
value=str(settings["template"]),
|
| 1433 |
+
lines=8 if settings["modality"] == "s2s" else (3 if is_audio else 8),
|
| 1434 |
+
label=(
|
| 1435 |
+
"Text to synthesize"
|
| 1436 |
+
if is_tts
|
| 1437 |
+
else (
|
| 1438 |
+
"Response instruction / prompt"
|
| 1439 |
+
if settings["modality"] == "s2s"
|
| 1440 |
+
else "Task template / prompt"
|
| 1441 |
+
)
|
| 1442 |
+
),
|
| 1443 |
+
),
|
| 1444 |
+
gr.update(value=reasoning, visible=not is_tts),
|
| 1445 |
+
gr.update(
|
| 1446 |
+
value=min(
|
| 1447 |
+
int(settings.get("reasoning_budget", DEFAULT_REASONING_BUDGET)),
|
| 1448 |
+
reasoning_budget_limit,
|
| 1449 |
+
),
|
| 1450 |
+
maximum=reasoning_budget_limit,
|
| 1451 |
+
visible=not is_tts,
|
| 1452 |
+
interactive=reasoning,
|
| 1453 |
+
),
|
| 1454 |
+
float(settings["temperature"]),
|
| 1455 |
+
float(settings["top_p"]),
|
| 1456 |
+
gr.update(
|
| 1457 |
+
value=TTS_MAX_NEW_TOKENS if is_tts else default_max_new_tokens,
|
| 1458 |
+
maximum=TTS_MAX_NEW_TOKENS if is_tts else max_new_tokens_limit,
|
| 1459 |
+
),
|
| 1460 |
+
gr.update(
|
| 1461 |
+
value=float(settings.get("guidance_scale", 2.0)),
|
| 1462 |
+
visible=has_speech_output,
|
| 1463 |
+
),
|
| 1464 |
+
gr.update(label="Status" if is_tts else "Answer"),
|
| 1465 |
+
gr.update(visible=not is_tts),
|
| 1466 |
+
gr.update(value=None, visible=has_speech_output),
|
| 1467 |
+
gr.update(
|
| 1468 |
+
value=player_value(0, reset=True),
|
| 1469 |
+
visible=is_tts and TTS_STREAMING_PLAYER_ENABLED,
|
| 1470 |
+
),
|
| 1471 |
+
)
|
| 1472 |
+
|
| 1473 |
+
|
| 1474 |
+
def update_reasoning_budget_limit(
|
| 1475 |
+
max_new_tokens: int,
|
| 1476 |
+
reasoning_budget: int,
|
| 1477 |
+
) -> dict[str, object]:
|
| 1478 |
+
maximum = _max_reasoning_budget(max_new_tokens)
|
| 1479 |
+
return gr.update(maximum=maximum, value=min(int(reasoning_budget), maximum))
|
| 1480 |
|
| 1481 |
+
|
| 1482 |
+
theme = gr.themes.Citrus()
|
| 1483 |
+
|
| 1484 |
+
with gr.Blocks(title="Nemotron-Labs-Audex") as demo:
|
| 1485 |
gr.Markdown(
|
| 1486 |
+
"# 🎧 Nemotron-Labs-Audex\n"
|
| 1487 |
+
'<div style="display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;">'
|
| 1488 |
+
'<a href="https://arxiv.org/abs/2607.05196">'
|
| 1489 |
+
'<img alt="Technical Report" src="https://img.shields.io/badge/2607.05196-Technical_Report-blue"></a>'
|
| 1490 |
+
'<a href="https://huggingface.co/collections/nvidia/nemotron-labs-audex">'
|
| 1491 |
+
'<img alt="Models" src="https://img.shields.io/badge/🤗-Models-blue"></a>'
|
| 1492 |
+
"</div>\n\n"
|
| 1493 |
+
"This is an interactive demo of [Nemotron-Labs-Audex-30B-A3B](https://huggingface.co/nvidia/Nemotron-Labs-Audex-30B-A3B) and [Nemotron-Labs-Audex-2B](https://huggingface.co/nvidia/Nemotron-Labs-Audex-2B). \n\n"
|
| 1494 |
+
"Audex extends the vocabulary for discrete audio tokens, as well as an audio encoder for audio inputs. Audex delivers strong abilities on audio tasks (audio understanding, speech recognition and translation, text-to-speech, audio generation, and speech-to-speech generation) while preserving very compelling reasoning, alignment, knowledge, long-context, and agentic capabilities of its text-only LLM backbone with marginal or no regression. Audex operates in both **thinking** and **instruct** (non-thinking) modes. \n\n"
|
| 1495 |
+
"Task selection loads a default prompt template and inference recipe.\n\n"
|
| 1496 |
+
"Your use of this model is governed by the [NVIDIA OneWay Noncommercial License](https://huggingface.co/nvidia/Nemotron-Labs-Audex-30B-A3B/blob/main/license/NVIDIA-OneWay-Noncommercial-License.docx/)."
|
| 1497 |
)
|
| 1498 |
with gr.Row():
|
| 1499 |
with gr.Column():
|
| 1500 |
+
unified_model = gr.Radio(
|
| 1501 |
+
choices=list(model_runtimes),
|
| 1502 |
+
value=DEFAULT_MODEL_NAME,
|
| 1503 |
+
label="Model",
|
| 1504 |
+
)
|
| 1505 |
+
unified_task = gr.Dropdown(
|
| 1506 |
+
choices=list(TASK_SPECS),
|
| 1507 |
+
value="Speech recognition (ASR)",
|
| 1508 |
+
label="Task",
|
| 1509 |
+
)
|
| 1510 |
+
unified_audio = gr.Audio(
|
| 1511 |
type="filepath",
|
| 1512 |
+
label=f"Input audio (up to {MAX_AUDIO_DURATION_SECONDS / 60:g} minutes)",
|
| 1513 |
sources=["upload", "microphone"],
|
| 1514 |
)
|
| 1515 |
+
unified_prompt = gr.Textbox(
|
| 1516 |
+
value=str(TASK_SPECS["Speech recognition (ASR)"]["template"]),
|
| 1517 |
+
label="Task template / prompt",
|
| 1518 |
+
lines=3,
|
| 1519 |
)
|
| 1520 |
+
gr.Markdown(
|
| 1521 |
+
"`<sound>` marks where audio embeddings enter the prompt; the app "
|
| 1522 |
+
"replaces it automatically. Text-only tasks have no input-modality token."
|
|
|
|
| 1523 |
)
|
| 1524 |
+
with gr.Row():
|
| 1525 |
+
unified_run_btn = gr.Button("Run", variant="primary", elem_id="audex-run")
|
| 1526 |
+
unified_stop_btn = gr.Button("Stop", variant="stop", elem_id="audex-stop")
|
| 1527 |
with gr.Accordion("Advanced options", open=False):
|
| 1528 |
+
unified_reasoning = gr.Checkbox(
|
| 1529 |
+
value=False,
|
| 1530 |
+
label="Enable reasoning (<think>) mode",
|
| 1531 |
+
)
|
| 1532 |
+
unified_reasoning_budget = gr.Slider(
|
| 1533 |
+
0,
|
| 1534 |
+
_max_reasoning_budget(MAX_NEW_TOKENS),
|
| 1535 |
+
value=DEFAULT_REASONING_BUDGET,
|
| 1536 |
+
step=16,
|
| 1537 |
+
label="Reasoning budget (0 = unlimited)",
|
| 1538 |
+
info=(
|
| 1539 |
+
"No reasoning cap by default. Set a positive threshold to enable "
|
| 1540 |
+
"a decoding-time cap with a 10% newline grace."
|
| 1541 |
+
),
|
| 1542 |
+
interactive=False,
|
| 1543 |
+
)
|
| 1544 |
+
unified_max_new_tokens = gr.Slider(
|
| 1545 |
16,
|
| 1546 |
MAX_NEW_TOKENS,
|
| 1547 |
+
value=DEFAULT_MAX_NEW_TOKENS,
|
| 1548 |
step=16,
|
| 1549 |
label="Max new tokens",
|
| 1550 |
+
info=(
|
| 1551 |
+
"Total cap shared by reasoning and the final answer. "
|
| 1552 |
+
"If reached, the displayed output is incomplete."
|
| 1553 |
+
),
|
| 1554 |
+
)
|
| 1555 |
+
unified_temperature = gr.Slider(
|
| 1556 |
+
0.1,
|
| 1557 |
+
1.5,
|
| 1558 |
+
value=1.0,
|
| 1559 |
+
step=0.05,
|
| 1560 |
+
label="Temperature",
|
| 1561 |
+
)
|
| 1562 |
+
unified_top_p = gr.Slider(
|
| 1563 |
+
0.1,
|
| 1564 |
+
1.0,
|
| 1565 |
+
value=1.0,
|
| 1566 |
+
step=0.05,
|
| 1567 |
+
label="Top-p",
|
| 1568 |
+
)
|
| 1569 |
+
unified_guidance_scale = gr.Slider(
|
| 1570 |
+
1.0,
|
| 1571 |
+
3.0,
|
| 1572 |
+
value=2.0,
|
| 1573 |
+
step=0.1,
|
| 1574 |
+
label="TTS CFG scale",
|
| 1575 |
+
visible=False,
|
| 1576 |
)
|
|
|
|
|
|
|
| 1577 |
gr.Markdown(
|
| 1578 |
+
"Task selection restores the official defaults. ASR and AST use "
|
| 1579 |
+
"greedy decoding; audio understanding and text reasoning use sampling. "
|
| 1580 |
+
"Reasoning has no separate cap unless you set one. TTS uses CFG 2.0; "
|
| 1581 |
+
"speech-to-speech uses CFG 1.5."
|
| 1582 |
)
|
| 1583 |
with gr.Column():
|
| 1584 |
+
unified_answer_out = gr.Textbox(label="Answer", lines=10)
|
| 1585 |
+
unified_thinking_out = gr.Textbox(
|
| 1586 |
+
label="Reasoning trace (if enabled)",
|
| 1587 |
+
lines=10,
|
| 1588 |
+
)
|
| 1589 |
+
unified_tts_audio_out = gr.Audio(
|
| 1590 |
+
label="Generated speech",
|
| 1591 |
+
type="filepath",
|
| 1592 |
+
autoplay=False,
|
| 1593 |
+
visible=False,
|
| 1594 |
+
)
|
| 1595 |
+
unified_tts_player = gr.HTML(
|
| 1596 |
+
value=player_value(0, reset=True),
|
| 1597 |
+
html_template=TTS_PLAYER_TEMPLATE,
|
| 1598 |
+
css_template=TTS_PLAYER_CSS,
|
| 1599 |
+
js_on_load=TTS_PLAYER_JS,
|
| 1600 |
+
apply_default_css=False,
|
| 1601 |
+
visible=False,
|
| 1602 |
+
)
|
| 1603 |
+
|
| 1604 |
+
unified_task.change(
|
| 1605 |
+
unified_task_defaults,
|
| 1606 |
+
inputs=unified_task,
|
| 1607 |
+
outputs=[
|
| 1608 |
+
unified_audio,
|
| 1609 |
+
unified_prompt,
|
| 1610 |
+
unified_reasoning,
|
| 1611 |
+
unified_reasoning_budget,
|
| 1612 |
+
unified_temperature,
|
| 1613 |
+
unified_top_p,
|
| 1614 |
+
unified_max_new_tokens,
|
| 1615 |
+
unified_guidance_scale,
|
| 1616 |
+
unified_answer_out,
|
| 1617 |
+
unified_thinking_out,
|
| 1618 |
+
unified_tts_audio_out,
|
| 1619 |
+
unified_tts_player,
|
| 1620 |
+
],
|
| 1621 |
+
api_name=False,
|
| 1622 |
+
)
|
| 1623 |
+
unified_reasoning.change(
|
| 1624 |
+
lambda enabled: gr.update(interactive=enabled),
|
| 1625 |
+
inputs=unified_reasoning,
|
| 1626 |
+
outputs=unified_reasoning_budget,
|
| 1627 |
+
api_name=False,
|
| 1628 |
+
)
|
| 1629 |
+
unified_max_new_tokens.change(
|
| 1630 |
+
update_reasoning_budget_limit,
|
| 1631 |
+
inputs=[unified_max_new_tokens, unified_reasoning_budget],
|
| 1632 |
+
outputs=unified_reasoning_budget,
|
| 1633 |
+
api_name=False,
|
| 1634 |
+
)
|
| 1635 |
|
| 1636 |
+
unified_run_event = unified_run_btn.click(
|
| 1637 |
+
run_unified,
|
| 1638 |
+
inputs=[
|
| 1639 |
+
unified_model,
|
| 1640 |
+
unified_task,
|
| 1641 |
+
unified_audio,
|
| 1642 |
+
unified_prompt,
|
| 1643 |
+
unified_reasoning,
|
| 1644 |
+
unified_reasoning_budget,
|
| 1645 |
+
unified_max_new_tokens,
|
| 1646 |
+
unified_temperature,
|
| 1647 |
+
unified_top_p,
|
| 1648 |
+
unified_guidance_scale,
|
| 1649 |
+
],
|
| 1650 |
+
outputs=[
|
| 1651 |
+
unified_answer_out,
|
| 1652 |
+
unified_thinking_out,
|
| 1653 |
+
unified_tts_audio_out,
|
| 1654 |
+
unified_tts_player,
|
| 1655 |
+
],
|
| 1656 |
+
api_name="run_unified",
|
| 1657 |
+
concurrency_id="audex-gpu",
|
| 1658 |
+
concurrency_limit=1,
|
| 1659 |
)
|
| 1660 |
+
unified_stop_btn.click(fn=None, cancels=[unified_run_event], api_name=False)
|
| 1661 |
|
| 1662 |
+
gr.Markdown(
|
| 1663 |
+
"### Curated examples\n"
|
| 1664 |
+
"Audio and text examples share the same task-driven interface."
|
| 1665 |
+
)
|
| 1666 |
gr.Examples(
|
| 1667 |
examples=[
|
| 1668 |
+
[
|
| 1669 |
+
"Speech recognition (ASR)",
|
| 1670 |
+
"examples/mlk_speech.wav",
|
| 1671 |
+
TASK_SPECS["Speech recognition (ASR)"]["template"],
|
| 1672 |
+
],
|
| 1673 |
+
[
|
| 1674 |
+
"Speech recognition (ASR)",
|
| 1675 |
+
"examples/sample_speech.wav",
|
| 1676 |
+
TASK_SPECS["Speech recognition (ASR)"]["template"],
|
| 1677 |
+
],
|
| 1678 |
+
[
|
| 1679 |
+
"Speech translation (AST)",
|
| 1680 |
+
"examples/korean_speech.wav",
|
| 1681 |
+
TASK_SPECS["Speech translation (AST)"]["template"],
|
| 1682 |
+
],
|
| 1683 |
+
[
|
| 1684 |
+
"Audio description",
|
| 1685 |
+
"examples/mlk_speech.wav",
|
| 1686 |
+
TASK_SPECS["Audio description"]["template"],
|
| 1687 |
+
],
|
| 1688 |
+
[
|
| 1689 |
+
"Audio description",
|
| 1690 |
+
"examples/sample_speech.wav",
|
| 1691 |
+
TASK_SPECS["Audio description"]["template"],
|
| 1692 |
+
],
|
| 1693 |
+
[
|
| 1694 |
+
"Audio question answering",
|
| 1695 |
+
"examples/mlk_speech.wav",
|
| 1696 |
+
TASK_SPECS["Audio question answering"]["template"],
|
| 1697 |
+
],
|
| 1698 |
+
[
|
| 1699 |
+
TEXT_TASK,
|
| 1700 |
+
None,
|
| 1701 |
+
MMLU_PRO_EXAMPLE_PROMPT,
|
| 1702 |
+
],
|
| 1703 |
+
[
|
| 1704 |
+
S2S_TASK,
|
| 1705 |
+
"examples/question_1059.mp3",
|
| 1706 |
+
S2S_UI_PROMPT,
|
| 1707 |
+
],
|
| 1708 |
+
[
|
| 1709 |
+
TTS_TASK,
|
| 1710 |
+
None,
|
| 1711 |
+
TTS_EXAMPLE_TEXT,
|
| 1712 |
+
],
|
| 1713 |
+
],
|
| 1714 |
+
inputs=[
|
| 1715 |
+
unified_task,
|
| 1716 |
+
unified_audio,
|
| 1717 |
+
unified_prompt,
|
| 1718 |
],
|
|
|
|
|
|
|
|
|
|
| 1719 |
cache_examples=False,
|
|
|
|
| 1720 |
)
|
| 1721 |
|
| 1722 |
if __name__ == "__main__":
|
| 1723 |
+
demo.queue(max_size=8).launch(
|
| 1724 |
+
server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 1725 |
+
server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
|
| 1726 |
+
share=os.environ.get("GRADIO_SHARE", "false").lower() == "true",
|
| 1727 |
+
theme=theme,
|
| 1728 |
+
ssr_mode=False,
|
| 1729 |
+
mcp_server=True,
|
| 1730 |
+
)
|
examples/korean_speech.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:65857d13573b3f2f793092865cc4da3ddd46d921c6f9ea6eb77e263aba00779c
|
| 3 |
+
size 255532
|
examples/question_1059.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f4615906395ee1dc6fdcb33dac1be954b211d32b7096132c8d110530d0d385d5
|
| 3 |
+
size 228000
|
reasoning_utils.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from transformers.generation.logits_process import LogitsProcessor
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ReasoningBudgetLogitsProcessor(LogitsProcessor):
|
| 11 |
+
"""Apply an optional reasoning-token budget for one sequence."""
|
| 12 |
+
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
tokenizer: Any,
|
| 16 |
+
prompt_length: int,
|
| 17 |
+
reasoning_budget: int,
|
| 18 |
+
grace_fraction: float = 0.1,
|
| 19 |
+
early_exit: str = ".\n</think>\n\n",
|
| 20 |
+
) -> None:
|
| 21 |
+
if reasoning_budget < 1:
|
| 22 |
+
raise ValueError("reasoning_budget must be positive")
|
| 23 |
+
|
| 24 |
+
self.tokenizer = tokenizer
|
| 25 |
+
self.prompt_length = prompt_length
|
| 26 |
+
self.reasoning_budget = reasoning_budget
|
| 27 |
+
self.hard_limit = reasoning_budget + max(
|
| 28 |
+
1, math.ceil(reasoning_budget * grace_fraction)
|
| 29 |
+
)
|
| 30 |
+
self.reasoning_end_ids = tokenizer.encode(
|
| 31 |
+
"</think>", add_special_tokens=False
|
| 32 |
+
)
|
| 33 |
+
self.early_exit_ids = tokenizer.encode(
|
| 34 |
+
early_exit, add_special_tokens=False
|
| 35 |
+
)
|
| 36 |
+
self.forced_index: int | None = None
|
| 37 |
+
self.done = False
|
| 38 |
+
|
| 39 |
+
def _force(self, scores: torch.FloatTensor, token_id: int) -> torch.FloatTensor:
|
| 40 |
+
scores.fill_(-float("inf"))
|
| 41 |
+
scores[:, token_id] = 0
|
| 42 |
+
return scores
|
| 43 |
+
|
| 44 |
+
def __call__(
|
| 45 |
+
self,
|
| 46 |
+
input_ids: torch.LongTensor,
|
| 47 |
+
scores: torch.FloatTensor,
|
| 48 |
+
) -> torch.FloatTensor:
|
| 49 |
+
if self.done:
|
| 50 |
+
return scores
|
| 51 |
+
if input_ids.shape[0] != 1:
|
| 52 |
+
raise ValueError("Reasoning budget control requires batch size 1")
|
| 53 |
+
|
| 54 |
+
generated_ids = input_ids[0, self.prompt_length :].tolist()
|
| 55 |
+
if self.forced_index is not None:
|
| 56 |
+
self.forced_index += 1
|
| 57 |
+
if self.forced_index >= len(self.early_exit_ids):
|
| 58 |
+
self.done = True
|
| 59 |
+
return scores
|
| 60 |
+
return self._force(scores, self.early_exit_ids[self.forced_index])
|
| 61 |
+
|
| 62 |
+
if generated_ids[-len(self.reasoning_end_ids) :] == self.reasoning_end_ids:
|
| 63 |
+
self.done = True
|
| 64 |
+
return scores
|
| 65 |
+
|
| 66 |
+
generated_tokens = len(generated_ids)
|
| 67 |
+
if generated_tokens < self.reasoning_budget:
|
| 68 |
+
return scores
|
| 69 |
+
|
| 70 |
+
ended_line = bool(
|
| 71 |
+
generated_ids
|
| 72 |
+
and "\n"
|
| 73 |
+
in self.tokenizer.decode(
|
| 74 |
+
[generated_ids[-1]],
|
| 75 |
+
skip_special_tokens=False,
|
| 76 |
+
clean_up_tokenization_spaces=False,
|
| 77 |
+
)
|
| 78 |
+
)
|
| 79 |
+
if not ended_line and generated_tokens < self.hard_limit:
|
| 80 |
+
return scores
|
| 81 |
+
|
| 82 |
+
self.forced_index = 0
|
| 83 |
+
return self._force(scores, self.early_exit_ids[0])
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
transformers
|
| 2 |
accelerate
|
| 3 |
sentencepiece
|
| 4 |
librosa
|
|
|
|
| 1 |
+
transformers==5.14.0
|
| 2 |
accelerate
|
| 3 |
sentencepiece
|
| 4 |
librosa
|
run_local.sh
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
+
VENV_DIR="${AUDEX_VENV_DIR:-$APP_DIR/.venv}"
|
| 6 |
+
PYTHON="$VENV_DIR/bin/python"
|
| 7 |
+
|
| 8 |
+
if [[ ! -x "$PYTHON" ]]; then
|
| 9 |
+
echo "Missing local environment. Run: bash $APP_DIR/setup_local.sh" >&2
|
| 10 |
+
exit 1
|
| 11 |
+
fi
|
| 12 |
+
|
| 13 |
+
export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0}"
|
| 14 |
+
|
| 15 |
+
gpu_info="$("$PYTHON" - <<'PY'
|
| 16 |
+
import re
|
| 17 |
+
import sys
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
if not torch.cuda.is_available():
|
| 22 |
+
raise SystemExit("CUDA is unavailable in this PyTorch environment.")
|
| 23 |
+
|
| 24 |
+
major, minor = torch.cuda.get_device_capability()
|
| 25 |
+
cuda = (torch.version.cuda or "unknown").replace(".", "")
|
| 26 |
+
torch_version = re.sub(r"[^0-9A-Za-z]+", "", torch.__version__.split("+", 1)[0])
|
| 27 |
+
python_version = f"{sys.version_info.major}{sys.version_info.minor}"
|
| 28 |
+
memory_gib = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
| 29 |
+
print(f"{major}.{minor}\tsm{major}{minor}-cu{cuda}-torch{torch_version}-py{python_version}\t{memory_gib:.0f}")
|
| 30 |
+
PY
|
| 31 |
+
)"
|
| 32 |
+
IFS=$'\t' read -r detected_arch runtime_tag memory_gib <<< "$gpu_info"
|
| 33 |
+
|
| 34 |
+
export AUDEX_TORCH_ARCH="${AUDEX_TORCH_ARCH:-$detected_arch}"
|
| 35 |
+
export AUDEX_WHEELS_DIR="${AUDEX_WHEELS_DIR:-$APP_DIR/.local/wheels/$runtime_tag}"
|
| 36 |
+
export AUDEX_BUILD_DIR="${AUDEX_BUILD_DIR:-$APP_DIR/.local/build/$runtime_tag}"
|
| 37 |
+
export AUDEX_MAX_AUDIO_SECONDS="${AUDEX_MAX_AUDIO_SECONDS:-900}"
|
| 38 |
+
export AUDEX_MAX_NEW_TOKENS="${AUDEX_MAX_NEW_TOKENS:-2048}"
|
| 39 |
+
export AUDEX_DEFAULT_MAX_NEW_TOKENS="${AUDEX_DEFAULT_MAX_NEW_TOKENS:-1024}"
|
| 40 |
+
export AUDEX_TTS_MAX_NEW_TOKENS="${AUDEX_TTS_MAX_NEW_TOKENS:-1024}"
|
| 41 |
+
export AUDEX_S2S_TEXT_MAX_NEW_TOKENS="${AUDEX_S2S_TEXT_MAX_NEW_TOKENS:-2048}"
|
| 42 |
+
export AUDEX_S2S_TTS_MAX_NEW_TOKENS="${AUDEX_S2S_TTS_MAX_NEW_TOKENS:-2400}"
|
| 43 |
+
export GRADIO_SERVER_NAME="${GRADIO_SERVER_NAME:-0.0.0.0}"
|
| 44 |
+
export GRADIO_SERVER_PORT="${GRADIO_SERVER_PORT:-7860}"
|
| 45 |
+
export GRADIO_SHARE="${GRADIO_SHARE:-false}"
|
| 46 |
+
|
| 47 |
+
echo "GPU architecture: $AUDEX_TORCH_ARCH (${memory_gib} GiB)"
|
| 48 |
+
echo "Models: Audex-30B-A3B and Audex-2B (downloaded from the Hub if uncached)"
|
| 49 |
+
|
| 50 |
+
exec "$PYTHON" "$APP_DIR/app.py"
|
setup_local.sh
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
+
VENV_DIR="${AUDEX_VENV_DIR:-$APP_DIR/.venv}"
|
| 6 |
+
PYTHON="$VENV_DIR/bin/python"
|
| 7 |
+
|
| 8 |
+
if [[ ! -x "$PYTHON" ]]; then
|
| 9 |
+
python3 -m venv "$VENV_DIR"
|
| 10 |
+
fi
|
| 11 |
+
|
| 12 |
+
"$PYTHON" -m pip install --upgrade pip
|
| 13 |
+
if ! "$PYTHON" -c "import torch" >/dev/null 2>&1; then
|
| 14 |
+
"$PYTHON" -m pip install "${AUDEX_TORCH_PACKAGE:-torch}"
|
| 15 |
+
fi
|
| 16 |
+
"$PYTHON" -m pip install \
|
| 17 |
+
--requirement "$APP_DIR/requirements.txt" \
|
| 18 |
+
"gradio==6.20.0" \
|
| 19 |
+
"huggingface_hub" \
|
| 20 |
+
"spaces>=0.51.0"
|
| 21 |
+
|
| 22 |
+
echo "Local environment ready: $VENV_DIR"
|
| 23 |
+
echo "Launch with: bash $APP_DIR/run_local.sh"
|
tts_player.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
_WORKLET_SOURCE = (Path(__file__).parent / "web" / "PCMPlayerWorklet.js").read_text()
|
| 8 |
+
|
| 9 |
+
TTS_PLAYER_TEMPLATE = """
|
| 10 |
+
<div class="audex-player">
|
| 11 |
+
<div class="audex-player-status">Ready</div>
|
| 12 |
+
<button class="audex-player-enable" type="button">Enable playback</button>
|
| 13 |
+
<audio class="audex-player-audio" controls></audio>
|
| 14 |
+
<a class="audex-player-download" hidden download="audex-tts.wav">Download WAV</a>
|
| 15 |
+
</div>
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
TTS_PLAYER_CSS = """
|
| 19 |
+
.audex-player {
|
| 20 |
+
display: grid;
|
| 21 |
+
gap: 0.75rem;
|
| 22 |
+
padding: 1rem;
|
| 23 |
+
border: 1px solid var(--border-color-primary);
|
| 24 |
+
border-radius: var(--radius-lg);
|
| 25 |
+
}
|
| 26 |
+
.audex-player-status {
|
| 27 |
+
color: var(--body-text-color-subdued);
|
| 28 |
+
}
|
| 29 |
+
.audex-player-audio {
|
| 30 |
+
width: 100%;
|
| 31 |
+
}
|
| 32 |
+
.audex-player-download {
|
| 33 |
+
color: var(--link-text-color);
|
| 34 |
+
font-weight: 600;
|
| 35 |
+
}
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
TTS_PLAYER_JS = """
|
| 39 |
+
const WORKLET_SOURCE = __WORKLET_SOURCE__;
|
| 40 |
+
const status = element.querySelector(".audex-player-status");
|
| 41 |
+
const enable = element.querySelector(".audex-player-enable");
|
| 42 |
+
const audio = element.querySelector(".audex-player-audio");
|
| 43 |
+
const download = element.querySelector(".audex-player-download");
|
| 44 |
+
let context = null;
|
| 45 |
+
let player = null;
|
| 46 |
+
let chunks = [];
|
| 47 |
+
let wavUrl = null;
|
| 48 |
+
let lastSequence = -1;
|
| 49 |
+
|
| 50 |
+
const ensurePlayer = async () => {
|
| 51 |
+
if (context) {
|
| 52 |
+
await context.resume();
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
context = new AudioContext();
|
| 56 |
+
const blob = new Blob([WORKLET_SOURCE], { type: "text/javascript" });
|
| 57 |
+
const moduleUrl = URL.createObjectURL(blob);
|
| 58 |
+
await context.audioWorklet.addModule(moduleUrl);
|
| 59 |
+
URL.revokeObjectURL(moduleUrl);
|
| 60 |
+
player = new AudioWorkletNode(
|
| 61 |
+
context,
|
| 62 |
+
"audex-pcm-player",
|
| 63 |
+
{ outputChannelCount: [1] },
|
| 64 |
+
);
|
| 65 |
+
player.connect(context.destination);
|
| 66 |
+
player.port.onmessage = (event) => {
|
| 67 |
+
if (event.data.type === "underrun") {
|
| 68 |
+
status.textContent = "Buffering generated speech…";
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
await context.resume();
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
const decodePcm = (encoded) => {
|
| 75 |
+
const binary = atob(encoded);
|
| 76 |
+
const bytes = new Uint8Array(binary.length);
|
| 77 |
+
for (let index = 0; index < binary.length; index += 1) {
|
| 78 |
+
bytes[index] = binary.charCodeAt(index);
|
| 79 |
+
}
|
| 80 |
+
return new Float32Array(bytes.buffer);
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
const resample = (input, sourceRate, targetRate) => {
|
| 84 |
+
if (sourceRate === targetRate) return input;
|
| 85 |
+
const output = new Float32Array(Math.max(1, Math.round(input.length * targetRate / sourceRate)));
|
| 86 |
+
const ratio = sourceRate / targetRate;
|
| 87 |
+
for (let index = 0; index < output.length; index += 1) {
|
| 88 |
+
const position = index * ratio;
|
| 89 |
+
const left = Math.floor(position);
|
| 90 |
+
const right = Math.min(left + 1, input.length - 1);
|
| 91 |
+
const fraction = position - left;
|
| 92 |
+
output[index] = input[left] * (1 - fraction) + input[right] * fraction;
|
| 93 |
+
}
|
| 94 |
+
return output;
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const buildWav = (parts, sampleRate) => {
|
| 98 |
+
const length = parts.reduce((total, part) => total + part.length, 0);
|
| 99 |
+
const buffer = new ArrayBuffer(44 + length * 2);
|
| 100 |
+
const view = new DataView(buffer);
|
| 101 |
+
const write = (offset, text) => {
|
| 102 |
+
for (let index = 0; index < text.length; index += 1) {
|
| 103 |
+
view.setUint8(offset + index, text.charCodeAt(index));
|
| 104 |
+
}
|
| 105 |
+
};
|
| 106 |
+
write(0, "RIFF");
|
| 107 |
+
view.setUint32(4, 36 + length * 2, true);
|
| 108 |
+
write(8, "WAVE");
|
| 109 |
+
write(12, "fmt ");
|
| 110 |
+
view.setUint32(16, 16, true);
|
| 111 |
+
view.setUint16(20, 1, true);
|
| 112 |
+
view.setUint16(22, 1, true);
|
| 113 |
+
view.setUint32(24, sampleRate, true);
|
| 114 |
+
view.setUint32(28, sampleRate * 2, true);
|
| 115 |
+
view.setUint16(32, 2, true);
|
| 116 |
+
view.setUint16(34, 16, true);
|
| 117 |
+
write(36, "data");
|
| 118 |
+
view.setUint32(40, length * 2, true);
|
| 119 |
+
let offset = 44;
|
| 120 |
+
for (const part of parts) {
|
| 121 |
+
for (const sample of part) {
|
| 122 |
+
const clipped = Math.max(-1, Math.min(1, sample));
|
| 123 |
+
view.setInt16(offset, clipped < 0 ? clipped * 32768 : clipped * 32767, true);
|
| 124 |
+
offset += 2;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
return new Blob([buffer], { type: "audio/wav" });
|
| 128 |
+
};
|
| 129 |
+
|
| 130 |
+
const reset = async () => {
|
| 131 |
+
chunks = [];
|
| 132 |
+
lastSequence = -1;
|
| 133 |
+
audio.removeAttribute("src");
|
| 134 |
+
audio.load();
|
| 135 |
+
download.hidden = true;
|
| 136 |
+
if (wavUrl) URL.revokeObjectURL(wavUrl);
|
| 137 |
+
wavUrl = null;
|
| 138 |
+
await ensurePlayer();
|
| 139 |
+
player.port.postMessage({ type: "reset" });
|
| 140 |
+
status.textContent = "Waiting for speech tokens…";
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
const consume = async () => {
|
| 144 |
+
const value = props.value || {};
|
| 145 |
+
if (value.sequence === lastSequence) return;
|
| 146 |
+
lastSequence = value.sequence;
|
| 147 |
+
if (value.reset) await reset();
|
| 148 |
+
if (value.pcm) {
|
| 149 |
+
await ensurePlayer();
|
| 150 |
+
const pcm = decodePcm(value.pcm);
|
| 151 |
+
chunks.push(pcm.slice());
|
| 152 |
+
const playback = resample(pcm, value.sample_rate || 16000, context.sampleRate);
|
| 153 |
+
player.port.postMessage({ type: "audio", samples: playback.buffer }, [playback.buffer]);
|
| 154 |
+
status.textContent = `Streaming ${value.token_count || 0} speech tokens…`;
|
| 155 |
+
}
|
| 156 |
+
if (value.done && chunks.length) {
|
| 157 |
+
const wav = buildWav(chunks, value.sample_rate || 16000);
|
| 158 |
+
wavUrl = URL.createObjectURL(wav);
|
| 159 |
+
audio.src = wavUrl;
|
| 160 |
+
download.href = wavUrl;
|
| 161 |
+
download.hidden = false;
|
| 162 |
+
status.textContent = `Complete · ${value.token_count || 0} speech tokens`;
|
| 163 |
+
}
|
| 164 |
+
};
|
| 165 |
+
|
| 166 |
+
const runButton = document.getElementById("audex-run");
|
| 167 |
+
if (runButton) runButton.addEventListener("click", () => ensurePlayer());
|
| 168 |
+
enable.addEventListener("click", async () => {
|
| 169 |
+
await ensurePlayer();
|
| 170 |
+
status.textContent = "Playback enabled";
|
| 171 |
+
});
|
| 172 |
+
const stopButton = document.getElementById("audex-stop");
|
| 173 |
+
if (stopButton) {
|
| 174 |
+
stopButton.addEventListener("click", () => {
|
| 175 |
+
if (player) player.port.postMessage({ type: "reset" });
|
| 176 |
+
status.textContent = "Stopped";
|
| 177 |
+
});
|
| 178 |
+
}
|
| 179 |
+
watch("value", consume);
|
| 180 |
+
consume();
|
| 181 |
+
""".replace("__WORKLET_SOURCE__", json.dumps(_WORKLET_SOURCE))
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def player_value(
|
| 185 |
+
sequence: int,
|
| 186 |
+
*,
|
| 187 |
+
pcm: str | None = None,
|
| 188 |
+
token_count: int = 0,
|
| 189 |
+
done: bool = False,
|
| 190 |
+
reset: bool = False,
|
| 191 |
+
) -> dict[str, object]:
|
| 192 |
+
return {
|
| 193 |
+
"sequence": sequence,
|
| 194 |
+
"sample_rate": 16000,
|
| 195 |
+
"pcm": pcm,
|
| 196 |
+
"token_count": token_count,
|
| 197 |
+
"done": done,
|
| 198 |
+
"reset": reset,
|
| 199 |
+
}
|
tts_utils.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import tempfile
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from queue import Queue
|
| 9 |
+
from threading import Event, Thread
|
| 10 |
+
from typing import Any, Iterator
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import soundfile as sf
|
| 14 |
+
import torch
|
| 15 |
+
from transformers import AutoModel
|
| 16 |
+
from transformers.generation.logits_process import LogitsProcessor
|
| 17 |
+
from transformers.generation.stopping_criteria import StoppingCriteria
|
| 18 |
+
from transformers.generation.streamers import BaseStreamer
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
SAMPLE_RATE = 16000
|
| 22 |
+
TTS_PREFIX = "<|text to speech|> Generate speech for this transcription. "
|
| 23 |
+
DEFAULT_SYSTEM_PROMPT = (
|
| 24 |
+
"You are a helpful and harmless assistant.\n\n"
|
| 25 |
+
"You are not allowed to use any tools."
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass(frozen=True)
|
| 30 |
+
class SpeechTokenMap:
|
| 31 |
+
start: int
|
| 32 |
+
end: int
|
| 33 |
+
codec_start: int
|
| 34 |
+
codec_end: int
|
| 35 |
+
eos: int
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass(frozen=True)
|
| 39 |
+
class TTSStreamEvent:
|
| 40 |
+
pcm: np.ndarray | None
|
| 41 |
+
token_count: int
|
| 42 |
+
elapsed_seconds: float
|
| 43 |
+
done: bool = False
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class SpeechTokenLogitsProcessor(LogitsProcessor):
|
| 47 |
+
def __init__(self, token_map: SpeechTokenMap):
|
| 48 |
+
self.token_map = token_map
|
| 49 |
+
|
| 50 |
+
def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
|
| 51 |
+
eos_scores = scores[:, self.token_map.eos].clone()
|
| 52 |
+
scores[:, : self.token_map.end] = -float("inf")
|
| 53 |
+
scores[:, self.token_map.codec_end + 1 :] = -float("inf")
|
| 54 |
+
scores[:, self.token_map.eos] = eos_scores
|
| 55 |
+
return scores
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class EventStoppingCriteria(StoppingCriteria):
|
| 59 |
+
def __init__(self, event: Event):
|
| 60 |
+
self.event = event
|
| 61 |
+
|
| 62 |
+
def __call__(
|
| 63 |
+
self,
|
| 64 |
+
input_ids: torch.LongTensor,
|
| 65 |
+
scores: torch.FloatTensor,
|
| 66 |
+
**kwargs: Any,
|
| 67 |
+
) -> torch.BoolTensor:
|
| 68 |
+
return torch.full(
|
| 69 |
+
(input_ids.shape[0],),
|
| 70 |
+
self.event.is_set(),
|
| 71 |
+
dtype=torch.bool,
|
| 72 |
+
device=input_ids.device,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class TokenIdStreamer(BaseStreamer):
|
| 77 |
+
_END = object()
|
| 78 |
+
|
| 79 |
+
def __init__(self) -> None:
|
| 80 |
+
self.queue: Queue[int | BaseException | object] = Queue()
|
| 81 |
+
self.is_prompt = True
|
| 82 |
+
|
| 83 |
+
def put(self, value: torch.Tensor) -> None:
|
| 84 |
+
if self.is_prompt:
|
| 85 |
+
self.is_prompt = False
|
| 86 |
+
return
|
| 87 |
+
token_ids = value.reshape(-1).tolist()
|
| 88 |
+
if len(token_ids) != 1:
|
| 89 |
+
raise ValueError(f"TTS streaming requires batch size 1, got {len(token_ids)} tokens")
|
| 90 |
+
self.queue.put(int(token_ids[0]))
|
| 91 |
+
|
| 92 |
+
def end(self) -> None:
|
| 93 |
+
self.queue.put(self._END)
|
| 94 |
+
|
| 95 |
+
def fail(self, error: BaseException) -> None:
|
| 96 |
+
self.queue.put(error)
|
| 97 |
+
self.end()
|
| 98 |
+
|
| 99 |
+
def __iter__(self) -> Iterator[int]:
|
| 100 |
+
while True:
|
| 101 |
+
item = self.queue.get()
|
| 102 |
+
if item is self._END:
|
| 103 |
+
return
|
| 104 |
+
if isinstance(item, BaseException):
|
| 105 |
+
raise item
|
| 106 |
+
yield int(item)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def build_tts_prompt(text: str, tokenizer: Any) -> str:
|
| 110 |
+
messages = [
|
| 111 |
+
{"role": "system", "content": DEFAULT_SYSTEM_PROMPT},
|
| 112 |
+
{"role": "user", "content": f"{TTS_PREFIX}{text}"},
|
| 113 |
+
]
|
| 114 |
+
return (
|
| 115 |
+
tokenizer.apply_chat_template(
|
| 116 |
+
messages,
|
| 117 |
+
tokenize=False,
|
| 118 |
+
add_generation_prompt=True,
|
| 119 |
+
enable_thinking=False,
|
| 120 |
+
)
|
| 121 |
+
+ "<speechgen_start>"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def build_tts_null_prompt(cond_prompt: str, tokenizer: Any, max_iters: int = 64) -> str:
|
| 126 |
+
target_len = len(tokenizer.encode(cond_prompt))
|
| 127 |
+
|
| 128 |
+
def template(null_text: str) -> str:
|
| 129 |
+
return build_tts_prompt(null_text, tokenizer)
|
| 130 |
+
|
| 131 |
+
base_len = len(tokenizer.encode(template("")))
|
| 132 |
+
n_unk = max(1, target_len - base_len)
|
| 133 |
+
for _ in range(max_iters):
|
| 134 |
+
prompt = template("<unk>" * n_unk)
|
| 135 |
+
current_len = len(tokenizer.encode(prompt))
|
| 136 |
+
if current_len == target_len:
|
| 137 |
+
return prompt
|
| 138 |
+
n_unk += 1 if current_len < target_len else -1
|
| 139 |
+
n_unk = max(1, n_unk)
|
| 140 |
+
raise ValueError("Unable to construct an equal-length TTS CFG null prompt")
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def build_speech_token_map(tokenizer: Any) -> SpeechTokenMap:
|
| 144 |
+
token_map = SpeechTokenMap(
|
| 145 |
+
start=tokenizer.convert_tokens_to_ids("<speechgen_start>"),
|
| 146 |
+
end=tokenizer.convert_tokens_to_ids("<speechgen_end>"),
|
| 147 |
+
codec_start=tokenizer.convert_tokens_to_ids("<speechcodec_0>"),
|
| 148 |
+
codec_end=tokenizer.convert_tokens_to_ids("<speechcodec_65535>"),
|
| 149 |
+
eos=int(tokenizer.eos_token_id),
|
| 150 |
+
)
|
| 151 |
+
expected = (131075, 131076, 131077, 196612)
|
| 152 |
+
actual = (token_map.start, token_map.end, token_map.codec_start, token_map.codec_end)
|
| 153 |
+
if actual != expected:
|
| 154 |
+
raise ValueError(f"Unexpected Audex speech token layout: expected={expected}, actual={actual}")
|
| 155 |
+
return token_map
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def load_speech_decoder(model_path: str, device: str = "cuda") -> Any:
|
| 159 |
+
path = Path(model_path)
|
| 160 |
+
for filename in ("config.json", "model.safetensors"):
|
| 161 |
+
if not (path / filename).is_file():
|
| 162 |
+
raise FileNotFoundError(f"Speech decoder file not found: {path / filename}")
|
| 163 |
+
|
| 164 |
+
decoder, loading_info = AutoModel.from_pretrained(
|
| 165 |
+
str(path),
|
| 166 |
+
trust_remote_code=True,
|
| 167 |
+
output_loading_info=True,
|
| 168 |
+
)
|
| 169 |
+
invalid_keys = {
|
| 170 |
+
key: loading_info[key]
|
| 171 |
+
for key in ("missing_keys", "unexpected_keys", "mismatched_keys")
|
| 172 |
+
if loading_info[key]
|
| 173 |
+
}
|
| 174 |
+
if invalid_keys:
|
| 175 |
+
raise RuntimeError(f"Speech decoder checkpoint loading was incomplete: {invalid_keys}")
|
| 176 |
+
|
| 177 |
+
for module in decoder.modules():
|
| 178 |
+
if {"theta", "cache"} <= getattr(module, "_non_persistent_buffers_set", set()):
|
| 179 |
+
module.rope_init(device=module.theta.device)
|
| 180 |
+
module._rope_ready = False
|
| 181 |
+
|
| 182 |
+
sample_rate = int(getattr(decoder.config, "sample_rate", SAMPLE_RATE))
|
| 183 |
+
if sample_rate != SAMPLE_RATE:
|
| 184 |
+
raise ValueError(f"Expected a {SAMPLE_RATE} Hz speech decoder, got {sample_rate} Hz")
|
| 185 |
+
decoder = decoder.to(device).eval()
|
| 186 |
+
for parameter in decoder.parameters():
|
| 187 |
+
parameter.requires_grad = False
|
| 188 |
+
return decoder
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def stream_tts(
|
| 192 |
+
model: Any,
|
| 193 |
+
tokenizer: Any,
|
| 194 |
+
decoder: Any,
|
| 195 |
+
text: str,
|
| 196 |
+
max_new_tokens: int,
|
| 197 |
+
*,
|
| 198 |
+
temperature: float = 0.8,
|
| 199 |
+
top_p: float = 1.0,
|
| 200 |
+
top_k: int = 0,
|
| 201 |
+
guidance_scale: float = 2.0,
|
| 202 |
+
seed: int = 0,
|
| 203 |
+
chunk_frames: int = 5,
|
| 204 |
+
) -> Iterator[TTSStreamEvent]:
|
| 205 |
+
text = text.strip()
|
| 206 |
+
if not text:
|
| 207 |
+
raise ValueError("Text to synthesize must not be empty")
|
| 208 |
+
|
| 209 |
+
token_map = build_speech_token_map(tokenizer)
|
| 210 |
+
cond_prompt = build_tts_prompt(text, tokenizer)
|
| 211 |
+
uncond_prompt = build_tts_null_prompt(cond_prompt, tokenizer)
|
| 212 |
+
cond_ids = tokenizer.encode(cond_prompt, return_tensors="pt").to(model.device)
|
| 213 |
+
uncond_ids = tokenizer.encode(uncond_prompt, return_tensors="pt").to(model.device)
|
| 214 |
+
if cond_ids.shape != uncond_ids.shape:
|
| 215 |
+
raise ValueError(
|
| 216 |
+
f"TTS CFG prompt lengths differ: conditional={cond_ids.shape[-1]}, "
|
| 217 |
+
f"unconditional={uncond_ids.shape[-1]}"
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
streamer = TokenIdStreamer()
|
| 221 |
+
cancel_event = Event()
|
| 222 |
+
session = decoder.create_session(
|
| 223 |
+
chunk_frames=chunk_frames,
|
| 224 |
+
sample_rate=SAMPLE_RATE,
|
| 225 |
+
return_numpy=True,
|
| 226 |
+
)
|
| 227 |
+
generation_error: list[BaseException] = []
|
| 228 |
+
started_at = time.perf_counter()
|
| 229 |
+
|
| 230 |
+
def generate() -> None:
|
| 231 |
+
try:
|
| 232 |
+
torch.manual_seed(seed)
|
| 233 |
+
torch.cuda.manual_seed_all(seed)
|
| 234 |
+
model.generate(
|
| 235 |
+
input_ids=cond_ids,
|
| 236 |
+
attention_mask=torch.ones_like(cond_ids),
|
| 237 |
+
negative_prompt_ids=uncond_ids,
|
| 238 |
+
negative_prompt_attention_mask=torch.ones_like(uncond_ids),
|
| 239 |
+
guidance_scale=guidance_scale,
|
| 240 |
+
do_sample=True,
|
| 241 |
+
temperature=temperature,
|
| 242 |
+
top_p=top_p,
|
| 243 |
+
**({"top_k": top_k} if top_k > 0 else {}),
|
| 244 |
+
max_new_tokens=max_new_tokens,
|
| 245 |
+
eos_token_id=[token_map.end, token_map.eos],
|
| 246 |
+
pad_token_id=tokenizer.pad_token_id or token_map.eos,
|
| 247 |
+
logits_processor=[SpeechTokenLogitsProcessor(token_map)],
|
| 248 |
+
stopping_criteria=[EventStoppingCriteria(cancel_event)],
|
| 249 |
+
streamer=streamer,
|
| 250 |
+
use_cache=True,
|
| 251 |
+
)
|
| 252 |
+
except BaseException as error:
|
| 253 |
+
generation_error.append(error)
|
| 254 |
+
streamer.fail(error)
|
| 255 |
+
|
| 256 |
+
thread = Thread(target=generate, daemon=True)
|
| 257 |
+
thread.start()
|
| 258 |
+
token_count = 0
|
| 259 |
+
completed = False
|
| 260 |
+
try:
|
| 261 |
+
for token_id in streamer:
|
| 262 |
+
if token_id == token_map.end or token_id == token_map.eos:
|
| 263 |
+
completed = True
|
| 264 |
+
break
|
| 265 |
+
if not token_map.codec_start <= token_id <= token_map.codec_end:
|
| 266 |
+
raise RuntimeError(f"Unexpected token in TTS output: {token_id}")
|
| 267 |
+
|
| 268 |
+
token_count += 1
|
| 269 |
+
codec_index = token_id - token_map.codec_start
|
| 270 |
+
if token_count == 1:
|
| 271 |
+
yield TTSStreamEvent(
|
| 272 |
+
pcm=None,
|
| 273 |
+
token_count=token_count,
|
| 274 |
+
elapsed_seconds=time.perf_counter() - started_at,
|
| 275 |
+
)
|
| 276 |
+
for _, pcm in session.push([[codec_index]]):
|
| 277 |
+
yield TTSStreamEvent(
|
| 278 |
+
pcm=np.asarray(pcm, dtype=np.float32),
|
| 279 |
+
token_count=token_count,
|
| 280 |
+
elapsed_seconds=time.perf_counter() - started_at,
|
| 281 |
+
)
|
| 282 |
+
else:
|
| 283 |
+
completed = True
|
| 284 |
+
|
| 285 |
+
if generation_error:
|
| 286 |
+
raise generation_error[0]
|
| 287 |
+
if completed:
|
| 288 |
+
for _, pcm in session.flush():
|
| 289 |
+
yield TTSStreamEvent(
|
| 290 |
+
pcm=np.asarray(pcm, dtype=np.float32),
|
| 291 |
+
token_count=token_count,
|
| 292 |
+
elapsed_seconds=time.perf_counter() - started_at,
|
| 293 |
+
)
|
| 294 |
+
yield TTSStreamEvent(
|
| 295 |
+
pcm=None,
|
| 296 |
+
token_count=token_count,
|
| 297 |
+
elapsed_seconds=time.perf_counter() - started_at,
|
| 298 |
+
done=True,
|
| 299 |
+
)
|
| 300 |
+
finally:
|
| 301 |
+
cancel_event.set()
|
| 302 |
+
thread.join(timeout=10)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def encode_pcm_chunk(pcm: np.ndarray) -> str:
|
| 306 |
+
samples = np.asarray(pcm, dtype="<f4")
|
| 307 |
+
return base64.b64encode(samples.tobytes()).decode("ascii")
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def write_wav(pcm: np.ndarray, sample_rate: int = SAMPLE_RATE) -> str:
|
| 311 |
+
output = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
| 312 |
+
output.close()
|
| 313 |
+
sf.write(output.name, np.asarray(pcm, dtype=np.float32), sample_rate, subtype="PCM_16")
|
| 314 |
+
return output.name
|
web/PCMPlayerWorklet.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class PCMPlayerProcessor extends AudioWorkletProcessor {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.queue = [];
|
| 5 |
+
this.offset = 0;
|
| 6 |
+
this.bufferedSamples = 0;
|
| 7 |
+
this.started = false;
|
| 8 |
+
this.startThreshold = Math.round(sampleRate * 0.25);
|
| 9 |
+
this.port.onmessage = (event) => {
|
| 10 |
+
if (event.data.type === "reset") {
|
| 11 |
+
this.queue = [];
|
| 12 |
+
this.offset = 0;
|
| 13 |
+
this.bufferedSamples = 0;
|
| 14 |
+
this.started = false;
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
if (event.data.type === "audio") {
|
| 18 |
+
const samples = new Float32Array(event.data.samples);
|
| 19 |
+
this.queue.push(samples);
|
| 20 |
+
this.bufferedSamples += samples.length;
|
| 21 |
+
}
|
| 22 |
+
};
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
process(_inputs, outputs) {
|
| 26 |
+
const output = outputs[0][0];
|
| 27 |
+
output.fill(0);
|
| 28 |
+
if (!this.started) {
|
| 29 |
+
this.started = this.bufferedSamples >= this.startThreshold;
|
| 30 |
+
if (!this.started) {
|
| 31 |
+
return true;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
let outputOffset = 0;
|
| 36 |
+
while (outputOffset < output.length && this.queue.length) {
|
| 37 |
+
const current = this.queue[0];
|
| 38 |
+
const count = Math.min(output.length - outputOffset, current.length - this.offset);
|
| 39 |
+
output.set(current.subarray(this.offset, this.offset + count), outputOffset);
|
| 40 |
+
outputOffset += count;
|
| 41 |
+
this.offset += count;
|
| 42 |
+
this.bufferedSamples -= count;
|
| 43 |
+
if (this.offset === current.length) {
|
| 44 |
+
this.queue.shift();
|
| 45 |
+
this.offset = 0;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
if (this.started && outputOffset < output.length) {
|
| 49 |
+
this.port.postMessage({ type: "underrun" });
|
| 50 |
+
}
|
| 51 |
+
return true;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
registerProcessor("audex-pcm-player", PCMPlayerProcessor);
|