Skip to content

Rebase pc/add-ace on upstream main#739

Closed
shreyaskarnik wants to merge 173 commits into
Blaizzy:pc/add-acefrom
shreyaskarnik:pc/add-ace
Closed

Rebase pc/add-ace on upstream main#739
shreyaskarnik wants to merge 173 commits into
Blaizzy:pc/add-acefrom
shreyaskarnik:pc/add-ace

Conversation

@shreyaskarnik

Copy link
Copy Markdown
Contributor

Summary

  • Rebased pc/add-ace on current main (v0.4.3) to resolve merge conflicts
  • Resolved conflicts in mlx_audio/tts/utils.py (new model entries from both sides merged)
  • Resolved conflicts in mlx_audio/tts/generate.py (imports + argparse args merged)
  • Dropped redundant commit (generate.py fix already upstream)

Ref: #499

Context

Per Prince's comment on #499 — driving the rebase so the branch is mergeable.

lyonsno and others added 30 commits April 5, 2026 00:23
No need to continue generating for clients that have hung up.
This reduces latency processing the next request.

Co-authored-by: Prince Canuma <prince.gdt@gmail.com>
…ons (Blaizzy#624)

Quantized Kokoro checkpoints (8bit/6bit/4bit) store conv-like weights in
MLX layout, while bf16 checkpoints use PyTorch layout. The sanitize()
functions were unconditionally transposing these weights, breaking
quantized models with shape mismatches.

- Detect packed quantized checkpoints via .scales/.biases key suffixes
- Skip 3D conv transposition for already-converted quantized weights
- Guard duration path with nan_to_num and cap max frames per phoneme
- Return silence instead of crashing on empty concatenation

Fixes Blaizzy#623

Co-authored-by: Prince Canuma <prince.gdt@gmail.com>
…1 dB

The parity test was permanently skipped, so several load-bearing bugs in
the MLX port went undetected. Wiring up the reference inference surfaced
them; this commit fixes them and verifies numerical parity.

Config / conversion
- Add zfturbo_vocals_v1 preset (dim=192, depth=8, hop=512,
  mask_estimator_depth=1) for ZFTurbo's v1.0.0 MIT-licensed vocals
  release asset. None of the existing three presets matched it.

Port bugs fixed in model.py
- RoPE convention: MLX used the halved split (x[:half] / x[half:]);
  rotary_embedding_torch (what ZFTurbo imports) uses interleaved pairs
  (x[2i] / x[2i+1]) with a doubled-frequency layout. Same math family,
  different rotation plane — attention output was uncorrelated with
  the reference until this was fixed. Single biggest jump in SDR.
- RMSNorm eps: mlx.nn.RMSNorm uses additive eps=1e-5; ZFTurbo uses
  F.normalize(x, dim=-1) which is effectively max(||x||, 1e-12). The
  two agree for typical magnitudes but diverge by up to 70% at the
  small-magnitude high-frequency STFT bins that the band-split's
  per-band RMSNorm ingests. Replaced with a local RMSNorm matching
  F.normalize semantics.
- to_gates: declared bias=False, but ZFTurbo's Linear(dim, heads) has
  the default bias=True and ships a to_gates.bias tensor.
- MaskEstimator: hardcoded to 3 linears (mask_estimator_depth=2 in
  effect). Now honors config.mask_estimator_depth, so depth-1
  checkpoints (like this one) load with the correct number of linears.
- MelFilterbank: custom Slaney triangular filter did not quite match
  librosa's. Swapped for librosa.filters.mel + ZFTurbo's explicit
  fb[0,0]=1 / fb[-1,-1]=1 force-assigns so every freq bin is covered.
- STFT: zero-padded the audio before framing, but torch.stft(center=True)
  pad_mode defaults to reflect. Built reflect pad via slice+concat
  (mx.pad has no reflect mode).
- STFT post-processing: .real() / .imag() as method calls; these are
  properties on mlx.core.array.
- BandSplit: freq_indices stored as np.ndarray, but MLX fancy-indexing
  rejects raw numpy — convert once at MelFilterbank init.

sanitize() remaps added
- Drop rotary_embed.freqs — MLX computes RoPE freqs on the fly.
- Mask-estimator per-band Sequential -> list index: PyTorch stores
  linears at positions {0, 2} (depth=1) or {0, 2, 4} (depth=2); MLX
  list positions are consecutive {0, 1, 2}.
- to_out.0.weight -> to_out.weight — PyTorch wraps in Sequential(Linear,
  Dropout), MLX uses a bare Linear.
- .gamma -> .weight — PyTorch RMSNorm names its scale "gamma";
  mlx.nn.RMSNorm names it "weight".

Test wiring in tests/sts/test_mel_roformer_parity.py
- Replace the pytest.skip stub in test_sdr_parity with the real
  single-chunk SDR comparison: dynamically imports a user-supplied
  torch_infer.py (see PARITY_TESTING.md) via importlib, runs both
  models on the same chunk of audio, asserts SDR > target (default
  40 dB).
- Add fixtures for MEL_ROFORMER_TORCH_INFER, MEL_ROFORMER_TORCH_CONFIG,
  MEL_ROFORMER_CHUNK_SAMPLES.

Verified
- All 25 existing unit tests in tests/sts/test_mel_roformer.py still pass.
- test_qkv_split_preserves_weights: pass.
- test_sdr_parity on the v1.0.0 ZFTurbo MIT vocals ckpt +
  ZFTurbo config + a deterministic synthetic 30s stereo signal:
  SDR = 58.11 dB (target > 40 dB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Register MelRoFormer, MelRoFormerConfig, and MelRoFormerResult in the
parent package so users can import via mlx_audio.sts.models without
reaching into the subpackage.

Rename our result dataclass from SeparationResult to MelRoFormerResult
to avoid a name collision with sam_audio.SeparationResult, which is
already upstream and has a different streaming-oriented shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-commit auto-fixes from running hooks before PR submission.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…el_type

Two related STT fixes:

1. granite_speech sanitize(): The Conv1d weight transposition was applied
   unconditionally to all non-quantized models. Models with weights already
   saved in MLX-native layout (out, kernel, in) — such as bf16 safetensors
   converted directly from HuggingFace — were being incorrectly transposed,
   producing garbled or empty transcriptions. Fix: only transpose when the
   weight is actually in PyTorch layout (out, in, kernel), detected by
   checking shape[-1] > shape[-2] (true for depthwise convs where kernel > 1).

2. parakeet ModelConfig.from_dict(): NeMo-format config.json files do not
   include a top-level model_type field. Downstream tools that rely on this
   field (e.g. omlx model discovery) would misidentify the model as an LLM,
   causing a KeyError at inference time. Fix: inject model_type: 'parakeet'
   in from_dict() when the field is absent.
…point

Adds an end-to-end streaming ingestion path for Voxtral Realtime so
audio can be consumed incrementally and transcription deltas emitted
as soon as the encoder is caught up, without changing the batch
generate() path. Exposes it over a new OpenAI Realtime-compatible
WebSocket endpoint on the built-in server, and closes the burst-mode
perf gap with voxmlx.

Streaming core (mlx_audio/stt/models/voxtral_realtime/streaming.py):
- StreamingAudioSource: thread-safe queue bridging any producer
  (thread or asyncio task) to the MLX consumer, with coalescing,
  close signal, blocking read, and dtype coercion.
- StreamingMel / StreamingCausalConv1d / StreamingConvStem: per-chunk
  equivalents of the batch mel + conv stem, bit-exact for mel and
  the conv stem via explicit edge state. StreamingConvStem casts mel
  to the conv weight dtype so bf16 weights don't promote the whole
  graph to fp32.
- StreamingEncoder: transformer layers with RotatingKVCache per
  layer and a running RoPE position; forces an explicit array mask
  so SDPA stays correct when Q_len < K_len.
- StreamingDownsampler: buffers encoder output to emit adapter
  frames in downsample_factor-aligned groups.
- VoxtralStreamingSession: stateful session where feed() is a cheap
  thread-safe push and step(max_decode_tokens=N) runs a bounded unit
  of MLX work. Splitting feed and step lets callers release the MLX
  executor between steps so multiple streams can interleave on a
  single-worker executor.

Model surface (voxtral_realtime.py):
- create_streaming_session(...) -> VoxtralStreamingSession
- generate_streaming(source, ...) as a thin convenience wrapper

Perf (c2c0f0c): port decoder/encoder to mx.fast.rope + RotatingKVCache,
quantize tok_embeddings, normalize checkpoint dtype, vectorize mel
window extraction, and cast mel -> conv weight dtype at the conv
stem. Burst-mode wall is within 1% of voxmlx on 10s audio
(3.594s vs 3.559s).

Config cleanup: SAMPLE_RATE / FRAME_RATE / HOP_LENGTH /
RAW_AUDIO_LENGTH_PER_TOK / AUDIO_LENGTH_PER_TOK and the
_num_audio_tokens / _num_delay_tokens helpers move to config.py so
streaming.py imports them at module scope. Expanded session
docstring documents transcription_delay_ms (incl. the 2400 ms preset
that matches offline-mode WER) and clarifies that max_decode_tokens
is a cooperative yield granularity, not a speedup.

Server (mlx_audio/server.py):
- New /v1/realtime WebSocket speaking a subset of the OpenAI
  Realtime protocol: session.created / session.update(d),
  input_audio_buffer.append (base64 PCM16), and
  input_audio_buffer.commit {final: true} -> streamed
  response.audio_transcript.delta plus a final
  response.audio_transcript.done.
- Model picked via ?model=<repo> (default:
  $MLX_AUDIO_REALTIME_MODEL or
  mlx-community/Voxtral-Mini-4B-Realtime-2602-4bit) and reused via
  the existing ModelProvider cache.
- Thin wrapper over create_streaming_session; session.step runs in
  asyncio.to_thread so the loop keeps receiving audio while MLX
  decodes. Drains between appends for low-latency deltas and fully
  flushes on final commit. After done we start a fresh session so
  the same socket handles multiple utterances.
- Existing VAD-based /v1/audio/transcriptions/realtime is left in
  place; the new endpoint is purely additive.
- Weight loading: support Mistral-native repos and improve default
  model resolution.

Docs: new Live Input Streaming section in
docs/models/stt/voxtral-realtime.md with StreamingAudioSource +
generate_streaming() example and a lower-level
create_streaming_session() example for async servers and
multi-stream executors.

Tests (mlx_audio/stt/tests/test_voxtral_realtime_streaming.py):
- StreamingAudioSource threading behavior (coalescing, close signal,
  blocking read, dtype coercion).
- StreamingMel parity vs compute_mel_spectrogram across chunk sizes.
- StreamingCausalConv1d parity on a freshly-initialized CausalConv1d
  (no model weights required).
- Encoder + session parity behind a weight-gated TestCase that skips
  when VOXTRAL_REALTIME_MODEL is not available.
Port of Boson AI's Higgs Audio v2 to MLX. Llama-3.2-3B backbone with a
dual-FFN decoder layer (text + audio paths share self-attention; LN + MLP
are per-path, routed by audio_out_mask) and delay-pattern audio emission
(codebook i lags by i frames).

Framework interface
-------------------
Exposes `Model` and `ModelConfig` in the shape expected by
`mlx_audio.tts.utils.load()` and `python -m mlx_audio.tts.generate`:

    python -m mlx_audio.tts.generate \
        --model mlx-community/higgs-audio-v2-3B-mlx-q8 \
        --text "Hello" --ref_audio voice.wav --ref_text "..."

`Model` subclasses the underlying `HiggsAudioModel` so safetensors keys
land unchanged. Tokenizer + codec attach via `post_load_hook`. The
generate() signature matches the standard TTS convention (text, voice,
ref_audio, ref_text, ...) and yields a mlx_audio.tts.models.base
GenerationResult.

`HiggsAudioServer` is kept as an additional Python entrypoint for
Higgs-specific kwargs (max_new_frames, ras_win_len, fade_in_ms, etc.).

Load-bearing details
--------------------
The first generated audio frame must be a synthetic all-stream_bos
frame — sampling from the model's audio_logits at the <|audio_out_bos|>
text position collapses to stream-EOS on half the codebooks (those
positions were never trained for direct audio prediction). See
Model.generate and HiggsAudioModel._generate_raw_frames for the full
AUDIO_INIT + K-frame ramp-in + EOS ramp-out state machine.

Quantization
------------
MLX native 4/6/8-bit on the Llama backbone. `Model.model_quant_predicate`
protects `audio_codebook_embeddings` and `audio_decoder_proj.audio_lm_head`
at bf16 — quantizing them introduces voice-character drift (pitch
register shifts at q6, trajectory instability at q4). RTF on M5 Max:
bf16 0.60× / q8 0.36× / q6 0.33×.

Bundled assets
--------------
Three drop-in reference voices in examples/voice_prompts/ (en_woman,
en_man, en_man_deep), generated via smart-voice mode so they're
license-clean. Example: examples/higgs_audio_clone_demo.py.

Tests
-----
16 unit tests in mlx_audio/tts/tests/test_higgs_audio.py — delay-pattern
round-trip, audio-embedding lookup, sampling equivalence at T=0, tiny
config model forward, selective-quant predicate verification, and the
framework-interface contract (Model subclass, ModelConfig.from_dict,
sample_rate, model_quant_predicate, generate-before-load guard).

References
----------
Original: https://github.com/boson-ai/higgs-audio
HF (bf16): https://huggingface.co/bosonai/higgs-audio-v2-generation-3B-base
HF (q8):   https://huggingface.co/mlx-community/higgs-audio-v2-3B-mlx-q8
HF (q6):   https://huggingface.co/mlx-community/higgs-audio-v2-3B-mlx-q6
HF codec:  https://huggingface.co/mlx-community/higgs-audio-v2-tokenizer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per @lucasnewman review comment on PR Blaizzy#654 — the final PR should not
contain scratchpad / planning .md files. PARITY_TESTING.md was an
internal M5 Pro setup guide used during parity verification; the
content has no use for downstream pip-install users of mlx-audio.

The parity test itself (`tests/sts/test_mel_roformer_parity.py`) remains
and is self-documenting via its docstrings and fixture parameters. Users
who want to run parity can follow the @pytest.mark.requires_torch flag
and the env-var fixtures directly.

README.md is unchanged — it's user-facing architecture + license +
usage documentation, not a planning doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed the workflow that checks for documentation updates on user-facing changes.

Signed-off-by: Prince Canuma <prince.gdt@gmail.com>
Matches the pre-commit config (.pre-commit-config.yaml — black 24.2.0,
isort 5.13.2 profile=black) enforced by the PR CI style job. No code
changes — pure formatting + import-order + uv.lock reconciliation with
the pydub dep already declared in pyproject.toml.
…-sanitize-and-parakeet-model-type

fix(stt): correct granite_speech Conv1d weight sanitization and add parakeet model_type
Addresses @lucasnewman's review request. Covers usage (CLI + standard
Python API + Higgs-specific HiggsAudioServer), voice cloning, parameter
table, available models / quantizations / RTF / memory, conversion,
architecture, and license. Links to the full port docs at
docs/models/tts/higgs_audio.md for depth.
…inference

fix(cohere): restore quantized inference for 8-bit and 4-bit checkpoints
…DACVAE decode

- Support Irodori-TTS v2 (latent_dim=32, Semantic-DACVAE) alongside v1
- Add VoiceDesign caption conditioning (use_caption_condition=True)
- Accept instruct kwarg as alias for caption in model.generate()
- Use chunked DACVAE decoding (chunk_size=50) to stay within 16GB memory limits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tian-sweetaylor and others added 27 commits May 20, 2026 15:02
Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
style: run pre-commit formatting

Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
Signed-off-by: tian-sweetaylor <153503632+tian-sweetaylor@users.noreply.github.com>
…ad-for-speech

Pre-flight model load on streaming audio routes
- Added methods for encoding audio to latent representations and normalizing audio inputs.
- Implemented instruction generation based on task types, improving flexibility for various audio tasks.
- Introduced new task type constants and instruction templates in the configuration file.
- Updated imports and module exports to include new constants and configurations.

These changes enhance the model's capabilities for audio processing and task management.
- Introduced a new section in the README for music generation, detailing the ACE-Step model for text-to-music tasks.
- Added CLI and Python API examples for generating music from text descriptions and enhancing existing audio tracks.
- Updated the `generate.py` script to accept additional generation parameters via a JSON string.
- Enhanced the ACE-Step model loading logic to check for weights in both turbo and root directories.

These changes expand the functionality of the MLX-Audio framework to include music generation features.
- Converter: export all weight prefixes (tokenizer, detokenizer,
  null_condition_emb) not just decoder/encoder
- Converter: keep decoder. prefix instead of renaming to dit.
- Converter: use underscore naming for decoder conv params
  (proj_in_weight) to match DiTModel bare parameters
- Sanitize: add dit. -> decoder. mapping for legacy weights
- Sanitize: scope conv param renaming to decoder only to avoid
  breaking detokenizer.proj_out (nn.Linear)
- Add "ace" to MODEL_REMAPPING for HF repo name resolution
The turbo model requires 5Hz LM hints to generate music.
Without LM-generated audio codes guiding the DiT, the
diffusion simply denoises back to silence. Previously,
LM hints were only generated for cover tasks.
The turbo model produces silence without LM hints, so
enable them by default for a working out-of-the-box experience.
Add explicit language section to the LM prompt template so the
5Hz planner model better respects the vocal_language parameter.
The turbo HF model uses Qwen3 word embeddings for lyrics (not
BPE phoneme tokens), which limits vocal synthesis to the model's
learned capabilities.
8 steps produces good instrumentals but vocals need more
diffusion steps to resolve clearly. 20 steps still runs
well under real-time (~8s diffusion for 30s of audio).
- Cache sliding window mask in DiT instead of recreating per-layer (240x)
- Pass None instead of all-zeros no-op self-attention mask
- Remove dead: test_model(), omega_scale param, silence_latent param,
  unused nested_weights, TASK_TYPES_TURBO/BASE, VAEConfig, TextEncoderConfig
- Remove redundant hidden_size attrs stored but never read (3 classes)
- Fix fragile .replace(".pt", ".npy") path handling
- Strip narrating comments that restate what code already expresses
22 tests covering: config, KVCache, attention masks, RMSNorm,
TimestepEmbedding, RotaryEmbedding, Attention (self + cross with
cache), MLP, DiTLayer, DiTModel (forward, cache, conv_transpose),
and weight sanitization (dit prefix remap, proj_in scoping,
scale_shift_table reshape).

Tests caught a stale cross_attn_mask variable left from the
simplify pass — fixed to None.
- Document use_lm=True default (required for turbo model)
- Document num_steps=20 default (needed for clear vocals)
- Add "How It Works" section explaining the LM planner + DiT pipeline
- Add Hugging Face repo links (fp32 and 4-bit quantized)
- Update performance table with actual M4 Max measurements
- Document LM size trade-offs (0.6B vs 4B)
- List known limitations (language detection, seed variance)
@shreyaskarnik shreyaskarnik mentioned this pull request May 22, 2026
@shreyaskarnik

Copy link
Copy Markdown
Contributor Author

Closing — rebased branch carries too much unrelated diff from main. Will open a focused PR instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.