{"slug": "narrate-complete-books-using-qwen3-tts-natural-sounding", "title": "Narrate Complete Books Using Qwen3 TTS, Natural Sounding", "summary": "A new open-source Rust project, tts-rs, delivers local text-to-speech on Apple silicon with three voice-cloning engines ported from PyTorch, achieving real-time factors as low as 0.252 for the Qwen3TTS engine on an M4/16 GB Mac. The project's benchmark shows a 1612-word chapter narrated in 2 minutes 40 seconds (11:36 audio) using Qwen3TTS, versus 5m 47s for Audio8 and 8m 15s for CosyVoice, with all engines running without Python at runtime. The author, drmhse, highlights that a 16-hour book would take about 4 hours of compute on a laptop, and notes a known but undiagnosed performance regression in Audio8's codec and CosyVoice's vocoder.", "body_md": "Local text-to-speech in Rust. Text in, speech out, one process, no Python at runtime — three voice-cloning speech models ported from PyTorch and validated stage by stage against fp32 activations dumped from their references.\n\n**Requirements: macOS on Apple silicon.** The custom kernels are Metal, and every number here\nwas measured on an M4 / 16 GB. It builds and runs correctly elsewhere with\n`--no-default-features`\n\n(CPU fallbacks, unit-tested), but that path is a portability\nguarantee rather than a deployment target — Audio8 measures RTF 2.151 on CPU against 0.554 on\nMetal, and it is the codec that suffers most (1.235 of it, against 0.214 on Metal).\n\n```\n./scripts/bootstrap.sh          # toolchain, all three checkpoints, assets, build\n\ncargo run -p tts-cli --release -- speak \\\n    --engine audio8 --voice voices/cosy-default \\\n    --text \"Hello from a fresh checkout.\" --out hello.wav\n```\n\nOne command, nothing manual. `bootstrap.sh`\n\ndownloads and converts the checkpoints, fetches\nthe fixtures for every gate, and builds. **Name the engines you want** — all three is ~13 GB,\none is ~4 GB:\n\n```\n./scripts/bootstrap.sh --list              # the ids, their models, what each costs\n./scripts/bootstrap.sh qwen3tts            # just one\n./scripts/bootstrap.sh audio8 cosyvoice    # two\n```\n\nEvery step is skipped if its output already exists, so re-running is cheap. Details in\n** docs/reference.md**.\n\nClone a voice and speak |\na 10-second reference clip becomes a tracked voice asset; no encoder at runtime |\nServe an HTTP API |\n`tts-serve` — one engine loaded once, 3.0 s start, per-request voice and seed, cost headers on every response |\nUse it as a library |\none `Engine` trait, engines chosen by string id at request time |\nNarrate a whole book |\nmarkdown in, delivery audio plus word-level timings out, resumable per stage |\n\nAll three narrating the same 1612-word chapter (`examples/chapter.txt`\n\n, in the repo), each in\nthe configuration it ships in, in two cloned voices, on one M4 / 16 GB laptop with no Python\nrunning. GitHub cannot embed audio in markdown — the\n[demo page](https://drmhse.github.io/tts-rs/) plays all six in place:\n\n| engine | RTF | wall time | audio produced | reach for it when |\n|---|---|---|---|---|\n`audio8` |\n0.527–0.536 | 5m 47s | 11:34 / 10:59 | you want 44.1 kHz — the highest-fidelity output here |\n`cosyvoice` |\n0.703–0.718 | 8m 15s | 12:48 / 11:44 | you want the widest language coverage |\n`qwen3tts` |\n0.252–0.261 |\n2m 40s |\n11:36 / 10:34 | you are narrating something long |\n\n**That bottom row is the point of the project: a chapter becomes 11 minutes of speech in 3\nminutes, on a laptop.** A 16-hour book is about 4 hours of compute rather than 12.\n\nCompare the wall-time column, not just RTF. The three do not produce the same duration from\nthe same text — `cosyvoice`\n\nspeaks slowest, 12:48 against `audio8`\n\n's 11:34 — and since RTF\ndivides by audio produced, a slower-speaking engine flatters its own RTF.\n\n```\ncargo run -p tts-cli --release -- speak --engine qwen3tts \\\n    --voice voices/cosy-default-qwen3tts --quant f16 \\\n    --text-file examples/chapter.txt --out chapter.wav\n```\n\n`qwen3tts`\n\ngets there by batching across sections, which needs `--quant f16`\n\nand needs length —\non a 7-segment passage it is the *slowest* of the three at 0.665. It also supports ten languages\nonly (en, de, es, zh, ja, fr, ko, ru, it, pt). The other two do not batch meaningfully and are\nsteady at any length; `audio8`\n\nis **2.36× its PyTorch reference** like-for-like, that reference\nrunning on MPS too.\n\nShort-passage figures, for comparison — `examples/senior.txt`\n\n, 132 words, median of five with\nthe engines interleaved: `audio8`\n\n0.554, `cosyvoice`\n\n0.726, `qwen3tts`\n\n0.665.\n\n**Ten languages on**, a closed list: en, de, es, zh, ja, fr, ko, ru, it, pt. Text outside it has no faithful path through that engine.`qwen3tts`\n\n**A known performance regression, undiagnosed.**`audio8`\n\n's codec and`cosyvoice`\n\n's vocoder are 35% and 32% slower than when first measured, while every transformer stage is unchanged. Both are convolution-heavy; the cause is likely the channels-last conv path.**Sampled output is not reproducible across implementations**, because the reference draws from torch's RNG. Pass a`seed`\n\nfor repeatability within this port; use the greedy path if you need to compare against PyTorch.\n\nOne comparison worth not making: `cosyvoice`\n\nlooks 6× faster than its PyTorch reference, but\nonly because upstream hardcodes `cuda if available else cpu`\n\nand has no MPS path, so CPU is all\nit can do. Against a service that does use MPS it is ahead by about 5%.\n\n```\nTTS_API_KEY=secret cargo run -p tts-serve --release -- --port 3003\n\ncurl -X POST localhost:3003/tts -H \"X-API-Key: secret\" \\\n     -H 'content-type: application/json' \\\n     -d '{\"text\":\"Hello from Rust.\",\"voice\":\"voices/cosy-default-male\",\"seed\":7}' \\\n     -o out.wav -D headers.txt\n```\n\n| route | |\n|---|---|\n`POST /tts` |\nWAV body, PCM s16le mono |\n`POST /tts/stream` |\nsame, buffered rather than incremental |\n`GET /v1/capabilities` |\nengines, sample rates, and the weight formats each supports |\n`GET /health` |\nliveness |\n`GET /` |\nlists the live routes and the unimplemented ones, which answer `501` |\n\n**Every response carries its own cost.** `x-audio-seconds`\n\n, `x-wall-seconds`\n\n, `x-rtf`\n\n, and\n`x-stages`\n\nwith the per-stage split (`llm=10.296,flow=25.129,vocoder=2.898`\n\n), so a client sees\nwhere the time went without a second request. ** voice and seed are per request** — the\nfirst selects a voice asset without a restart, the second makes a render reproducible.\n\n```\nscripts/narrate-book.sh --book path/to/document --out narration --engine qwen3tts\nscripts/verify-narration.py narration/*.webm\n```\n\nOne engine load for the whole run, resumable per *stage* (a section with a WAV master is never\nre-synthesised), deterministic under a seed. A 16-hour document is about **4 hours** of\nsynthesis at `qwen3tts`\n\n's 0.260, against ~12 at `cosyvoice`\n\n's 0.726, plus an hour of\nrecognition either way.\n\n``` js\nuse tts_core::{EngineConfig, SynthesisRequest, Voice};\n\nlet config = EngineConfig::new(tts_engines::default_root(\"cosyvoice\"));\nlet engine = tts_engines::load(\"cosyvoice\", &config)?;\n\nlet voice = Voice::load(\"voices/cosy-default-cosyvoice\")?;\nlet request = SynthesisRequest::new(\"Hello from Rust.\").with_voice(voice);\nengine.validate(&request)?;                 // rejects a mismatched asset up front\n\nlet out = engine.synthesize(&request)?;\ntts_core::wav::write(\"hello.wav\", &out.audio)?;\nprintln!(\"RTF {:.3}\", out.stats.rtf(out.audio.seconds()));\n```\n\nEverything else is one file: ** docs/reference.md**.\n\n|\n\n[Architecture](/drmhse/tts-rs/blob/main/docs/reference.md#architecture)[Validation](/drmhse/tts-rs/blob/main/docs/reference.md#validation)[Performance](/drmhse/tts-rs/blob/main/docs/reference.md#performance)[Porting traps](/drmhse/tts-rs/blob/main/docs/reference.md#porting-traps)[Serving and narration](/drmhse/tts-rs/blob/main/docs/reference.md#serving-and-narration)[What did not work](/drmhse/tts-rs/blob/main/docs/reference.md#what-did-not-work)\n\n```\ncrates/tts-core/        the Engine trait, voice assets, segmentation, WAV, the PRNG\ncrates/tts-nn/          shared model machinery + the custom Metal kernels\ncrates/tts-engines/     the registry — the one place that knows which engines exist\ncrates/tts-cli/         the `tts` binary: engines / voice / speak\ncrates/tts-serve/       the HTTP service: one engine, loaded once, behind a semaphore\ncrates/tts-bench/       the thermally-honest measurement harness\ncrates/tts-probe/       op-level benchmarks, one binary per question\ncrates/{audio8,cosyvoice,qwen3tts}/   one engine each, plus its fixture gate\n\nreferences/{audio8,cosyvoice,qwen3tts}/   the PyTorch side: conversion, fixtures, quality\nfixtures/{audio8,cosyvoice,qwen3tts}/     per-stage ground truth the gates compare against\nvoices/                 voice assets, one directory each (tracked — they are small)\nexamples/               senior.txt and chapter.txt, the two benchmark fixtures\nscripts/                bootstrap, fetch-assets, gates, render-examples, narration\n```\n\nEverything shared is named `tts-*`\n\n; everything engine-specific is named for its engine, and\n`crates/audio8`\n\n, `crates/cosyvoice`\n\nand `crates/qwen3tts`\n\nmatch the ids `--engine`\n\ntakes.\n\nWeights and virtualenvs are not tracked and [docs/reference.md](/drmhse/tts-rs/blob/main/docs/reference.md#setup) builds\nthem. Fixtures are fetched rather than regenerated — `scripts/fetch-assets.sh`\n\npulls ~130 MB of\nchecksummed ground truth from\n[ drmhse/tts-rs-assets](https://huggingface.co/datasets/drmhse/tts-rs-assets), so\n\n`./scripts/gates.sh`\n\nruns without any PyTorch.Apache-2.0 ([LICENSE](/drmhse/tts-rs/blob/main/LICENSE)). Contains **no model weights** — you download those yourself,\nand all three are Apache-2.0 per their model cards:\n[Audio8-TTS-Preview-0.6b](https://huggingface.co/Audio8/Audio8-TTS-Preview-0.6b),\n[Fun-CosyVoice3-0.5B](https://huggingface.co/FunAudioLLM/Fun-CosyVoice3-0.5B),\n[Qwen3-TTS-12Hz-1.7B-Base](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-Base).\n\nTwo kinds of *derived* artifact are distributed: the voice assets in `voices/`\n\n, encoded from\nreference audio those models ship, and the fixtures in\n[ drmhse/tts-rs-assets](https://huggingface.co/datasets/drmhse/tts-rs-assets). The upstream\nlicences reach both; attribution and the statement of changes are in\n\n[NOTICE](/drmhse/tts-rs/blob/main/NOTICE).", "url": "https://wpnews.pro/news/narrate-complete-books-using-qwen3-tts-natural-sounding", "canonical_source": "https://github.com/drmhse/tts-rs", "published_at": "2026-08-16 09:07:57+00:00", "updated_at": "2026-08-16 09:40:59.122208+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "generative-ai", "ai-tools", "ai-infrastructure"], "entities": ["tts-rs", "Qwen3TTS", "Audio8", "CosyVoice", "Apple M4", "Metal", "PyTorch", "drmhse"], "alternates": {"html": "https://wpnews.pro/news/narrate-complete-books-using-qwen3-tts-natural-sounding", "markdown": "https://wpnews.pro/news/narrate-complete-books-using-qwen3-tts-natural-sounding.md", "text": "https://wpnews.pro/news/narrate-complete-books-using-qwen3-tts-natural-sounding.txt", "jsonld": "https://wpnews.pro/news/narrate-complete-books-using-qwen3-tts-natural-sounding.jsonld"}}