Repository navigation
Conversation
Add mlx-sortformer as a diarization backend option using mlx-audio's MLX conversion of nvidia/diar_streaming_sortformer_4spk-v2.1 (mlx-community/diar_streaming_sortformer_4spk-v2.1-fp16). Pure MLX, no NeMo, no ONNX, no network after cache — works offline on Apple Silicon. - diarization/sortformer_mlx_backend.py: SortformerMLXDiarization (shared model holder) + SortformerMLXDiarizationOnline (per-session streaming via model.feed(chunk, state) -> SpeakerSegments), mirroring the NeMo backend's insert_audio_chunk/insert_silence/diarize/get_segments contract. - core.py: wire mlx-sortformer in the engine init and online_diarization_factory, mirroring the sortformer branches. - config.py: sortformer_mlx_model field; sortformer_max_speakers validation now accepts mlx-sortformer (the streaming backend applies the same cap). - parse_args.py: add mlx-sortformer to --diarization-backend choices. mlx-audio is imported inside the loader, so the backend (and the package) import cleanly without it installed.
Dedicated extra so --diarize --diarization-backend mlx-sortformer works without pulling in other MLX extras just for mlx-audio. Constrained to Apple Silicon (darwin/arm64), matching where mlx-audio runs.
Hermetic contract tests: the mlx_audio import boundary is faked, so the streaming contract (chunk accumulation -> feed at chunk_size -> segment accumulation -> max_speakers filter -> close) is verified without a model download. Also covers the lazy-import property, the config validation parity for mlx-sortformer, and the online_diarization_factory wiring.
The diarization-mlx-sortformer extra (mlx-audio) requires transformers >=5.5 while qwen3-streaming and qwen3-vllm pin transformers==4.57.6 via qwen-asr. The undeclared conflict made the universal lock unresolvable, turning the Lint-and-lockfile and Diart-compatibility checks red. Declare both extra pairs (mirroring the existing qwen3-vllm-metal row) and regenerate uv.lock. Verified locally: uv lock --check passes; the diart import smoke (uv run --isolated --locked --no-dev --extra diarization-diart, py3.12) passes with torch/torchaudio at 2.8.0.
The previous lockfile regeneration was produced on macOS; uv's --check on the CI's linux runner re-resolved against a different fork structure and reported the lock stale. Start from main's lock (the maintainer's CI-green resolution) and merge only the pyproject-driven change (mlx-audio pinned <0.5 by the new extra). Verified: uv lock --check passes for py3.12 and py3.13; the diart import smoke passes (torch/torchaudio 2.8.0).
The lockfile recorded an exclude-newer cutoff inherited from a global uv config on the machine that regenerated it. CI resolves without that cutoff, so uv lock --check always reported the lock stale on linux. Regenerate without any local config so the lock matches what CI computes. Verified: uv lock --check passes for py3.12 and py3.13 with UV_NO_CONFIG=1; the diart import smoke passes (torch/torchaudio 2.8.0).
|
Nice to have a NeMo-free option on Mac, thanks. I ran it against current main (with #461 merged) and there are a few problems in how it plugs into the pipeline:
A test going through |
|
Friendly ping on the review above. The mlx-audio range is the point that interacts with #426: if the Sortformer conversion works on mlx-audio 0.5.x, both backends can share one range and be installed together. The buffered-path detection ( |
|
@QuentinFuxa I probably don't have bandwidth to seriously review what the agents change addressing the feedback and rebase. happy to update the PR but i also don't want to irresponsibly ship it. |
|
Thanks for being upfront about that, I prefer it to a rushed update. Let's put this on hold: no pressure, and I won't ping again. I'm converting it to draft so the status is clear. Mark it ready again whenever you or someone else picks it up. For that day: the four points above still apply, and for the dependency range, #426 will most likely land with |
feat: MLX-native sortformer diarization backend (pure MLX, no NeMo)
Summary
The existing
sortformerdiarization backend requires NeMo plus a.nemomodel download, which is heavy and fails offline. This PR adds
mlx-sortformer: the same streaming Sortformer model running throughmlx-audio's MLX conversion
(
mlx-community/diar_streaming_sortformer_4spk-v2.1-fp16), entirely onApple Silicon, with no NeMo, no ONNX, and no network access after the
first cache.
The backend mirrors the NeMo backend's contract exactly
(
insert_audio_chunk/insert_silence/diarize/get_segments/close), so the pipeline's diarization loop treats it as a peer. It isselected with
--diarization-backend mlx-sortformerand wired in the sametwo places as the other backends (engine init and the per-session
factory). Speaker capping (
sortformer_max_speakers) works the same asfor the NeMo backend.
mlx-audio is an optional extra (
diarization-mlx-sortformer, AppleSilicon only) and is imported inside the model loader, so the package
imports cleanly without it.
User impact
sortformeranddiartbehave exactly as before, including thesortformer_max_speakersvalidation (now also accepting
mlx-sortformer, since the MLX backendapplies the same cap).
offline on MLX.
Validation
tests/test_diarization_mlx_backend.py, hermetic: themlx-audio import boundary is faked, no model download): the streaming
contract (chunk accumulation, feed at chunk size, segment accumulation,
max-speaker filter, close), the lazy-import property, config validation
parity, and the factory wiring. 8 tests.
test clip processed at RTF 0.024 through the ported contract, 12
segments, single speaker correctly identified end to end.
uv run pytest -q tests/ --ignore=tests/test_pipeline.py: failure anderror set identical to
main(compared run-to-run in the same checkout;the pre-existing canary, deepgram, and qwen3-shim failures are
environmental). The new tests all pass.
ruff check .: passes.Checklist
relevant. (No existing issue covers an MLX-native diarization
backend.)
discoverable through
--diarization-backend --help; a README mentioncan follow.)
ruff check ..private data.