Skip to content

Releases: loudreader/loudkit

Release list

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 10 Sep 13:02

The fingerprint moves: 79f71f5821477353 -> 7cd75498ad4e7531. Changes
to sampling, postprocessing, chunking and text preparation are audible, so the same text, voice and seed render
differently than they did on 0.1.0. That is what a moved fingerprint means and
it is the only way this project is allowed to change a reading:
the identity contract lists what
loudkit-1 now includes, and every amendment below carries the measurement
that bought it.

Both model bundles now include all 28 voices: ten English voices and two for
each of the other nine languages. Henry, Oliver, Miles, Oscar, Clara, Emma,
Lucy and Sophie load by name without a separate profile download.

Python voice-profile saving now packs strided tensors into contiguous storage
before passing them to safetensors. Cropped enrollment mels could otherwise be
silently saved with incorrect values, changing the voice after reload in both
models. Already affected profiles must be regenerated from their source audio;
loading and resaving a corrupted file cannot restore the missing values.

Why a patch release moves it: before 1.0 the patch component tracks the API
surface and the fingerprint carries the audible contract, a clause
COMPATIBILITY.md states. This entry meets it.

All five SDKs read both checkpoint formats. Turbo uses format_version 2;
version-1 checkpoints retain their existing interpretation.

Source-breaking in Swift: TokenGenerator.generate now throws
(LoudKitError.cancelled at the poll that fires), so code that reaches the
generator through Engine.tokenGenerator must try. No compatibility shim.

Source-breaking for manually constructed voice profiles: Rust voice::Profile
now stores enrolment; add enrolment: "first-10s".into() to a struct literal
(or preserve the actual method if known). Go callers using positional voice.Profile
literals must add the new Enrolment field; keyed literals can omit it. Loading
and enrolling profiles use the same API and now preserve the enrollment method
when saved across all five SDKs.

Added

  • mp3 and opus from every door: /v1/synthesize, /v1/audio/speech,
    gRPC and MCP. Both come from the codecs of the libsndfile that soundfile
    bundles, so neither adds a package. Hermes Agent and OpenClaw ask an
    OpenAI-compatible server for mp3, or for Opus when the reply is a voice note,
    and now get it from loudkit serve. aac stays refused: libsndfile cannot
    write it.

  • The long-form conformance case is now driven through engine.stream and
    engine.synthesize as well as chunk by chunk, so Python consumes the flat
    long_form.cases[].tokens list the four ports consume; the parity page's
    seven weighted rows are compared by a slow test that regenerates with
    --checkpoint (the parity job runs it); enrollment from a WAV file is held
    to the enrollment fixture; lk.enroll and loudkit speak on a turbo release
    directory have goldens; Result.save() is pinned to the shared quantise
    rule; the production fingerprint is pinned weight-free; and the Swift
    offline-receipt test asserts the stderr line the other four ports assert.

  • loudkit text previews what will be spoken without loading a model: the
    prepared text on stdout, a word diff of what the funnel changed on stderr.
    loudkit --help lists eight commands.

  • loudkit speak --play plays the WAV it just saved through the system
    player (afplay on macOS, aplay or paplay on Linux).

  • Speed measured on 0.1.1 for both models: eight local paths on an Apple M3
    Pro (PyTorch split CPU/MPS, native CoreML, ONNX Runtime CPU, the CPU
    reference, and the Swift, Rust, Go and TypeScript ports) and six NVIDIA
    parts (A100, L4, RTX 3090, T4, GTX 1080 Ti, Jetson Orin Nano), eager and
    with CUDA graphs, plus batched throughput on four of them. loudr-1-turbo
    runs 1.4x to 2.1x faster than loudr-1 end to end and 2.6x to 2.8x faster in
    aggregate throughput (Benchmarks).

  • A second model, loudreader/loudr-1-turbo, behind the same one line.
    lk.load("loudreader/loudr-1-turbo") is the whole difference: same voices,
    same API, same seeds. It is the fusion_mtp2 generator below beside a
    renderer distilled to one Euler step, published as a second repository with
    the same layout. Python, Swift, Go, Rust and TypeScript support both models,
    using the matching ONNX graphs or CoreML packages. One enrolled voice
    profile works with both models.
    Choosing a model says which is which.

  • A second decode mode, fusion_mtp2, two speech tokens per transformer
    forward: one head reads the hidden state, a second reads that state beside
    the first token's embedding, and a fusion MLP folds both into the one KV
    slot the pair occupies. Checkpoints that carry it declare decode.mode in
    their manifest and ship as format_version 2. The mode is a property of
    the weights, not a runtime option: it is fingerprinted, and a mode/weights
    disagreement is a load error. Measured with CUDA graphs against 0.1.0 on
    the same lineage, RTF on the short / medium / long benchmark samples:

    0.1.0 0.1.1
    RTX 3090 1.22 / 6.03 / 7.73 1.43 / 7.47 / 8.45
    Jetson Orin Nano 0.53 / 1.60 / 1.84 0.63 / 1.82 / 2.16

    Beside it, none fingerprinted: sampling runs on the device inside the
    captured graph, graphs are kept across calls, the excitation noise is drawn
    on the GPU, and the vocoder no longer vocodes padding it throws away
    (ExecutionConfig.vocoder_ragged, on by default on CUDA renderers, in the
    identity contract's equivalent class; --no-vocoder-ragged for byte
    agreement with a 0.1.0 build). End-to-end figures: docs/benchmarks.md.

  • New release bundles use a 20 ms raised-cosine ramp on both window edges
    to soften onset taps. The tap is a hot first mel frame from the flow decoder
    on three of the 28 voices, not a vocoder edge. Measured on oscar, the hottest:
    the residual burst in the first 12 ms of a window falls from 2.2 dB below the
    window RMS with the old 5 ms ramp to 14.7 dB below with 20 ms, at a cost of
    0.7 dB over the first 50 ms of a window that opens on speech; 50 ms would
    cost 3 dB there. edge_fade_seconds is part of the algorithm and its
    fingerprint, including the new 0.02 s default. Only the historical 0.005 s
    spelling stays omitted for identity compatibility.

  • loudkit clone ends the reference prompt in silence by default: the clip
    is cut at its last pause before the ten-second limit, or at 9.6 s when no
    pause fits, and padded with 0.4 s of silence, because the model speaks from
    where the prompt ends. On one cloned voice the onset moved from 0 to 50 ms
    and the first 60 ms from -32 to -51 dBFS. --no-end-in-silence keeps the
    recording as given, which is what the library's enroll does unless asked.
    Profiles made with the cut carry the enrolment label first-10s-pause.

  • Every download directory carries a receipt, .loudkit-release.json:
    the repo, the revision asked for, the commit it resolved to, the digest of
    SHA256SUMS and when it was fetched. A load whose revision still resolves
    to that commit hashes nothing; any other receipt, or none, fetches and
    verifies again, keeping every file that already hashes to the new manifest;
    a load that cannot reach the hub uses the receipt and says so on stderr.
    The same rules in Python (loudkit download --local-dir), Go, Rust, JS
    and Swift, pinned by one fixture. One hub call per load; a hub answer
    naming no commit is an error in all five.

  • The JS binding no longer needs Python. download(repo, dir) fetches
    the ONNX release over fetch with no new dependency, on the same plan and
    rules as loudkit download --for onnx; examples/hello.mjs is the
    quickstart. Enroller.enroll takes a WAV path or bytes as well as samples.

  • Go: safetensors.Write, voice.Profile.Save, engine.Result.SaveWav.
    Rust: safetensors::write, voice::Profile::save, hub::rejected_name,
    hub::parse_sums, hub::is_official, Bundle::can_enroll,
    execution::locate_runtime. JS: saveVoice, profileFrom,
    writeSafetensors, hub.rejectedName, hub.parseSums, hub.canEnroll,
    Enroller.close. Swift: Safetensors.write, VoiceProfile.save,
    Engine.voiceNames, Engine.voice(named:), Engine.enroll(contentsOf:).

  • export.json beside every exported graph set, ONNX and CoreML, naming
    what each file was traced from. The backends refuse a set whose members
    disagree with each other or with the checkpoint; a release requires one.

Changed

  • Turbo CoreML uses an fp32 estimator: waveform correlation with ONNX now
    exceeds 0.999 on both conformance cases. The stricter gate is shared.

  • Python downloads the native generator and fusion graphs required by the
    chosen backend. Legacy renderer-only CoreML bundles remain readable.
    Complete CoreML bundles run without PyTorch; explicit fp16 token generation
    retains the faster PyTorch/CoreML path. See the measured tradeoff in
    benchmarks.

  • Go, Rust, JS, Swift: Enroll / enroll_wav / enroll on an engine loaded
    by repo id fetches the three enrollment graphs (Swift: packages) into the
    engine's own cache directory the first time, through the same verified
    download, and reads them from there afterwards. An engine loaded from a
    directory of your own still says how to fetch them (Cloning: true /
    cloning: true / { cloning: true }).

  • Go, Rust, JS, Swift: one cache layout, <user cache>/loudkit/<org>--<name>
    (~/Library/Caches on macOS, $XDG_CACHE_HOME or...

Read more

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 20:44