Skip to content

tools/paradee: run Paradee-8M v1.1 natively (source-filter head) - #86

Merged
lalalune merged 1 commit into
mainfrom
paradee/v11-head
Oct 21, 2026
Merged

lalalune merged 1 commit into
mainfrom
paradee/v11-head

Conversation

@lalalune

Copy link
Copy Markdown
Member

What

This PR runs Paradee-8M v1.1 (sahilmahendrakar/Paradee-8M-v1.1 at 330617ad, Apache-2.0) natively in tools/paradee, alongside v1.0. It is the follow-up proposed in #85.

  • Converter. convert_paradee_pt_to_gguf.py --version 1.1 checks the v1.1 files against the new pins.json "v1.1" section. That section pins the HF files, vhead.py / export_paradee_v1_1.py at paradee 606e16e1, and the output GGUF. The output is byte-reproducible (--check-output passes for both versions).
  • Native head. src/paradee-head.cpp implements vhead.py SourceFilterHead2:
    • ConvNeXt blocks at 80 fps on the kokoro layers.
    • A 64-harmonic sine bank from F0.
    • Gains and phase at 80 mel-spaced points with a pulse shift, plus pulsing noise.
    • Mixed-radix STFT/iSTFT at 600/150, and a 150/75 noise-burst path.
    • The noise is seeded (xoshiro256** + Box-Muller, paradee_synth_options::noise_seed, default 0), so output is deterministic. Callers can also supply the noise (paradee_stages::noise_in).
    • The main path streams one STFT frame at a time.
  • Routing. The converter writes paradee.decoder.head (istftnet_lock / source_filter_v2) and paradee.version, and paradee_load_model picks the graph from them. A GGUF without the key is v1.0.
    • The pinned 357477… v1.0 file still loads and gives byte-identical audio.
    • libelizainference needs no change. Checked through libelizainference.dylib: paradee_load + synthesize_alloc with the v1.0 and then the v1.1 GGUF in one context.
  • kokoro. decoder_front_forward exposes the AdaIN trunk on its own (the existing weight filling, moved). Kokoro-82M audio is byte-identical before and after (kokoro-tts, af_bella).
  • Reference and gates.
    • dump_paradee_reference.py dump --version 1.1 runs the upstream module in float32 with fixed noise: torch.randn seeded per sentence, or read from --noise-dir. It writes the noise as a stage.
    • test-paradee-parity gains the v1.1 gates below.
    • test-paradee-api covers the seed, injected noise and the head API.
    • PARADEE_FIXTURE_DIR can now be set at run time, for device runs.

Parity (5 sentences; M4 Max and TB336FU arm64 both OK)

Gate Limit Measured
ids, integer durations identical identical
text side unchanged (PARADEE_GGUF_V10) bert/d/dur/F0/N/asr bit-identical between the v1.0 and v1.1 GGUFs bit-identical
bert, d, dur, F0, N, asr ≤ 1e-3 × RMS ≤ 6e-6
trunk (from the reference asr/F0/N) ≤ 1e-3 × RMS ≤ 9e-6
hn (ConvNeXt stack, from the reference trunk) ≤ 1e-3 × RMS ≤ 1e-5
sine bank (from the reference F0) ≤ 1e-4 × RMS of the float64 bank, and no further from float32 sine_bank than float32 is from float64 1.3–1.4e-5 (float32 itself: 2.6–3.1e-4)
main path, burst path, head output (reference trunk, F0 and noise) ≤ 1e-3 × RMS ≤ 9.3e-5
end to end, with the reference noise SNR ≥ 35 dB 68.5–81.9 dB on the Mac, 69.0–77.3 dB on the tablet

The sine bank is gated against float64 because torch's float32 sin(2πk·cyc) is itself up to 9e-4 × RMS off. This follows the v1.0 lock gate. The v1.0 parity test, test-paradee-api (both versions), test-paradee-phase-lock, the kokoro tests and eliza-inference-test-paradee-{asr,stream-output} all pass.

v1.1 against v1.0 (the 40 sentences from #85, 130.1 s)

v1.0 v1.1
RTF, M4 Max, 1 thread 0.025 0.0083
RTF, TB336FU (HA2JZVTQ, MT6835), 8 threads 0.263 0.091
RTF, TB336FU, 1 thread on an A76 core 0.533 0.105
WER, Phonon-2 / whisper.cpp base.en 3.90% / 3.90% 3.90% / 3.12%
Peak RssAnon, TB336FU, 19.8 s utterance (paradee-tts --repeat 3) 202 MB 69–73 MB
Peak footprint, M4 Max, same utterance 300 MB 110–115 MB
  • The v1.1 WER equals upstream's ONNX v1.1 on the same sentences. Phonon-2's transcripts of the native and ONNX v1.1 audio are identical for all 40 sentences, even though the two draw different noise.
  • For reference, upstream's ONNX single-thread figures on the M4 Max are 0.010 (v1.1) and 0.051 (v1.0). Native v1.1 is at 0.0083.
  • The tablet test binaries ran from /data/local/tmp/paradee-v11, an NDK 29 arm64 build (android-28). That directory has been removed.
  • The Mac had a load average of ~50 from other work, and the tablet ~11–14, so the absolute RTFs are approximate. Each pair was measured back to back.
  • The v1.1 head runs on one thread. Most of the 8-thread gain on the tablet is the text side's.

GGUFs

Size sha256
paradee-8m-v1.1-f32.gguf (--version 1.1) 31,275,232 B (deflate -9: 29.0 MB) f205ab055f0120d571aa6fda2deadde1e6fbc88e8d8bd56a592917fd0e50892e
paradee-8m-v1.0-f32.gguf (this converter) 32,072,864 B c0d8f2010571c01015277bc11c9901d4d4cfbbfb25145932cdd86398464281d2

The v1.0 sha changes only because the converter (its blob sha is in the metadata) and two new keys changed. The tensors are the same. The shipped 357477… file does not need to move.

What would change downstream once this merges (not done here)

elizaOS/eliza (a reviewed PR that bumps plugins/plugin-local-inference/native/llama.cpp to the merged commit):

  • packages/app/platforms/android/paradee-voice/inputs.json paradee: path tts/paradee/paradee-8m-v1_1.gguf, bytes 31275232, sha256 f205ab05…, and source text --version 1.1 from sahilmahendrakar/Paradee-8M-v1.1.
  • plugins/plugin-native-inference/src/model-catalog/voice-model-versions.json:
    • a new mobile-voice version (1.3.0) whose paradee-8m-v1.1 row has the new path, hfPath, sha and bytes;
    • source set to repo sahilmahendrakar/Paradee-8M-v1.1, revision 330617ad…, with decoder.pt 143f9678… (14,655,657 B) and config.json c3f420e8…; text_side.pt and tokenizer.json are unchanged;
    • a changelog entry, and the missingAssets filename.
  • The same sha and path in:
    • plugins/plugin-local-inference/src/host-tts-worker.ts;
    • BionicSpeechInstrumentedTest.java (MODEL_HASH);
    • ElizaBionicInferenceServer.java (PARADEE_MODEL);
    • aosp-voice-download.ts and its test (filename migration);
    • the packages/app/README.md text.

Grace (senior-care):

  • Move vendor/eliza and upstream.lock.json to that reviewed eliza commit.
  • apps/app/native/android/voice/voice.lock.json pins no Paradee hash itself; it points at eliza inputs.json. Nothing hash-bearing changes there. Only update speech if a field names the version.
  • apps/app/scripts/apk.mjs asset path …/tts/paradee/paradee-8m-v1_1.gguf.
  • voice/assets/grace-voice/NOTICE.txt (the v1.1 URL), the voice/build.gradle comment, and docs/device-voice.md, docs/prd.md and the docs/verification.md GGUF lines.
  • docs/decisions.md row 3 names Paradee-8M v1.0, so the switch is an owner decision.
  • Re-stage with stage-device-voice.mjs --build.

Not covered

AI-assisted change.

🤖 Generated with Claude Code

Paradee-8M v1.1 keeps v1.0's text side and AdaIN trunk and replaces the
iSTFTNet generator and phase lock with vhead.py SourceFilterHead2. This adds:

- converter --version 1.1, with the v1.1 HF files and source scripts pinned in
  pins.json; paradee.version / paradee.decoder.head in the GGUF
- src/paradee-head.cpp: the ConvNeXt stack on the kokoro layers, the 64-harmonic
  sine bank, pulsing and burst noise, mixed-radix STFT/iSTFT 600/150 and
  150/75, a seeded Gaussian noise source (or caller-supplied noise)
- routing in paradee_load_model from paradee.decoder.head (absent = v1.0, so
  existing v1.0 GGUFs load unchanged and give byte-identical audio)
- kokoro decoder_front_forward: the AdaIN trunk alone (Kokoro-82M output
  byte-identical)
- dump_paradee_reference.py dump --version 1.1 with fixed noise, and v1.1
  gates in test-paradee-parity (text side bit-identical to v1.0, trunk / head
  stages within 1e-3 x RMS given the reference inputs and noise, sine bank vs
  float64, end-to-end SNR >= 35 dB)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation examples labels Oct 20, 2026
@lalalune
lalalune merged commit 6003a10 into main Oct 21, 2026
2 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation examples

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant