Skip to content

tools/paradee: native Paradee-8M v1.0 TTS with phase lock and PyTorch parity gate - #64

Merged
lalalune merged 5 commits into
eliza/develop-pin-ea8b3f2from
claude/alpha-voice-paradee-native
Oct 14, 2026
Merged

lalalune merged 5 commits into
eliza/develop-pin-ea8b3f2from
claude/alpha-voice-paradee-native

Conversation

@lalalune

Copy link
Copy Markdown
Member

Adds Paradee-8M v1.0 as a native TTS in the fork, so Alpha Phone can ship Paradee instead of Kokoro-82M without ONNX. The model is sahilmahendrakar/Paradee-8M-v1.0 (Apache-2.0): 8.07M parameters, one voice (af_heart), 24 kHz, distilled from Kokoro-82M.

Upstream publishes Paradee as ONNX and as PyTorch weights. This PR uses the PyTorch weights:

  • A converter turns them into a GGUF.
  • The model's own Kokoro modules run on the existing tools/kokoro layers, which now take their widths from the tensors.
  • The phase-locking filter is reimplemented natively.

Base is eliza/develop-pin-ea8b3f2 (7efee00, the gitlink on eliza develop).

What changes

  1. tools/paradee/convert_paradee_pt_to_gguf.py and pins.json.

    • Before loading, the converter checks the size and sha256 of pytorch/text_side.pt, pytorch/decoder.pt, config.json and tokenizer.json. The pinned HF revision is c7f83f86; the v1.0 tag points at 8f34b01e, which differs only in README.md.
    • It loads with torch.load(weights_only=True) and folds weight_norm with torch._weight_norm.
    • Every state-dict key must be consumed. Only the unused ALBERT pooler is skipped.
    • The GGUF holds every width, both learned styles (32 and 16), the vocabulary and the phase-lock parameters. It also holds the input pins and the converter's git blob sha.
    • The F32 output (32 MB) is byte-reproducible: sha256 9a18ae71…3a254, enforced by --check-output.
    • F16 halves the file but fails the parity gate (end-to-end SNR 16–23 dB), so it is not pinned.
  2. tools/kokoro: widths from the tensor shapes. The predictor, decoder and generator no longer hard-code 512/128/1024/514/1090/256. predictor_forward / decoder_forward / generator_forward take a config with the student-only options:

    • the asr_proj GELU MLP
    • the trained InstanceNorm affine
    • torch.round durations
    • a separate predictor style
    • a float64 harmonic source and a canonical source STFT
    • cancellation, stage outputs
    • opt-in faster kernels: per-tap SGEMM on Accelerate, register-tiled NEON conv on the portable/Android path, and a polynomial Snake. test-kokoro-layer-options checks them against the defaults.

    Kokoro-82M is unchanged bit for bit. kokoro-tts WAVs for three sentences at speed 1.0 and 1.3, with Accelerate and with KOKORO_FORCE_GGML, and the kokoro-stage-dump outputs are byte-identical before and after. The existing Kokoro tests pass.

  3. tools/paradee: native Paradee.

    • paradee_lib, with the API the ABI work consumes:
      • paradee_load_model, paradee_sample_rate, paradee_synthesize_ids
      • paradee_phonemes_to_ids: adds the pads, rejects more than 510 symbols and any symbol outside the vocabulary
      • paradee_phonemes_to_symbol_ids: unpadded and unlimited, for chunkers
      • paradee_synthesize: struct form
    • The phase lock (paradee-phase-lock.cpp): STFT 1024/256, 2–8 kHz, voiced frames, 33-frame smoothing, radix-2 FFT. No ONNX.
    • The paradee-tts CLI.
    • LLAMA_BUILD_PARADEE (default ON, needs Kokoro) and the LLAMA_BUILD_MISAKI_G2P hook in the root and tools/ CMake. Both come before tools/omnivoice, so the fused library can fold them in.
  4. Parity gate.

    • tools/dump_paradee_reference.py runs the pinned PyTorch reference: torch 2.14.0, kokoro 0.9.4, and paradee source 9c8b4d75 with the sha256 of each training script pinned.
    • Input is the misaki 0.9.4 phonemes of samples/sentences.txt, committed as a fixture.
    • The reference is deterministic. SineGen has no random phase and no noise, as in tools/kokoro, and runs in float64. The source STFT is canonical float64.

Results (5 sentences, macOS arm64)

Check Result Gate
integer durations identical, all sentences identical
bert, d, dur, F0, N, asr max err ≤ 7e-6 × RMS 1e-3 × RMS
harmonic source (reference F0 in) ≤ 5e-7 × RMS 1e-3 × RMS
decoder output (reference asr/F0/N/source in) ≤ 3e-5 × RMS (118 dB) 1e-3 × RMS
phase lock alone vs phase_lock.lock() in float64 ≤ 4e-9 (synthetic fixture: 3e-8) 1e-4
end-to-end waveform SNR vs reference 58–76 dB ≥ 35 dB
log-mel L1 (DTW) vs published seeded samples native 0.538, reference 0.538 report only
whisper.cpp base.en WER native 1.7%, published samples 1.7% (same transcripts) report only

Notes:

  • The acoustic stages are each fed the reference's inputs. The source integrates F0 over the whole utterance, so F0's float32 rounding (~1e-6 relative) would otherwise be charged to the source and decoder.
  • The shipped float32 phase_lock.lock() differs from its own float64 evaluation by up to 1.1e-3, at bins where the source is nearly silent. The native lock is exactly that far from it and no further, so the 1e-4 gate is against float64.
  • The distance to the published samples comes entirely from the reference itself: the samples use random noise, and their durations are off by one frame. The native output adds nothing measurable on top.

Single-thread speed. M4 Max, best of several runs, with the machine heavily loaded (load average 70–80):

Path Realtime
Accelerate up to ~32x
Portable NEON (the Android path) up to ~14x
Kokoro arithmetic (PARADEE_EXACT_LAYERS=1) ~21x

The portable path was 1.8x before the tiled kernels. These figures are not a clean benchmark. Linux x86_64 was not measured: no Linux host or running Docker daemon was available.

Tests run

  • Host build: -DGGML_METAL=OFF -DLLAMA_BUILD_TESTS=ON -DPARADEE_GGUF=… -DPARADEE_REF_DIR=…. ctest -R "kokoro|paradee" passes 10/10, both with Accelerate and with the portable path (-DKOKORO_NO_ACCELERATE).
  • Android NDK r28 compile only, no device run:
    • arm64-v8a: paradee_lib, paradee-tts and the tests
    • x86_64 with LLAMA_BUILD_TOOLS=OFF: paradee_lib
  • Not run: anything on a device or emulator. This PR does not touch tools/omnivoice.

Known issues, unchanged here

  • kokoro_predictor_forward reads the predictor style at ref_s + 512, not ref_s[128:]. That is two voice rows further, and past the end of the voice pack for the longest inputs; with a 256-float ref_s file, kokoro-stage-dump output varies from run to run. Fixing it changes Kokoro's audio, so it needs its own PR and parity run. It is marked in the code.
  • tests/test-chat.cpp does not build with LLAMA_BUILD_SERVER=OFF (mtmd.h not found). This is unrelated and pre-existing.

For the ABI work (claude/alpha-voice-ffi-v17)

That branch's contract double expects paradee_phonemes_to_ids to return unpadded ids and to drop unknown symbols. This header follows the agreed contract instead: it adds the pads and rejects unknown symbols. The chunker should call paradee_phonemes_to_symbol_ids, which is unpadded, unlimited, and rejects unknown symbols. The other names the bridge uses all exist here: PARADEE_MAX_INPUT_IDS, paradee_synth_options, paradee_audio, paradee_synthesize, PARADEE_E_CANCELLED, PARADEE_E_OOM.

AI disclosure: Claude Code wrote this change for the elizaOS maintainers, as part of moving Alpha Phone's voice to llama.cpp.

🤖 Generated with Claude Code

lalalune and others added 4 commits October 13, 2026 23:08
convert_paradee_pt_to_gguf.py reads pytorch/text_side.pt and
pytorch/decoder.pt from sahilmahendrakar/Paradee-8M-v1.0 at the revision in
pins.json, after checking size and sha256 of both checkpoints, config.json
and tokenizer.json, and loads them with torch.load(weights_only=True).

- weight_norm pairs are folded with torch._weight_norm, so the GGUF weights
  equal the reference module's effective weights.
- Every state-dict key must be consumed (only the unused ALBERT pooler is
  skipped) and every width is checked against the hparams it writes.
- The GGUF carries general.architecture=paradee, all widths, both learned
  styles, the vocabulary (symbols + ids), the sample rate, the phase-lock
  parameters, the input pins and the converter's git blob sha.
- F32 by default (32 MB). The output is byte-reproducible; pins.json holds
  its sha256 and --check-output enforces it.

pins.json also pins the paradee source commit and the training scripts the
parity reference imports, the reference environment, the published samples
and the test fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The predictor, decoder and generator had Kokoro-82M's widths as literals
(hid 512 and style 128 in the predictor, 514/1090/1024/512 and style 128 in
the decoder, 512 -> 256 -> 128 in the generator, asr width 512). They now
come from the GGUF tensor shapes, so width-scaled students of Kokoro (such as
Paradee-8M) run on the same code:

- predictor_forward(ctx, PredictorConfig, ...) and
  decoder_forward(ctx, DecoderConfig, ...) take a tensor namespace and the
  student-only options: the asr_proj GELU MLP, the trained InstanceNorm
  affine inside AdaIN1d, torch.round durations, a separate predictor style,
  a double-precision harmonic source, a canonical source STFT (exact zeros
  below the DFT rounding floor), cancellation hooks and stage outputs.
- generator_forward(..., GeneratorConfig, ...) replaces the literals; the
  old entry points keep Kokoro's defaults.
- KokoroLayerOptions (opt-in, scoped): per-tap SGEMM convolutions on
  Accelerate, register-tiled NEON Conv1d/ConvTranspose1d on the portable
  path, and a polynomial Snake. test-kokoro-layer-options checks them
  against the default kernels.
- The iSTFT evaluates its basis once instead of per sample (same values,
  same sums).

Kokoro-82M is unchanged bit for bit: kokoro-tts output for three sentences
at speed 1.0 and 1.3, with Accelerate and with KOKORO_FORCE_GGML, and the
kokoro-stage-dump outputs compare equal byte for byte before and after.

The predictor still reads its style at ref_s + 512 rather than ref_s + 128
(kokoro/model.py uses ref_s[:, 128:]). That is a separate fix with its own
parity run; it is marked in kokoro_predictor_forward and left as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ty gate

paradee_lib runs Paradee-8M v1.0 on the generalised tools/kokoro layers:
ALBERT 6x256 -> duration encoder (style 32) -> integer durations ->
F0/N; TextEncoder -> asr_proj (192 -> 512 -> 512, exact GELU); AdaIN decoder
(256, style 16) -> iSTFTNet generator (128 -> 64 -> 32) with the
deterministic source; then the phase lock.

paradee-phase-lock.cpp ports training/scripts/phase_lock.py lock(): STFT
1024/256 with reflect padding, bins 2-8 kHz, voiced frames (F0 > 60 Hz),
33-frame Hann smoothing of S*conj(E)/|E|, phase = angle(E) + angle(smoothed),
magnitude kept, out = s + iSTFT(change). Radix-2 FFT in double; no ONNX.

API (include/paradee.h): paradee_load_model, paradee_sample_rate,
paradee_phonemes_to_ids (adds the pads, rejects >510 symbols and symbols
outside the vocabulary), paradee_phonemes_to_symbol_ids (no pads or limit,
for chunkers), paradee_synthesize_ids (cancel polled between stages and in
the generator and lock loops), paradee_synthesize (struct form), and the
staged/decode/lock entry points the parity gate uses. paradee-tts writes a
WAV from misaki phonemes; paradee-stage-dump writes every stage.

LLAMA_BUILD_PARADEE (default ON, needs LLAMA_BUILD_KOKORO) and the
LLAMA_BUILD_MISAKI_G2P hook are added to the root and tools CMake, both ahead
of tools/omnivoice so the fused library can fold them in.

Parity gate: tools/dump_paradee_reference.py runs the pinned PyTorch
Paradee with a deterministic source on the misaki 0.9.4 phonemes of the
model card's samples/sentences.txt (committed as
tests/fixtures/sentences.misaki.json) and dumps every stage. It also writes
the self-contained lock fixture, and reports the log-mel distance to the
published seeded samples and the whisper.cpp WER (report only).

Tests:
- test-paradee-phase-lock (always runs): the native lock is within 3e-8 of
  phase_lock.lock() in float64 (gate 1e-4).
- test-paradee-api (PARADEE_GGUF): ids, vocabulary and length errors, input
  validation, determinism, speed, cancellation.
- test-paradee-parity (PARADEE_GGUF + PARADEE_REF_DIR), all 5 sentences:
  - integer durations are identical;
  - bert/d/dur/F0/N/asr are within 7e-6 x RMS;
  - source and decoder output, each fed the reference's inputs, are within
    3e-5 x RMS (gate 1e-3);
  - the lock alone is within 4e-9 of float64 phase_lock (gate 1e-4);
  - end-to-end waveform SNR is 58-76 dB (gate 35 dB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…UF params

Review fixes on top of the native Paradee port:

- The v17 FFI bridge chunked with paradee_phonemes_to_ids, which the real
  library pads, limits to 510 symbols and makes strict on unknown symbols.
  Against the real paradee_lib every text longer than 510 phonemes, and
  every text with a symbol outside the vocab, failed with INVALID_ARG, and
  chunks carried embedded pads. Add paradee_phonemes_to_symbol_ids_lenient
  (unpadded, unlimited, unknown symbols dropped and counted, the documented
  ABI behaviour) and use it in the bridge; sync the contract header and fake.
- Speed is bounded to [0.25, 4] in the library (durations scale with
  1/speed, so a tiny speed made the decoder buffers unbounded); the FFI
  clamps to that range as its header now documents.
- The loader bounds the widths, upsample rates, sample rate and phase-lock
  parameters it reads from the GGUF.
- paradee_phase_lock checks `s` before copying from it.
- LLAMA_BUILD_PARADEE no longer fails the configure when Kokoro is off
  (metal-transfer-validation builds with LLAMA_BUILD_KOKORO=OFF); it skips.
- Tests: lenient mapping, speed bounds, concurrent synthesis on one model,
  FFI speed clamping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lalalune
lalalune force-pushed the claude/alpha-voice-paradee-native branch from 5c4bd32 to 1ef617d Compare October 14, 2026 03:39
@lalalune

Copy link
Copy Markdown
Member Author

Review fixes pushed in 1ef617d (rebased on the current base, which now has the ABI v17 bridge from #59):

  • Bridge contract mismatch. The v17 FFI chunked with paradee_phonemes_to_ids, which this library pads, caps at 510 symbols and makes strict on unknown symbols. The bridge's test double did none of that, so the fake tests passed, but against the real paradee_lib: any text over 510 phonemes, and any text with a symbol outside the vocab, failed with INVALID_ARG, and the chunks carried embedded pads. Added paradee_phonemes_to_symbol_ids_lenient (unpadded, unlimited, drops and counts unknown symbols, which is what the ABI header documents), switched the bridge to it, and synced the contract header (including the status enum values) and the fake. Checked end to end with the real libelizainference.dylib built with tools: native misaki English G2P (misaki-g2p) for Paradee-8M, with agreement gate #63's misaki_g2p merged in: a ~32 s paragraph chunks and synthesizes, and text with an emoji and #%& synthesizes instead of failing.
  • Speed bound. Durations scale with 1/speed, so a speed close to 0 made the frame count and decoder buffers unbounded. The library now accepts [0.25, 4] and the FFI clamps to that range, as the header now says.
  • The loader bounds widths, rates, sample rate and phase-lock parameters read from the GGUF.
  • paradee_phase_lock checked s for null only after copying from it.
  • Configure no longer fails when LLAMA_BUILD_KOKORO=OFF. metal-transfer-validation.yml configures that way. Paradee is now skipped in that case.
  • Tests added: lenient mapping, speed bounds, concurrent synthesis on one model, FFI speed clamp.

Re-verified on macOS arm64: ctest -R 'kokoro|paradee|eliza-inference' 12/12 pass (parity gate included). Kokoro-82M kokoro-tts/kokoro-stage-dump outputs are byte-identical to the pre-PR baseline. An ASan/UBSan run of the paradee and kokoro-layer tests is in progress.

🤖 Generated with Claude Code

@github-actions github-actions Bot added documentation Improvements or additions to documentation examples build labels Oct 14, 2026
Resolve kokoro-decoder against the stage-boundary cancel from #45: keep the
shared decoder_forward and pass the caller's cancel callback through
DecoderConfig. Keep both new kokoro tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lalalune
lalalune merged commit 3f131ab into eliza/develop-pin-ea8b3f2 Oct 14, 2026
1 of 38 checks passed
lalalune pushed a commit that referenced this pull request Oct 14, 2026
Every change on this branch already reached develop-pin (and so main) in a
newer form through #64: Paradee hardening, Kokoro stage cancel from #45.
Keep main's tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build documentation Improvements or additions to documentation examples

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant