|
| 1 | +# Higgs Audio v2 |
| 2 | + |
| 3 | +Higgs Audio v2 is a Llama-3.2-3B-backed TTS with multi-codebook acoustic tokens and delay-pattern streaming. The MLX port targets the 3B open-weights release from Boson AI and reuses the in-tree HiggsAudio acoustic tokenizer (originally added for OmniVoice). |
| 4 | + |
| 5 | +## Highlights |
| 6 | + |
| 7 | +- Real-time voice cloning on Apple Silicon (RTF ≈ 0.6× bf16 / 0.36× q8 / 0.33× q6 on M5 Max) |
| 8 | +- Reference-audio voice cloning via ChatML prompt format |
| 9 | +- Full `AUDIO_INIT` + delay-pattern ramp-in/out state machine |
| 10 | +- Repetition-avoidance sampling (RAS) for stable long-form output |
| 11 | +- MLX native 4/6/8-bit quantization with optional per-layer protection |
| 12 | + |
| 13 | +## Basic usage |
| 14 | + |
| 15 | +### Top-level CLI |
| 16 | + |
| 17 | +```bash |
| 18 | +python -m mlx_audio.tts.generate \ |
| 19 | + --model mlx-community/higgs-audio-v2-3B-mlx-q8 \ |
| 20 | + --text "Hello from Higgs Audio on MLX." \ |
| 21 | + --ref_audio path/to/reference.wav \ |
| 22 | + --ref_text "Transcript of the reference clip." |
| 23 | +``` |
| 24 | + |
| 25 | +The `Model` class conforms to the standard mlx-audio interface, so the |
| 26 | +existing `mlx_audio.tts.generate` CLI and `mlx_audio.server` both work |
| 27 | +unchanged against Higgs. |
| 28 | + |
| 29 | +### Python API (standard) |
| 30 | + |
| 31 | +```python |
| 32 | +from mlx_audio.tts.utils import load |
| 33 | +import soundfile as sf |
| 34 | + |
| 35 | +model = load("mlx-community/higgs-audio-v2-3B-mlx-q8") |
| 36 | + |
| 37 | +for result in model.generate( |
| 38 | + text="Hello from Higgs Audio on MLX.", |
| 39 | + ref_audio="path/to/reference.wav", # optional; strongly recommended |
| 40 | + ref_text="Transcript of the reference clip.", |
| 41 | + temperature=0.7, |
| 42 | + top_p=0.95, |
| 43 | + max_new_frames=1200, |
| 44 | + fade_in_ms=30.0, |
| 45 | +): |
| 46 | + sf.write("output.wav", result.audio, result.sample_rate) |
| 47 | +``` |
| 48 | + |
| 49 | +Without `ref_audio`, generation runs in "smart voice" mode (random voice |
| 50 | +per sample). This works but is less reliable than voice cloning — the |
| 51 | +sampling occasionally collapses to `stream_eos` early and produces silent |
| 52 | +output. If that happens, rerun (each call draws fresh noise) or pass |
| 53 | +`ref_audio`. For production use, a reference voice is strongly recommended. |
| 54 | + |
| 55 | +### Python API (Higgs-specific kwargs) |
| 56 | + |
| 57 | +For direct access to the full Higgs parameter surface (RAS windowing, |
| 58 | +sampling warmup, pre-loaded codec override, etc.), use `HiggsAudioServer`: |
| 59 | + |
| 60 | +```python |
| 61 | +from mlx_audio.tts.models.higgs_audio import HiggsAudioServer |
| 62 | +import soundfile as sf |
| 63 | + |
| 64 | +server = HiggsAudioServer.from_pretrained( |
| 65 | + model_path="bosonai/higgs-audio-v2-generation-3B-base", # bf16 base |
| 66 | + codec_path="mlx-community/higgs-audio-v2-tokenizer", # acoustic tokenizer |
| 67 | +) |
| 68 | + |
| 69 | +result = server.generate( |
| 70 | + target_text="Hello from Higgs Audio on MLX.", |
| 71 | + temperature=0.7, |
| 72 | + top_p=0.95, |
| 73 | + max_new_frames=1200, |
| 74 | + fade_in_ms=30.0, |
| 75 | +) |
| 76 | +sf.write("output.wav", result.pcm, result.sampling_rate) |
| 77 | +``` |
| 78 | + |
| 79 | +### Recommended parameters |
| 80 | + |
| 81 | +- `temperature=0.7`, `top_p=0.95` — proven stable across prompt lengths during the M5 benchmark |
| 82 | +- `max_new_frames=1200` — generous cap; generation stops naturally at the EOS ramp |
| 83 | +- `fade_in_ms=30.0`, `fade_out_ms=15.0` — suppresses the first-frame transient that the 5ms default occasionally lets through |
| 84 | + |
| 85 | +## Voice cloning |
| 86 | + |
| 87 | +Pass `ref_audio` (path or pre-loaded mx.array at 24 kHz mono) together with |
| 88 | +`ref_text` (the transcript of that clip). Reference audio is encoded through |
| 89 | +the in-tree `HiggsAudioTokenizer` and stitched into the assistant turn of a |
| 90 | +ChatML prompt — the transcript is required for stable alignment between the |
| 91 | +cloned voice and the target text. |
| 92 | + |
| 93 | +```python |
| 94 | +for result in model.generate( |
| 95 | + text="Hello, this is a cloned voice.", |
| 96 | + ref_audio="reference.wav", |
| 97 | + ref_text="Transcript of the reference clip.", |
| 98 | + temperature=0.7, |
| 99 | + top_p=0.95, |
| 100 | + max_new_frames=1200, |
| 101 | + fade_in_ms=30.0, |
| 102 | +): |
| 103 | + sf.write("output.wav", result.audio, result.sample_rate) |
| 104 | +``` |
| 105 | + |
| 106 | +Best results come from 5–15 seconds of clean reference speech. |
| 107 | + |
| 108 | +### Bundled sample voices |
| 109 | + |
| 110 | +Three drop-in reference voices ship in `examples/voice_prompts/`, generated via Higgs smart-voice mode so they're license-clean: |
| 111 | + |
| 112 | +- `en_woman.wav` — English, feminine register |
| 113 | +- `en_man.wav` — English, masculine register |
| 114 | +- `en_man_deep.wav` — English, masculine register, lower pitch |
| 115 | + |
| 116 | +Each `.wav` is paired with a matching `.txt` transcript. See `examples/voice_prompts/README.md` for the usage snippet. |
| 117 | + |
| 118 | +## Streaming |
| 119 | + |
| 120 | +For chunked streaming output (e.g. Pipecat pipelines), use |
| 121 | +`HiggsAudioServer.generate_stream`: |
| 122 | + |
| 123 | +```python |
| 124 | +for pcm_chunk in server.generate_stream( |
| 125 | + target_text="Generating in chunks for live playback.", |
| 126 | + reference_audio_path="reference.wav", |
| 127 | + reference_text="...", |
| 128 | + chunk_ms=640.0, |
| 129 | +): |
| 130 | + # emit or resample pcm_chunk (float32 at 24 kHz) |
| 131 | + ... |
| 132 | +``` |
| 133 | + |
| 134 | +Current shape: full generate, then chunk the resulting PCM. Per-chunk quality matches non-streaming exactly. Mid-generation streaming (emit-as-you-go) is not yet supported because the neural-vocoder codec produces subtly different PCM at the same sample position when called with different accumulated lengths — boundary discontinuities become audible. Proper overlap-add streaming is follow-up work. |
| 135 | + |
| 136 | +## Quantization |
| 137 | + |
| 138 | +MLX native 4/6/8-bit quantization works on the Llama backbone. The audio head and audio codebook embeddings benefit from staying at bf16 — quantizing them introduces voice-character drift (pitch register shifts at q6, trajectory instability at q4). |
| 139 | + |
| 140 | +Already-quantized checkpoints load transparently via `load(...)` — config.json carries a `quantization` block that the framework applies before weight load. To quantize in place on a fresh bf16 load, use `model.model_quant_predicate`: |
| 141 | + |
| 142 | +```python |
| 143 | +import mlx.core as mx |
| 144 | +import mlx.nn as nn |
| 145 | +from mlx_audio.tts.utils import load |
| 146 | + |
| 147 | +model = load("bosonai/higgs-audio-v2-generation-3B-base") |
| 148 | +nn.quantize(model, group_size=64, bits=8, class_predicate=model.model_quant_predicate) |
| 149 | +mx.eval(model.parameters()) |
| 150 | +``` |
| 151 | + |
| 152 | +Benchmark on M5 Max (warm), long-prompt RTF: |
| 153 | + |
| 154 | +| variant | RTF | weights size | notes | |
| 155 | +|---------|-------|--------------|---------------------------------------------| |
| 156 | +| bf16 | 0.60× | 6.8 GB | `bosonai/higgs-audio-v2-generation-3B-base` (authoritative) | |
| 157 | +| q8 | 0.36× | 6.18 GB | `mlx-community/higgs-audio-v2-3B-mlx-q8` | |
| 158 | +| q6 | 0.33× | 4.75 GB | `mlx-community/higgs-audio-v2-3B-mlx-q6` | |
| 159 | +| q4 | 0.26× | 3.32 GB | deferred — seed-sensitive, follow-up PR | |
| 160 | + |
| 161 | +bf16 is served directly from the authoritative `bosonai/*` upload — no need for a redundant mlx-community re-host. q8 and q6 are MLX-specific selectively-quantized variants. |
| 162 | + |
| 163 | +## Sampling controls |
| 164 | + |
| 165 | +- `temperature=0.7`, `top_p=0.95` are the Higgs defaults. |
| 166 | +- `ras_win_len=7`, `ras_max_repeat=2` enables repetition-avoidance sampling (catches near-tie mispicks that compound into loops). Set `ras_win_len=None` to disable. |
| 167 | +- `sampling_warmup_frames=N` uses greedy sampling for the first N frames, then switches to temperature. Exposed for experimentation; not helpful at default settings. |
| 168 | +- `fade_in_ms=5.0`, `fade_out_ms=5.0` applies a short linear fade to the decoded PCM boundaries. Below onset perception threshold on bf16/q8; masks rounding-click transients on quantized variants. |
| 169 | + |
| 170 | +## Implementation notes |
| 171 | + |
| 172 | +The generation state machine is the non-obvious piece of this port. See source at `mlx_audio/tts/models/higgs_audio/higgs_audio.py:HiggsAudioModel._generate_raw_frames`. The first audio frame is **synthetic all `audio_stream_bos_id`** (AUDIO_INIT) — not sampled from audio_logits at the `<|audio_out_bos|>` text position, because those logits were never trained for direct audio prediction. Without this, the model emits the stream-EOS token on half the codebooks at step 1 and output collapses to a stuck pitch. |
| 173 | + |
| 174 | +Codebook `i` is emitted with `i`-frame delay, so the first K frames are a progressive ramp-in (cb₀ sampled at frame 1, cb₁ at frame 2, etc.; the rest forced to BOS). On any codebook emitting EOS, a K-frame ramp-out begins — trailing codebooks forced to EOS before termination. After `revert_delay_pattern`, the first and last aligned columns are dropped (BOS-seed and EOS-seal — they decode to arbitrary codec token 1023 and produce audible clicks otherwise). |
| 175 | + |
| 176 | +## References |
| 177 | + |
| 178 | +- Original repo: <https://github.com/boson-ai/higgs-audio> |
| 179 | +- Paper / blog: <https://boson.ai/blog/higgs-audio-v2> |
| 180 | +- HF model (reference): <https://huggingface.co/bosonai/higgs-audio-v2-generation-3B-base> |
| 181 | +- HF model (MLX q8): <https://huggingface.co/mlx-community/higgs-audio-v2-3B-mlx-q8> |
| 182 | +- HF model (MLX q6): <https://huggingface.co/mlx-community/higgs-audio-v2-3B-mlx-q6> |
| 183 | +- HF codec: <https://huggingface.co/mlx-community/higgs-audio-v2-tokenizer> |
0 commit comments