Skip to content

Latest commit

 

History

99 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CrispTTS: Modular German Text-to-Speech Synthesizer

CrispTTS is a versatile command-line Text-to-Speech (TTS) tool designed for synthesizing German speech using a variety of popular local and cloud-based TTS engines. Its modular architecture allows for easy maintenance and straightforward addition of new TTS handlers.

Part of the Crisp ecosystem

Project Role
Susurrus Python GUI + CLI — 30+ ASR, 12 TTS, translation
CrispASR C++ ASR/TTS engine — 26+ ASR, 20+ TTS backends, ggml inference
CrispTTS This repo — Python TTS CLI with 37+ handlers
CrisperWeaver Flutter transcription app — desktop + mobile

NOTE: This is in experimental / work in progress state. Some Python-only models may be broken due to dependency conflicts. The CrispASR-based handlers (crispasr_*) are the most reliable — they use native C++ inference with no Python ML dependencies.

Features

  • 37+ TTS Engine Support:
    • CrispASR native C++ engines (16 backends, auto-download, no Python ML deps):
      • Kokoro (multilingual, Apache 2.0)
      • Orpheus + Kartoffel-Orpheus DE (19 German speakers, llama3.2 license)
      • Qwen3-TTS (voice cloning + voice design, Apache 2.0)
      • Chatterbox (CFM synthesis, MIT)
      • VibeVoice TTS (voice cloning)
      • IndexTTS (zero-shot cloning, Apache 2.0)
      • VoxCPM2 (48 kHz, 30 languages, Apache 2.0)
      • F5-TTS (flow-matching, voice cloning, Apache 2.0)
      • MeloTTS (VITS2, 44.1 kHz, MIT)
      • Piper (250+ community voices, 30+ languages — faster than Python Piper)
      • BananaMind-TTS (Tacotron-lite + HiFi-GAN, en/de)
      • Dots.TTS (Qwen2.5 LLM + DiT + BigVGAN, 48 kHz, CAM++ voice cloning)
      • CosyVoice3 (multi-GGUF: LLM+flow+CAM+++HiFT, voice cloning)
      • CSM/Sesame (Llama backbone + Mimi codec, causal mode, voice cloning)
      • OmniVoice (Qwen3 masked iterative, 600+ languages, voice cloning)
      • MOSS-TTS-Local (4B transformer + codec-v2, 48 kHz)
    • Microsoft Edge TTS (cloud-based, requires edge-tts)
    • Coqui TTS (XTTS v2, VITS, etc.)
    • Piper (local ONNX, requires piper-tts)
    • Orpheus GGUF (local, requires llama-cpp-python)
    • Orpheus via LM Studio / Ollama API
    • OuteTTS (LlamaCPP or HF backend)
    • SpeechT5 (German fine-tune via HF Transformers)
    • FastPitch (German via NeMo)
    • mlx-audio (Bark, Kokoro, Dia — Apple Silicon)
    • LLaSA (hybrid, German, multilingual variants)
    • F5-TTS (MLX/PyTorch)
    • Kokoro ONNX (lightweight)
    • TTS.cpp (GGUF models)
    • Zonos (acoustic conditioning)
    • Chatterbox Python (Kartoffelbox)
  • AI Audio Watermarking & Provenance:
    • AudioSeal neural watermark (MIT — code + weights; preferred; survives Opus and 64 kbps MP3 at full confidence; pip install 'crisptts[robust]')
    • Spread-spectrum watermark (always on, low-level — 20–25 dB SNR on speech)
    • WavMark neural watermark (MIT — code + weights; quietest at 36.3 dB, but far slower; pip install 'crisptts[watermark-mit]')
    • WAV LIST/INFO, MP3 ID3v2, FLAC Vorbis comment, and Opus/OGG metadata marking audio as AI-generated
    • C2PA content credentials signed by default for WAV/MP3/FLAC/M4A (core dep; c2pa-audio / CrispASR used as fast paths when present)
    • Voice-cloning consent gate (--i-have-rights CLI / "i_have_rights": true API)
    • Spoken AI disclaimer prepended to voice-cloned audio — and to models whose preset voice is an identifiable person (speaker_identity, --speaker-identity) — in all 24 EU official languages plus zh/ja/ko (--list-disclosure-langs), bundled for offline use
    • Persistent consent audit log at ~/.cache/crisptts/consent_audit.log, owner-only, auto-pruned, with --consent-log-erase for GDPR Art. 17
  • CrispASR Integration:
    • --verify: ASR roundtrip verification of TTS output quality
    • --translate: Pre-synthesis translation (EN→DE via m2m100/MadLad)
    • --speech-speed: Rate multiplier (maps to CrispASR --pace)
    • --trim-silence: Remove leading/trailing silence from output
    • --tts-steps: Diffusion model inference steps (quality vs speed)
    • --tts-language: Override language for multilingual models
    • --pitch-shift: Pitch shift in Hz for FastPitch backends
    • --instruct: Natural-language voice descriptions (Qwen3-TTS VoiceDesign)
    • --stream: Stream audio playback during synthesis
    • --output-sample-rate: Resample output to target sample rate
  • OpenAI-Compatible API Server (--server):
    • POST /v1/audio/speech — drop-in replacement for OpenAI TTS
    • GET /v1/audio/models — list all configured models
    • Voice-cloning consent gate (returns 403 if i_have_rights missing)
    • All responses watermarked + metadata-injected
  • Text Input Flexibility: Synthesize from CLI, .txt, .md, .html, .pdf, .epub
  • Smart Text Chunking: Automatic sentence-boundary splitting for long texts
  • SSML-lite: Supports <break>, <prosody rate>, <say-as>, <phoneme> tags in input text
  • Customizable Output: Save audio to .wav, .mp3, .flac, .m4a or .opus (.opus/.ogg carry no C2PA manifest, so they require a neural watermark — pip install 'crisptts[robust]')
  • Direct Playback: Play synthesized audio immediately
  • Voice Selection: Override default voices/speakers for most models
  • Model Parameter Tuning: JSON-formatted parameters for fine-tuning
  • Comprehensive Testing:
    • --test-all: Test all models with default voices
    • --test-all-speakers: Test all models with all configured voices
    • 439 unit and live tests
  • Modular Design: config.py + utils.py + handlers/ + main.py
  • Logging: Configurable logging levels
  • Automatic Patching: Runtime monkeypatches for library compatibility

Project Structure

crisptts_project/
├── main.py                     # Main CLI application script
├── config.py                   # Model configurations and global constants
├── utils.py                    # Shared utility functions and classes
├── watermark.py                # Audio watermarking, metadata, consent gate, C2PA
├── chunking.py                 # Smart sentence-boundary text splitting
├── server.py                   # OpenAI-compatible HTTP API server
├── ssml.py                     # SSML-lite tag preprocessor
├── cache.py                    # Synthesis result caching (LRU)
├── decoder.py                  # User-provided decoder for Orpheus models (if used)
├── handlers/                   # Package for individual TTS engine handlers
│   ├── __init__.py             # Makes 'handlers' a package, exports handler functions
│   ├── crispasr_handler.py     # CrispASR native C++ TTS (10 backends)
│   ├── edge_handler.py         # Edge TTS cloud service handler
│   ├── piper_handler.py        # Piper TTS (ONNX) handler
│   ├── orpheus_gguf_handler.py # Local Orpheus GGUF model handler
│   ├── orpheus_api_handler.py  # Handlers for LM Studio and Ollama API
│   ├── outetts_handler.py      # OuteTTS model handler
│   ├── speecht5_handler.py     # SpeechT5 model handler
│   ├── nemo_handler.py         # NeMo FastPitch handler
│   ├── coqui_tts_handler.py    # Coqui TTS handler (for XTTS, VITS etc.)
│   ├── kartoffel_handler.py    # Orpheus "Kartoffel" Transformers handler
│   ├── kokoro_onnx_handler.py  # Kokoro (multilingual but no German) ONNX handler
│   ├── llasa_hybrid_handler.py # LLaSA Hybrid handler
│   ├── tts_cpp_handler.py      # TTS.cpp handler supporting GGUF models
│   ├── f5_tts_handler.py       # F5-TTS handler (MLX/PyTorch)
│   ├── zonos_handler.py        # Zonos acoustic conditioning handler
│   ├── chatterbox_handler.py   # Chatterbox/Kartoffelbox handler
│   └── mlx_audio_handler.py    # Handler for mlx-audio library (e.g., Bark)
├── tests/                      # Unit and integration tests
├── requirements.txt            # Python package dependencies
├── pyproject.toml              # Project metadata and build config
└── README.md                   # This documentation file

Setup and Installation

Prerequisites

  • Python 3.10+ and pip for installing packages
  • For mlx-audio based models: Apple Silicon Mac is required for GPU acceleration
  • For TTS.cpp a C++ compiler and CMake are required to build the engine

Installation Steps

  1. Clone/Download Files

    git clone https://github.com/CrispStrobe/CrispTTS
  2. Create a Virtual Environment (Recommended):

    python -m venv venv
    source venv/bin/activate  # On Windows: venv\Scripts\activate
  3. Install Dependencies:

    pip install -r requirements.txt

    Optional feature groups:

    pip install crisptts[robust]          # RECOMMENDED: AudioSeal neural watermark (MIT)
    pip install crisptts[watermark-mit]   # WavMark instead — quieter, much slower
    pip install crisptts[dev]             # ruff, bandit, pytest

    C2PA signing (c2pa-python), container metadata (mutagen), soundfile and pydub are core dependencies, not extras — without them the AI-provenance marking cannot be applied and CrispTTS fails closed rather than emitting unmarked synthetic audio. The metadata and provenance extras are kept only so existing install commands keep working.

    Note: Some libraries like PyTorch, NeMo, LlamaCPP, and mlx-audio can have specific installation needs depending on your OS and hardware (e.g., CUDA for Nvidia GPUs, Metal for Apple Silicon). Please refer to their official documentation if you encounter issues. Ensure you have ffmpeg installed and available in your system's PATH if you encounter issues with audio file format conversions or direct playback (some underlying libraries might need it).

  4. Install and Build Engine-Specific Dependencies (required for certain handlers):

    For TTS.cpp: Clone and build the TTS.cpp project separately.

    git clone https://github.com/mmwillet/TTS.cpp.git
    cd TTS.cpp
    cmake -B build
    cmake --build build --config Release
    cd ..

    Update the tts_cpp_executable_path in config.py to point to ./TTS.cpp/build/cli

    For kokoro-onnx: Install the Python package and download model files.

    pip install kokoro-onnx

    Download the .onnx model and voices.bin from the kokoro-onnx GitHub releases page.

    Update the paths in config.py to point to your downloaded files.

  5. Environment Variables (Optional but Recommended):

    • HF_TOKEN: If you need to download models from gated or private Hugging Face repositories, set this environment variable with your Hugging Face API token:
      export HF_TOKEN="your_huggingface_token_here"
    • GGML_METAL_NDEBUG=1: Set automatically by main.py to reduce verbose Metal logs from llama-cpp-python on macOS.

Configuration (config.py)

The config.py file is central to defining which TTS models are available and their default settings.

  • GERMAN_TTS_MODELS Dictionary: This is the primary configuration structure. Each key is a unique MODEL_ID used in the CLI. The value is a dictionary containing:

    • "handler_function_key" (Optional, defaults to MODEL_ID): The key used to look up the synthesis function in handlers.ALL_HANDLERS
    • Specific parameters for that model (e.g., model_repo_id, default_voice_id, API URLs, onnx_repo_id, etc.)
    • "notes": A brief description of the model
  • mlx-audio Bark Configuration Example: To use the mlx-audio Bark model, your configuration might look like this, enabling the dual-source strategy for voice prompts (main model from mlx-community, voice NPYs from suno):

    "mlx_audio_bark_de": {
        "handler_function_key": "mlx_audio",
        "mlx_model_path": "mlx-community/bark-small", # Main MLX model
        # Voice prompts will be fetched by the patched handler from "suno/bark-small"
        "default_voice_id": "v2/de_speaker_3", 
        "available_voices": ["v2/de_speaker_0", "v2/de_speaker_1", "v2/de_speaker_3", "..."],
        "lang_code": "de",
        "sample_rate": 24000,
        "notes": "mlx-audio (Bark) with main model from mlx-community/bark-small and voices from suno/bark-small (via patch)."
    },
  • Global Constants: API URLs, default voice names, and sample rates are also defined here

  • Adding/Modifying Models: To add a new variation of an existing engine or a completely new engine (after creating its handler), you would add a new entry to GERMAN_TTS_MODELS

Usage (main.py)

All interactions are done through main.py from your project's root directory.

Basic Command Structure

python main.py [ACTION_FLAG | --model-id <MODEL_ID> [OPTIONS]]
# or using the --backend shortcut for CrispASR engines:
python main.py --backend kokoro --input-text "Hello" --output-file out.wav

CLI Reference

Primary Actions

Flag Description
--list-models List all configured TTS models with their notes
--voice-info MODEL_ID Show available voices/speakers for a model
--test-all Test all models with default voices (requires --input-text or --input-file)
--test-all-speakers Test all models with ALL configured voices
--skip-models M1 M2 ... Skip specific model IDs during --test-all / --test-all-speakers
--detect-watermark FILE Detect AI-generated watermark in a WAV file and report confidence
--server Run as HTTP server with OpenAI-compatible endpoints
--check With --list-models: probe CrispASR backends for availability

Synthesis Options

Flag Default Description
--model-id MODEL_ID TTS model to use (see --list-models for choices)
--backend NAME Shortcut for CrispASR backends (e.g., kokoro, piper, dots-tts)
--input-text TEXT Text to synthesize (mutually exclusive with --input-file)
--input-file PATH Input file: .txt, .md, .html, .pdf, .epub
--output-file PATH Save audio to file (format detected from extension: .wav, .mp3, .flac, .opus)
--output-dir DIR tts_test_outputs Output directory for --test-all / --test-all-speakers
--play-direct off Play audio immediately after synthesis
--german-voice-id ID model default Override voice/speaker (name, ID, or path to .wav for cloning)
--model-params JSON JSON string of model-specific parameters, e.g. '{"temperature":0.7}'
--speech-speed FLOAT 1.0 Speech rate multiplier (>1 = faster, <1 = slower)
--trim-silence off Remove leading/trailing silence from output
--tts-steps N backend default Diffusion/flow-matching inference steps (quality vs. speed)
--tts-language LANG model default Override language code for multilingual models (e.g. de, en, zh, ja)
--pitch-shift HZ 0 Pitch offset in Hz (positive = higher, negative = lower)
--instruct TEXT Natural-language voice description for VoiceDesign models (Qwen3-TTS)
--output-sample-rate HZ native Resample output to target sample rate (e.g. 16000, 22050, 44100)
--stream off Stream audio playback during synthesis (CrispASR backends only)
--ref-text TEXT Transcript of reference voice audio for inline voice cloning (TADA, dots-tts)
--no-spoken-disclaimer off Skip the AI-disclosure spoken prefix on voice-cloned audio
--lexicon TSV_PATH Custom word→phoneme TSV file for CrispASR pronunciation
--batch off Split input at blank lines, produce numbered output files
--jobs N 1 Concurrent synthesis jobs for --batch mode
--normalize off Peak-normalize output audio to -3 dB

CrispASR Integration

Flag Default Description
--verify off Run ASR on output for roundtrip quality verification
--verify-backend NAME parakeet ASR backend for --verify (e.g. parakeet, whisper)
--translate off Translate input text before synthesis
--translate-from LANG en Source language for translation
--translate-to LANG de Target language for translation
--translate-backend NAME m2m100 Translation backend (m2m100 or madlad)

Watermarking & Provenance

Flag Default Description
--no-watermark off Disable all marking layers: audio watermark, metadata, C2PA (debug only)
--allow-unmarked off Deliver output even if marking fails or is undetectable
--accept-marking-responsibility off Required for any provenance opt-out; logged as [MARKING]
--watermark-model PATH Path to AudioSeal GGUF model for neural watermarking
--i-have-rights off Consent attestation for voice-cloning models (required)
--disclosure-lang LANG model's language Language of the spoken AI disclosure on cloned audio
--list-disclosure-langs List the languages the spoken disclosure is available in
--consent-log-prune Drop consent audit entries past the retention window
--consent-log-erase [SUBJECT] Erase consent audit entries (GDPR Art. 17)
--c2pa-cert PEM X.509 PEM certificate for C2PA content credentials
--c2pa-key PEM PEM private key for C2PA content credentials

Server Options

Flag Default Description
--server off Start the HTTP API server
--server-host ADDR 127.0.0.1 Server bind address
--server-port PORT 8880 Server port
--rate-limit N 10 Max synthesis requests per minute per IP (0=unlimited)
--warm-up MODEL_ID Pre-synthesize at startup to warm model caches

Model-Specific Parameters (--model-params)

Parameters are passed as a JSON string. Available keys depend on the backend:

Key Backends Description
temperature Most LLM-based Sampling temperature (higher = more varied)
seed All CrispASR Random seed for reproducible output
top_p LLM-based Nucleus sampling threshold
repetition_penalty LLM-based Penalize token repetition
tts_steps Diffusion/flow Number of inference steps
speech_speed CrispASR Rate multiplier (same as --speech-speed)
pitch_shift FastPitch Hz offset (same as --pitch-shift)
top_k LLM-based Top-K candidates
min_p LLM-based Min-P threshold
cfg_weight Chatterbox Classifier-free guidance weight
cfg_scale Chatterbox, F5, TADA CFG scale for acoustic conditioning
exaggeration Chatterbox Emotion exaggeration factor
length_scale VITS Duration scaling factor
speaker_name Multi-speaker Speaker name override
speaker_id Piper Multi-speaker model ID
do_sample TADA 0=greedy, 1=sample talker
num_candidates TADA Acoustic flow-matching candidates
num_steps TADA, flow-matching FM/diffusion inference steps
noise_temp TADA FM noise temperature
noise_scale Piper VITS variance
noise_w Piper Stochastic duration predictor
max_speech_tokens Chatterbox Max AR tokens
tts_speed OmniVoice Target-length speed estimate

Example:

python main.py --model-id crispasr_chatterbox \
  --model-params '{"cfg_weight": 3.0, "exaggeration": 0.7, "temperature": 0.8}' \
  --input-text "Emotional speech test." --output-file chatterbox.wav

Common Examples

List all available models:

python main.py --list-models

Get information about voices for a specific model:

python main.py --voice-info edge
python main.py --voice-info mlx_audio_bark_de

Synthesize text using a specific model:

python main.py --model-id edge --input-text "Hallo, wie geht es Ihnen heute?" --output-file hallo_edge.mp3 --play-direct

Synthesize text using mlx-audio Bark (German):

python main.py --model-id mlx_audio_bark_de --input-text "Das ist ein Test mit Bark auf Apple Silicon." --output-file bark_test_de.wav

Use a specific German voice (if supported by the model):

python main.py --model-id edge --input-text "Ein Test mit einer anderen Stimme." --german-voice-id de-DE-ConradNeural --output-file conrad_test.mp3

Check --voice-info <MODEL_ID> for available voice IDs/formats for that model.

Synthesize text from a file:

python main.py --model-id piper_local --input-file ./my_text.txt --output-file piper_output.wav

Supported input file types: .txt, .md, .html, .pdf, .epub.

Use model-specific parameters (as a JSON string):

python main.py --model-id orpheus_gguf --input-text "Ein Test." --model-params "{\"temperature\": 0.8, \"n_gpu_layers\": -1}" --output-file orpheus_custom.wav

Test all configured models with default voices:

python main.py --input-text "Dies ist ein kurzer Test für alle Modelle." --test-all --output-dir ./test_results

Test all models with all their configured available voices/speakers:

python main.py --input-text "Ein Test für alle Stimmen." --test-all-speakers --output-dir ./test_results_all_speakers

Speech speed and pitch control:

python main.py --model-id crispasr_kokoro --input-text "Schneller sprechen." --speech-speed 1.3 --output-file fast.wav
python main.py --model-id crispasr_kokoro --input-text "Höher." --pitch-shift 50 --output-file high.wav

Silence trimming and resampling:

python main.py --model-id crispasr_kokoro --input-text "Test." --trim-silence --output-sample-rate 16000 --output-file trimmed_16k.wav

VoiceDesign — generate voices from text descriptions:

python main.py --model-id crispasr_qwen3_tts_voicedesign --instruct "A calm elderly man" --input-text "Hallo" --output-file calm.wav

Streaming playback (hear audio while it generates):

python main.py --model-id crispasr_kokoro --input-text "Dies wird sofort abgespielt." --stream

Run as OpenAI-compatible API server:

python main.py --server --server-port 8880
# Then: curl -X POST http://localhost:8880/v1/audio/speech \
#   -H "Content-Type: application/json" \
#   -d '{"model":"crispasr_kokoro","input":"Hallo Welt","voice":"af_heart"}' \
#   --output speech.wav

Voice cloning (with consent attestation):

# CLI
python main.py --model-id coqui_xtts_v2_de_clone --i-have-rights \
  --input-text "Hallo" --german-voice-id ref_voice.wav --output-file cloned.wav

# API (include i_have_rights in request body)
curl -X POST http://localhost:8880/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"crispasr_f5_tts","input":"Hallo","voice":"ref.wav","i_have_rights":true}' \
  --output cloned.wav

SSML-lite markup:

python main.py --backend kokoro --output-file out.wav --input-text \
  'Hello. <break time="500ms"/> <prosody rate="fast">This part is fast.</prosody> Normal again.'

Batch synthesis with parallel jobs:

python main.py --backend kokoro --batch --jobs 4 \
  --input-file book.txt --output-dir chapters/

Audio normalization:

python main.py --backend kokoro --input-text "Test" --normalize --output-file normalized.wav

Change Logging Level (for debugging):

python main.py --model-id edge --input-text "Debug Test." --loglevel DEBUG

Override API URLs (for API-based models like Orpheus LM Studio/Ollama):

python main.py --model-id orpheus_lm_studio --input-text "Hallo API" --lm-studio-api-url http://localhost:5000/v1/completions
python main.py --model-id orpheus_ollama --input-text "Hallo Ollama" --ollama-api-url http://localhost:11223/api/generate --ollama-model-name my-orpheus-ollama-model

Supported TTS Engines

Refer to the output of python main.py --list-models for the currently configured models and their notes. The script supports integration with:

  • CrispASR native C++ (16 backends: Kokoro, Orpheus, Qwen3-TTS, Chatterbox, VibeVoice, IndexTTS, VoxCPM2, F5-TTS, MeloTTS, Piper, BananaMind, Dots.TTS, CosyVoice3, CSM/Sesame, OmniVoice, MOSS-TTS-Local)
  • Microsoft Edge TTS
  • Piper TTS
  • Orpheus GGUF (via llama-cpp-python)
  • Orpheus via LM Studio API
  • Orpheus via Ollama API
  • OuteTTS (LlamaCPP and Hugging Face ONNX backends)
  • SpeechT5 (Hugging Face Transformers)
  • FastPitch (NeMo / Hugging Face)
  • Coqui TTS (XTTS, VITS, etc.)
  • Orpheus "Kartoffel" (Transformers-based)
  • LLaSA Hybrid (Experimental MLX + PyTorch)
  • mlx-audio (e.g., Bark for Apple Silicon)
  • F5-TTS (MLX/PyTorch)
  • Zonos (acoustic conditioning)
  • Chatterbox/Kartoffelbox (Python)

Adding New TTS Handlers

The modular design makes it easy to add support for new TTS engines:

  1. Create a New Handler File: In the handlers/ directory, create a new Python file (e.g., my_new_tts_handler.py)

  2. Implement Synthesis Function: Inside this file, write a function that takes the standard arguments: model_config, text, voice_id_override, model_params_override, output_file_str, play_direct. This function should handle all aspects of using the new TTS engine.

  3. Update handlers/__init__.py: Import your new function and add it to the ALL_HANDLERS dictionary.

  4. Update config.py: Add a new entry to GERMAN_TTS_MODELS for your new engine.

decoder.py Requirement for Orpheus

For all Orpheus-based models (GGUF local, LM Studio API, Ollama API, Kartoffel), this project relies on a user-provided decoder.py file located in the project's root directory. This file must contain a function:

def convert_to_audio(multiframe_tokens: list[int], total_token_count: int) -> bytes | None:
    # Your implementation here to convert Orpheus token IDs to raw PCM audio bytes
    # (16-bit, 24000 Hz, mono)
    # Return audio frame bytes, or None/empty bytes on error.
    pass

If this file or function is missing, Orpheus models will not produce audible output, and a placeholder will be used.

Voice & model licensing

CrispTTS is a synthesis tool — it does not bundle or redistribute any voice/model weights. Each model is downloaded at runtime from its upstream repository into a local cache (Piper voices from rhasspy/piper-voices, Coqui models via the TTS library, etc.). You obtain the weights directly from the source, under that source's terms.

You are responsible for honouring each voice's license for whatever you produce. Licenses vary per voice and are not uniform across rhasspy/piper-voices — check the upstream MODEL_CARD (and, where it only says "See URL", the underlying dataset), because the card fields are self-reported. Notable cases among the German Piper voices CrispTTS lists:

  • thorsten, kerstin — CC0 (public domain).
  • eva_k, karlsson, ramonaM-AILABS, BSD-style (commercial OK; retain the copyright notice).
  • mls — CC-BY 4.0 (attribution required).
  • pavoqueCC BY-NC-SA 4.0 (non-commercial) — do not use the output commercially.

For a redistributable, pre-curated permissive-only GGUF set (the same voices minus the non-commercial/restricted ones, converted for the CrispASR/CrisperWeaver native runtime), see cstr/piper-voices-GGUF.

Audio Watermarking & Provenance

CrispTTS automatically marks all synthesized audio as AI-generated using a multi-layered provenance system.

Every path that writes an audio file — CLI synthesis, --batch, --test-all and API server responses — marks it through the single watermark.mark_audio_file() entry point, in every supported format (WAV, MP3, FLAC, Opus/OGG). Marking is the last step, after trimming, normalization, resampling and the spoken disclaimer, so nothing downstream can strip it. CrispASR C++ backends watermark at the binary level; all other handlers are watermarked in Python post-synthesis.

Marking follows the file that was written, not the one that was requested. Most handlers force their own container regardless of the extension you ask for — the Edge handler writes .mp3, most local ones .wav — so --output-file out.wav --model-id edge produces out.mp3, and that is the file disclosed, marked, verified and played.

Generation is gated on sufficient marking. Three rules:

  1. Preflight. Before any model is loaded, CrispTTS checks that the requested output can be marked — supported container, codec dependencies present. If not, synthesis is refused up front, so unmarkable audio is never produced.
  2. Verified, not assumed. After marking, the watermark is read back, and the output is delivered only if at least one robust layer is confirmed: the watermark detected above threshold, or a C2PA manifest signed. Container metadata alone is never sufficient — any transcode strips it. So an undetectable watermark is fatal for FLAC and Opus, which cannot carry a manifest, while a WAV or MP3 may still ship on its manifest alone; the MarkResult reports which layers actually applied. This catches audio that is silent or otherwise unable to carry a mark, and it applies to CrispASR backends too: their binary-level watermark is verified rather than taken on trust.
  3. Watermark floor. --no-watermark is honoured only when another robust layer (a C2PA manifest) still marks the output. When the container cannot carry one, the watermark is forced back on — no path can emit a fully unmarked AI file. This mirrors CrispASR's watertight-CLI guarantee.

Any provenance opt-out (--no-watermark, --allow-unmarked, --no-spoken-disclaimer) additionally requires --accept-marking-responsibility, which is recorded as a [MARKING] audit line next to [CONSENT].

Layers

Layer What Status Install
AudioSeal Neural watermark (Meta, 16-bit message, MIT code and weights). Measured 28.9 dB SNR; survives 64 kbps MP3 and Opus at confidence 1.000 Auto-detected (preferred) pip install 'crisptts[robust]'
Spread-spectrum Frequency-domain watermark (32 bins, alpha 0.05 — the active band's default) Always active Built-in (numpy)
WavMark Neural watermark (MIT, 16-bit payload, measured 36.3 dB SNR — the quietest option). Slow: read the cost warning below Auto-detected (fallback) pip install 'crisptts[watermark-mit]'
WAV/MP3/FLAC/Opus metadata LIST/INFO, ID3v2, Vorbis comments — AI_GENERATED=true Always active Built-in (mutagen, core dep)
C2PA credentials Signed provenance manifests (trainedAlgorithmicMedia) — self-signed unless you supply a certificate Always active (WAV/MP3/FLAC/M4A) Built-in (c2pa-python, core dep)
Spoken disclaimer AI disclosure prepended to voice-cloned audio, in the model's language Auto for cloning Built-in
Consent gate Voice-cloning attestation + persistent audit logging Required for cloning Built-in

Watermark backend priority: AudioSeal (Python) > WavMark (MIT) > CrispASR GGUF > spread-spectrum (always-on fallback). Neural backends are lazy-loaded on first synthesis, so --list-models and --help stay fast — measured 0.36 s of CPU, against 0.10 s for starting a bare Python interpreter. (That claim was aspirational until v0.9.7: the __main__ block imported torch unconditionally, so those commands cost 4.3 s of CPU. A test now asserts --help imports no ML framework.)

Choosing a neural backend

Both are MIT for code and weights (AudioSeal's weights moved from CC-BY-NC to MIT in April 2024), so the choice is purely operational. Measured here on 10 s of speech at 16 kHz:

AudioSeal WavMark
Model load 1.9 s 6.9 s
Embed + detect, 10 s audio 2.5 s 12.4 s
Embed + detect, 20 s audio ~3 s 14.3 s
SNR 28.9 dB 35.8 dB
64 kbps MP3 1.000 1.000
Opus round-trip 1.000

AudioSeal remains the default: it is still several times faster, and it is the one measured to survive an Opus round-trip, which is the container C2PA cannot sign. WavMark's ~7 dB quieter embed is its real advantage — take it when imperceptibility outranks throughput.

WavMark used to be unusable here (embed ~180 s for 10 s of audio, and a detect that did not return within 10 minutes). Nothing about the model changed; three things about how CrispTTS drives it did:

  • Device. load_wavmark() selected CUDA-or-CPU and never MPS, so every Apple Silicon machine took the slowest device it owned. One forward pass on a 1 s chunk: 16–30 s on CPU at torch's default 4 threads, 5.4 s at 8 threads, 0.54 s on MPS. The loader now prefers CUDA → MPS → CPU, and raises the thread count on the CPU path.
  • Detection scan. wavmark.decode_watermark scans every window position and averages all hits, because it is recovering a payload. CrispTTS only asks "is this marked, with our payload", so it stops at the first batch containing an exact start-bit match: 34.7 s → 9.3 s at 10 s, and 79.3 s → 6.8 s at 20 s. Upstream's cost scales with duration; this does not.
  • Marks are device-independent: MPS and CPU embeds differ by 2.4e-07, and a file marked on either verifies on the other.

None of this required forking or patching wavmark — only model.encode and model.decode, its public model API. Note that upstream's encode_watermark declares min_snr=20, max_snr=38, so 38 dB is the ceiling of its iterative per-chunk search, not a floor it clears.

Unmarked audio is the worst case for both backends and stays slow (~35 s for 10 s of audio with WavMark): there is no hit to stop on, so the full scan runs.

Confidence values are not comparable across backends. AudioSeal's detector saturates (measured 1.000 watermarked / 0.000 clean); the spread-spectrum detector reads ~0.17 unmarked and ~0.99 marked. Both are gated at 0.65 and each clears it unambiguously, but do not compare a number from one against a number from the other.

Robustness of the built-in fallback

The built-in spread-spectrum watermark places a 32-bin comb in FFT bins 64–204 of a 1024-point transform, matching CrispASR's wm_params. The band is defined in bin indices, not hertz, so the frequency range it occupies scales with the file's sample rate:

Output sample rate Comb occupies Note
16 kHz ~1.0–3.2 kHz inside the speech band
24 kHz ~1.5–4.8 kHz the design target, and CrispASR's rate
44.1 kHz ~2.8–8.8 kHz extends well above the speech band
48 kHz ~3.0–9.6 kHz as above — MeloTTS, VoxCPM2, MOSS, Dots.TTS

Interoperability with CrispASR is unaffected, because both sides address the comb by bin index. Perceptually the placement is not equivalent at every rate, and the "~1.5–4.8 kHz" figure quoted here previously was only ever true at 24 kHz.

The detector was replaced in v0.9.8 and these numbers describe the new one. The old one scored 32 bins by the sign of their excess over neighbouring bins, discarding the size — a coin flip per bin, so its score sat only 1.7 standard deviations above chance at the 0.65 threshold. Measured over a corpus of unmarked audio and marked audio under attack:

old (sign test) new
False positives at 0.65 8.6% 1.3%
True positives at 0.65 97.0% 98.3%

So the old detector was both flagging genuine recordings as AI-generated and missing 3% of real watermarks. The new one measures how far the comb's excess is consistent across frames, and how far that is specific to our pattern rather than to any pattern (by scoring 15 decoy patterns on the same audio). The embed is unchanged, so files marked by earlier releases and by CrispASR still verify.

Confidence landmarks moved with the statistic — unmarked audio now reads ~0.17 median (it read ~0.44), a healthy mark ~0.99 (it read ~0.84). The threshold is still 0.65.

Fixed in v0.9.12: the stationary-tone false positive. A perfectly stationary synthetic tone used to read as marked (0.88), because every frame is identical so a chance correlation with the comb repeats without end and imitates consistency. The fix is a third condition — the real pattern must out-score the strongest decoy, not merely the decoy median. On such a tone every absent pattern also scores extremely (t 11.44 against a decoy maximum of 19.44), and the real pattern losing to a decoy is the tell. It cost nothing: true positives stayed at 99.4% on the tuning corpus while false positives went to zero there, and 2.5% → 1.3% on the broader one.

Signal-to-noise ratio: 20–25 dB mean, 14–17 dB worst case — not the "~38 dB" / "~39.5 dB" quoted in earlier revisions of this file. Those figures came from a single favourable segment. The watermark is low-level and sits under speech, but on quiet or sparse passages it is not categorically inaudible, and this README no longer claims that it is. If imperceptibility matters more to you than the built-in layer's robustness, install a neural backend: pip install 'crisptts[robust]' brings in AudioSeal at 28.9 dB, and WavMark reaches 36.3 dB if you can afford it. See Choosing a neural backend below.

These numbers supersede an earlier table measured on the pre-#260 wideband comb (0.94 after embed but 0.63 after a resample — below threshold, i.e. the mark was lost). Moving the comb into the speech band lowered the immediate reading while making it survive resampling and transcoding. CRISPASR_WATERMARK_LEGACY=1 restores the old band for A/B against older files; detection always sweeps both, so previously-marked audio still verifies.

Until v0.9.3 the embed also ran at the legacy band's strength (alpha 0.08) rather than the speech band's designed 0.05, because watermark_embed() kept 0.08 as a hardcoded signature default when the comb moved. That cost 3–4 dB of SNR for a confidence gain the threshold never required. Files marked by earlier versions remain valid and detectable — they are simply louder than intended.

This is why C2PA signing is on by default rather than opt-in: for WAV, MP3, FLAC and M4A output the signed manifest, not the spread-spectrum watermark, is the durable and interoperable provenance layer. The watermark is what survives having the manifest stripped; the manifest is what survives a resample. Neither alone is sufficient for every downstream path, so both are applied.

For Opus/OGG, which C2PA cannot sign, a neural backend is required rather than recommended — see below. Install it also if audio in any container may be transcoded and stripped; it is MIT-licensed and far more robust:

pip install 'crisptts[robust]'

CrispTTS logs a warning once per run when the spread-spectrum backend is the only robust layer present. WavMark is an extra rather than a core dependency because it pulls in PyTorch (~2 GB).

C2PA content credentials

Every WAV and MP3 output is signed with a C2PA manifest asserting digitalSourceType: trainedAlgorithmicMedia — the standard, machine-readable claim that content is AI-generated — plus a c2pa.training-mining assertion opting the audio out of AI training.

By default CrispTTS signs with the bundled development certificate in c2pa_dev_cert.py. Its private key is public, by design: it is in the source tree and in every wheel. A manifest signed with it proves the file has not been altered since signing, but it does not attest to who produced the file and will not validate against C2PA trust lists. CrispTTS never reports such a signature as trusted — MarkResult.c2pa_signer is "self-signed", and it logs a warning once per run.

For a credential others can attribute to you, obtain a certificate from a C2PA-recognised authority and pass --c2pa-cert / --c2pa-key (or set C2PA_CERT_PATH / C2PA_KEY_PATH); the signer is then reported as "ca-issued". The certificate must be a chain (leaf followed by its CA) and the key must be PKCS#8 — c2pa-python rejects a bare self-signed leaf and a SEC1 key. scripts/make_dev_cert.sh shows a working profile.

WAV, MP3, FLAC and M4A all carry a manifest. FLAC and M4A were excluded until it turned out that only sign_file() refuses them — c2pa-python's streaming Builder.sign() signs both, and the result reads back validation_state: Valid with the trainedAlgorithmicMedia assertion intact. CrispTTS uses the streaming path for every container, so those two gained a manifest they were previously denied. A test signs each listed format for real, so the set cannot drift back into overclaiming.

Opus/OGG cannot. c2pa-rs does not list it among its supported types at all, and every format string tried (opus, audio/opus, ogg, audio/ogg, application/ogg) returns NotSupported for embedded signing.

The detached-sidecar route does not rescue it either, and fails in a way worth recording: Builder.set_no_embed() followed by sign() reports success on Opus while writing a byte-identical copy of the input — the output begins OggS and hashes the same as the source. There is no manifest in it. An earlier revision of this file said a .c2pa sidecar "can be produced"; that was never verified, and it is false.

So the audio watermark is Opus/OGG's only robust layer, which is why --no-watermark is overridden for it, and why:

Opus and OGG output requires a neural watermark backend. With only the built-in spread-spectrum comb installed, synthesis to those containers is refused up front — a fixed-key comb as the sole robust layer is not marking that is "robust as far as technically feasible". Install one with pip install 'crisptts[robust]' — AudioSeal survives an Opus round-trip at confidence 1.000, measured — or choose a manifest-carrying container, or take the duty on yourself with --allow-unmarked --accept-marking-responsibility.

Signing backends

Three signers are tried in order, selectable with CRISPTTS_C2PA_BACKEND=auto|python|audio|crispasr|off (default auto):

Order Backend Availability
1 c2pa-audio Native, fast. Not on PyPI — build from source
2 c2pa-python Always — core dependency, and the only path where CrispTTS controls the manifest

CrispASR is deliberately not a signing backend. Checked against crispasr 0.8.25: --c2pa-cert / --c2pa-key configure signing of its own synthesis output and there is no flag that signs an existing file. What it does instead is better — see Upstream manifests below.

Every native result is verified before it is accepted. c2pa-audio's sign_wav() takes a certificate and a key but no manifest, so the library decides its own assertions — and a manifest without trainedAlgorithmicMedia marks a file as unaltered rather than as AI-generated. CrispTTS therefore reads the manifest back after any native signer runs; if the AI assertion is missing, that result is discarded and c2pa-python re-signs with a manifest that carries it. watermark.manifest_asserts_ai(path) exposes the same check.

Upstream manifests are preserved, not overwritten

CrispASR signs its TTS output during synthesis, by default, with a manifest that already asserts trainedAlgorithmicMedia. Every marking step rewrites the file, and any rewrite breaks that manifest's hash — injecting the WAV LIST/INFO chunk alone takes a CrispASR output from validation_state: Valid to Invalid.

So when a file already carries a manifest asserting AI generation, CrispTTS leaves it exactly as it is: no metadata injection, no re-signing, reported as c2pa:preserved. This keeps the upstream signer's identity (softwareAgent: CrispASR TTS) instead of replacing it with ours, and removes the failure mode where a broken-then-not-repaired manifest made an untampered file look tampered.

CrispTTS also no longer claims the CrispASR watermark as a layer. Measured on crispasr 0.8.25 kokoro output, CrispTTS's spread-spectrum detector reads 0.44 — its noise floor — so audio-watermark:upstream is reported only when verification actually detects a mark.

Voice cloning safety

Voice-cloning models require explicit consent attestation before synthesis is allowed:

  • CLI: --i-have-rights flag required (synthesis blocked without it)
  • API: "i_have_rights": true in request body (returns 403 without it)
  • Audit log: written to stderr AND ~/.cache/crisptts/consent_audit.log, including a SHA-256 digest of the reference recording. --test-all logs the attestation too, not just the gate check. See Audit log retention below.
  • Fails closed: if the gate cannot be evaluated at all (the watermark module is missing), synthesis is refused rather than allowed through. An unknown cloning status is treated as cloning, not as permission.

Detection, strongest signal first:

  1. A reference recording as the voice (.wav, .mp3, .flac, .ogg, .opus, .m4a). Handing the system somebody's voice to imitate is the cloning act itself, so this always gates — even for a model declared voice_cloning: false.
  2. The model's explicit voice_cloning key in config.py. Every shipped model sets it, in both directions; a test asserts this so a new backend cannot be added without answering the question.
  3. Handler key / model-ID keywords, as a fallback for user-supplied model dicts that predate the explicit key. This tier fails open, which is why tier 2 exists.

Whose voice is it? Cloning from a reference recording is not the only route to a deepfake. Art. 3(60) defines one by what the output resembles — "an existing person" — not by how the resemblance was produced, and a single-speaker model finetuned on one identifiable person's recordings produces audio of that person just as much. The voice donor's consent to their recordings being used for training is a licensing question; it is not the audience knowing the audio is synthetic, which is what Art. 50(4) is about.

So every model declaring voice_cloning: false additionally declares speaker_identity, and a test enforces it:

Value Meaning Spoken disclosure
real_person The preset voice is an identifiable individual — a named donor, or a corpus speaker such as VCTK's p225 Yes, as for cloning
synthetic A designed or blended voice that is not any one person No
unknown Provenance not established No, but warns once per model

unknown is a question, not a default: guessing "synthetic" would silently drop a disclosure that may be owed, and guessing "real_person" would prepend a sentence to every stock TTS voice. Override per run with --speaker-identity real_person|synthetic|unknown (API: "speaker_identity") when you know more about a voice than the config does.

The values come from reading each model's own documentation, and the evidence is recorded as a comment beside every non-obvious entry in config.py. The seven still marked unknown are the ones whose providers do not say:

Model(s) What the provider discloses
orpheus_lex_au, orpheus_ollama, mlx_audio_orpheus_llama, crispasr_orpheus Canopy Labs states 100k+ h of "permissive/non-copyrighted" audio and nothing about the origin of tara, leah, jess, leo, dan, mia, zac, zoe
edge Microsoft's TTS transparency note defines "voice talent" only for custom neural voice; it says nothing about how the prebuilt voices were built, and whoever they were modelled on is not publicly identified
crispasr_melotts MeloTTS documents its architecture lineage but not its speakers
crispasr_bananamind_tts No training-data documentation found

The real_person entries that are not self-evident from the model name trace back to identifiable recorded people: Kartoffel-Orpheus's 19 German speakers were extracted from podcasts and lectures; SauerkrautTTS's Tom and Anna are original studio recordings (Max and Lena are synthetic, but the model is classified by what it can speak as); SpeechT5's voice comes from a CMU ARCTIC x-vector, i.e. one of seven recorded individuals; and the German FastPitch is trained on HUI-Audio-Corpus-German, whose narrators — Eva, Karlsson and four others — are the same donors as the Piper voices.

Spoken disclosure — prepended to cloned output and to real_person preset voices, in all 24 EU official languages plus Chinese, Japanese and Korean (27 total; --list-disclosure-langs prints them). Art. 50 governs content placed on the EU market, so a disclosure an EU audience can understand means any EU official language — a German sentence in front of Greek audio discloses nothing to a Greek listener. The German and English wording is kept identical to Susurrus's disclosure.spoken string, so the Crisp projects disclose in the same words.

Which language is used, in order of precedence:

  1. --disclosure-lang (CLI) or "disclosure_lang" (API) — an explicit choice
  2. The model's declared language in config.py
  3. German, the default — with a warning

Step 3 is a fallback, not a decision. Around half the shipped cloning backends are multilingual (CosyVoice3, OmniVoice, IndexTTS, Qwen3-TTS, LLaSA-Multilingual, OuteTTS, Spark, VoxCPM2, MOSS, VibeVoice, F5), and for those the output language is a property of the input text, not of the model — so it cannot be derived from the config at all. They declare "language": "multilingual", which CrispTTS treats as unknown rather than silently substituting German, and warns that you should pass --disclosure-lang. A test asserts every cloning model declares a language key, so a new backend cannot skip the question.

Sources, in order:

  1. CrispASR kokoro, if the binary is available — local, no network
  2. Edge TTS, if installed — needs network
  3. A pre-rendered clip bundled in crisptts_assets/ — no backend, no model download, no network, no configuration

Tier 3 is why disclosure does not fail on an offline machine. It is a real spoken sentence in the right language, so it counts as a disclosure. All 27 languages ship a clip (~2 MB total in the wheel), so offline disclosure works in every one of them, not just German. Regenerate with python scripts/make_disclosure_assets.py after editing DISCLAIMER_TEXTS; a test fails if any language lacks a bundled clip.

Only if all three fail does CrispTTS fall back to a tone marker, which is refused: three beeps are an audible signal, not a disclosure a listener can understand. In that case the output is discarded rather than delivered, and --no-spoken-disclaimer --accept-marking-responsibility is the way to take the Art. 50(4) duty on explicitly.

EU AI Act: what this tool does, and what you must still do

Article 50 of Regulation (EU) 2024/1689 sits in Chapter IV and has applied since 2 August 2026 under Art. 113 — it is in force now, not upcoming. Article 4 (AI literacy, Chapter I) has applied since 2 February 2025. Releasing under an open-source licence does not exempt Article 50 — Art. 2(12) expressly carves it back in. Check the current consolidated text before relying on these dates; the Regulation has been subject to amendment proposals since adoption.

What CrispTTS does for you (provider-side, Art. 50(2)):

  • Marks every synthetic audio output in a machine-readable format
  • Signs WAV/MP3/FLAC/M4A output with a C2PA manifest by default, so the trainedAlgorithmicMedia claim is readable by any C2PA verifier and not only by Crisp tools. Measured, such a file reads back validation_state: Valid with the AI assertion intact; the manifest is self-signed, so a verifier additionally reports signingCredential.untrusted — see Certificate trust, which is about attribution rather than about marking
  • Fails closed rather than emitting unmarked audio
  • Reports honestly what was applied (MarkResult, server X-CrispTTS-* headers), and never presents a bundled-certificate signature as trusted
  • Gates voice cloning behind an attestation and logs it with a digest of the reference recording — and refuses to synthesize at all if that gate cannot be evaluated
  • Never plays audio to a listener before it has been marked and verified
  • Prepends a spoken AI disclosure to voice-cloned output — and to models whose preset voice is an identifiable person — in any of the 24 EU official languages, and refuses to deliver such audio without one

What remains your responsibility as the deployer (Art. 50(4)):

  • Disclosing that content is artificially generated where you publish it — a watermark is machine-readable, not a disclosure to the audience
  • Choosing the disclosure language. CrispTTS defaults to the model's declared language and warns when it cannot determine one, but only you know what language your audience speaks. Pass --disclosure-lang when using a multilingual model — a disclosure the audience cannot understand does not discharge the Art. 50(4) duty
  • Obtaining genuine consent from anyone whose voice you clone. The --i-have-rights flag is an unverified self-attestation; it records your claim, it does not establish a legal basis
  • Checking the voice/model licences you use (see the licensing section above)
  • GDPR: a cloned voice is personal data, and the consent audit log contains reference-audio paths — see Audit log retention below
  • Art. 50(5) accessibility. The spoken disclosure is audio, so it does not reach a deaf or hard-of-hearing audience. Art. 50(5) requires the disclosure to conform to the applicable accessibility requirements, so wherever you publish with captions, subtitles or a transcript, the disclosure sentence must be carried into them — it is the first thing in the audio, so a verbatim transcript already contains it. Do not strip it
  • Answering speaker_identity for models CrispTTS records as unknown. The warning names the model; if its preset voice is an identifiable person, pass --speaker-identity real_person
  • Art. 4 (AI literacy): ensuring the people operating this tool understand what it does and what its output is. In practice, for CrispTTS, that means whoever runs it should have read this section

Not applicable, having been checked: CrispTTS performs no biometric categorisation and no emotion recognition (Kartoffelbox's emotion control is emotion synthesis), so Art. 5 prohibited practices do not engage; it is not an Annex III high-risk system; and single-purpose TTS models are not general-purpose AI models, so Chapter V obligations do not attach to the models converted by convert_f5_to_mlx.py. Art. 50(1) (informing a person that they are interacting with an AI system) does not attach either: CrispTTS synthesizes audio on request and holds no conversation with anyone — if you embed it in something that does talk to people, that system is the one Art. 50(1) binds, and the duty is yours.

Certificate trust: the known limit of the C2PA layer

C2PA signing is a core dependency precisely because the audio watermark is readable only by Crisp tooling, and Art. 50(2) asks for marking that is interoperable. But the default signer is the bundled development certificate in c2pa_dev_cert.py. Any C2PA verifier will parse the manifest and read the trainedAlgorithmicMedia assertion; it will also report the signer as untrusted, because that certificate chains to nothing on the C2PA known-certificate trust list.

So the layer is interoperable in format and not in trust. CrispTTS does not paper over this — it warns once per run and MarkResult.c2pa_signer distinguishes self-signed from ca-issued.

What that does and does not cost you. Earlier revisions of this file called the self-signed certificate the largest remaining Art. 50(2) gap and told you to go and buy one. That was wrong, and measuring it says so. Reading a default-signed CrispTTS file back through c2pa-python:

validation_state : Valid
success          : claimSignature.validated, claimSignature.insideValidity,
                   assertion.hashedURI.match (x3), assertion.dataHash.match
failure          : signingCredential.untrusted
action           : c2pa.created
                   digitalSourceType: ...#trainedAlgorithmicMedia

The manifest validates, the signature verifies, the hashes match, and the AI-generation assertion is read out in full by any C2PA tool. The single failure is about the signer, not the marking.

Art. 50(2) requires outputs to be "marked in a machine-readable format and detectable as artificially generated". It does not require the mark to prove who generated it. That is attribution, and it is a different property — a valuable one, but not the one the article asks for. On the evidence above, the default configuration satisfies the marking duty.

Supply your own credential when you want attribution — a claim others can tie back to you, which matters for a publisher, a newsroom, or anyone whose provenance chain has to survive being contested:

crisptts --c2pa-cert /path/to/chain.pem --c2pa-key /path/to/key.pem ...
# or: C2PA_CERT_PATH=... C2PA_KEY_PATH=... crisptts ...

CrispASR ships a self-signed certificate too (assets/c2pa/, CN literally "CrispASR (AI-generated, self-signed)"), so this is the ecosystem's shared position rather than a CrispTTS shortcut.

Code of Practice on Transparency of AI-generated Content

The Commission facilitates a voluntary Code of Practice under Art. 50(7); its Section 1 covers provider-side marking and detection. Adherence is not mandatory, and CrispTTS is not a signatory, but signing is the route the Commission designates for demonstrating Art. 50(2) compliance "in a legally certain and predictable manner", so a deployer who needs that certainty should know where this tool stands against it.

The Code declines to mandate a single technique, on the express ground that no one technique satisfies all four of Art. 50(2)'s criteria — effective, interoperable, robust, reliable — and instead asks for layered solutions combining watermarking, metadata and content provenance. CrispTTS's three layers map onto that structure directly:

Code expectation CrispTTS Gap
Layered rather than single-technique marking Watermark + container metadata + C2PA manifest none
Marking applied to all generated output One mark_audio_file() path; fails closed none
Provenance metadata standard, not proprietary C2PA trainedAlgorithmicMedia signer untrusted by default (above)
Robust to common transformations Survives resample and 64 kbps MP3 (table above) built-in layer is Crisp-readable only; neural backend is an extra
Detection tooling available to third parties --detect-watermark FILE, and the C2PA manifest reads in any C2PA tool watermark detection needs Crisp tooling

The honest summary: the architecture is what the Code asks for, and the residual gap is about third parties being able to read the mark — a spread-spectrum watermark no one else implements. (The untrusted certificate was listed here as the second gap until v0.9.11, when measuring it showed the manifest validates and carries its AI assertion regardless; see Certificate trust above.)

Deliberately not signing. Adherence would commit this project to a fixed description of how it marks content, and the marking here is still moving — the detector was replaced twice in one week, the preferred neural backend changed, and the certificate question turned out to have been misread for five releases. Signing up to a public commitment while the implementation is changing underneath it produces a claim that goes stale, which is the failure mode this repository has spent most of its recent history correcting.

The Code stays a design target rather than a signature: the table above is the useful part, and it is cheaper to keep honest. Revisit when the marking layers stop changing.

Whether these obligations bind this project at all

Recorded because it has been assumed rather than analysed in every prior audit. Art. 50(2) binds providers. Under Art. 3(9)–(10) an obligation arises on placing a system on the Union market, and "making available" is defined as supply for distribution or use in the course of a commercial activity. A non-commercial FOSS project published on a code-hosting site has a real argument that it never crosses that line, and that the obligations attach instead to whoever deploys it commercially.

CrispTTS deliberately assumes the stricter reading — that it is a provider and Art. 50(2) applies — and implements accordingly. That is a defensive posture, not a legal conclusion, and it is the maintainer's own view rather than advice. If you are redistributing CrispTTS commercially, or running it as part of a service, the analysis is yours to make and you are much more clearly a provider than the upstream project is.

Timing. Art. 50 has applied since 2 August 2026. Systems already on the market before that date have until 2 December 2026 to meet the Art. 50(2) machine-readable marking requirement; CrispTTS predates the cutoff, so that grace period covers it. This is not a reason to defer — the marking is implemented — but it is the operative deadline.

Audit log retention

[CONSENT] and [MARKING] lines are written to ~/.cache/crisptts/consent_audit.log. That file records reference-audio paths, which routinely contain personal names, so it is personal data:

  • Created 0600, owner-only, rather than at the umask default

  • Entries older than 730 days are pruned automatically on every append (GDPR Art. 5(1)(e) storage limitation). Set CRISPTTS_CONSENT_LOG_RETENTION_DAYS to change the window, 0 to disable

  • --consent-log-prune prunes on demand

  • --consent-log-erase [SUBJECT] handles an Art. 17 erasure request: with a reference-audio path or ref_sha256 digest it removes only that speaker's lines; with no argument it erases the whole log

  • Hash-chained and anchored. Each line carries the SHA-256 of its predecessor, and the entry count plus head hash are mirrored into a sibling .anchor file. --consent-log-verify checks both. The log's whole purpose is to evidence that somebody attested a right to clone a voice, tied to a digest of the exact recording — a text file anyone can silently edit is weak evidence of that. The chain catches edits and deletions; the anchor catches truncation of the tail, which a chain cannot see on its own.

    This is tamper-evidence, not tamper-proofing: whoever can write the file can rebuild the chain. Ship the log somewhere append-only if you need more.

    Retention pruning and Art. 17 erasure both have to remove entries, which is exactly what the chain exists to detect. They are not exempted — they are recorded: the survivors are re-chained and a [CHAIN-REBUILT] line notes the reason and how many entries went. An unexplained gap is tampering; a gap with a rebuild record beside it is a documented erasure. The rebuild record deliberately does not name the subject, since it has to outlive the erasure it documents.

Lines with no parseable timestamp are kept — an unreadable record is not evidence that it has expired.

C2PA manifests signed with the bundled development certificate prove the file is unaltered since signing, but will not validate against C2PA trust lists. Supply --c2pa-cert / --c2pa-key for a credential others can verify.

This is a summary of how the implementation is intended to map onto the regulation, not legal advice.

Compliance comparison across the Crisp ecosystem

Feature CrispTTS CrispASR CrisperWeaver
Spread-spectrum watermark numpy (Python) C++ header-only Dart LSB + native FFI
WavMark neural watermark (MIT) Python (wavmark)
AudioSeal neural watermark Python + crispasr GGUF C++ ggml (GGUF) via CrispASR FFI
WAV LIST/INFO metadata ISFT + ICMT ISFT + ICMT ISFT + ICMT + IART + ICRD
MP3 ID3v2 tags TXXX (AI_GENERATED) TXXX (AI_GENERATED) TXXX (AI_GENERATED)
FLAC/Opus metadata Vorbis comments (mutagen)
C2PA content credentials c2pa-python by default; c2pa-audio / CrispASR as fast paths, each verified c2pa-c (compile-time)
Spoken AI disclaimer CrispASR kokoro / Edge TTS / bundled clips, 27 languages (all 24 EU official); refuses if unavailable Native TTS (cached) Beep marker
Voice-cloning consent gate CLI + API (403) CLI + server JSON GDPR Art. 9(2)(a) consent files
Consent audit logging stderr + consent_audit.log [CONSENT] stderr [CONSENT] log + .consent.json
Post-embed verification detect after save detect after save detect after embed
Watermark detection CLI --detect-watermark --detect-watermark detect in service
Cross-project detection Yes (shared PRNG key) Yes (shared PRNG key) Yes (via CrispASR FFI)

Marking-enforcement strength across the ecosystem

The layers above answer what is applied. This answers what happens when it doesn't work — which is where the projects genuinely differ:

Re-checked against the sibling repositories on 2026-08-03. The previous version of this table was stale and understated Susurrus on four rows — it had since gained fail-closed discard, non-WAV marking, a declarative floor and an attestation-gated opt-out. A comparison of other people's projects goes out of date silently and unfairly; re-read the code before trusting this.

Enforcement CrispTTS CrispASR Susurrus CrisperWeaver
Refuse before generating if unmarkable Yes
Marking verified after embedding Yes (gates)
Unmarkable output discarded Yes Yes No
Watermark floor (opt-out only if C2PA carries it) Yes Yes Yes (declarative) No
Opt-out requires marking attestation Yes Yes Yes No
Non-WAV outputs marked Yes Yes Yes (mp3/flac/m4a/opus)
Silent no-op on short audio Refused Yes (<4608 samples)
Playback marked+verified before it is heard Yes
Consent gate fails closed if unevaluable Yes
Disclosure in all 24 EU official languages Yes 2 locales
Audit-log retention limit + erasure command Yes
Hash-chained, anchored audit log Yes Yes (biometric events)
Detector reports its own statistical power p-band per backend p-value + 3-way verdict backend named

Where each idea came from, since none of this is one project's work: CrispASR contributed the watermark floor and the attestation gate, and independently found the detector's coin-flip null that Phase 28 fixed here — answering it with binomial p-values where CrispTTS replaced the statistic. Susurrus contributed the hash-chained, anchored audit log adopted above, and had the "fall back to spread-spectrum when the neural detector says no" rule right before CrispTTS did.

The gap CrispTTS still has alone: preflight refusal and verification-as-gate. That cuts both ways — because marking failure here deletes the output, a detector error is not a diagnostic inconvenience the way it is in CrispASR, where "embedding is unconditional and the watertight floor does not consult the detector".

Usage

# Default: spread-spectrum watermark + metadata (no extra deps)
python main.py --model-id edge --input-text "Hallo" --output-file out.mp3

# With the AudioSeal neural watermark (MIT, preferred — survives Opus and MP3)
pip install 'crisptts[robust]'
python main.py --model-id edge --input-text "Hallo" --output-file out.mp3

# With C2PA content credentials
pip install c2pa-python
python main.py --c2pa-cert cert.pem --c2pa-key key.pem --model-id edge --input-text "Hallo" --output-file out.mp3

# Voice-cloning models require consent attestation (spoken disclaimer auto-prepended)
python main.py --model-id coqui_xtts_v2_de_clone --i-have-rights --input-text "Hallo" --output-file out.wav

# Multilingual cloning model: say which language the disclosure should be in,
# because the model config cannot know what language your text is
python main.py --model-id crispasr_cosyvoice3_tts --i-have-rights \
  --disclosure-lang zh --input-text "你好" --output-file out.wav

# What languages can the spoken disclosure be in?
python main.py --list-disclosure-langs

# Detect watermark in existing audio
python main.py --detect-watermark out.wav

# GDPR housekeeping on the consent audit log
python main.py --consent-log-prune                 # drop entries past retention
python main.py --consent-log-erase /refs/alice.wav # Art. 17, one speaker
python main.py --consent-log-erase                 # Art. 17, everything

# Disable ALL marking layers (debug only — you take on the Art. 50 responsibility)
python main.py --no-watermark --model-id edge --input-text "Hallo" --output-file out.mp3

# Keep output even if marking fails (default is to discard it and exit non-zero)
python main.py --allow-unmarked --model-id edge --input-text "Hallo" --output-file out.mp3

Detection (Python API)

from watermark import watermark_detect
import soundfile as sf

pcm, sr = sf.read("out.wav", dtype="float32")
confidence = watermark_detect(pcm, sample_rate=sr)
print(f"Watermark confidence: {confidence:.3f}")  # >0.65 = AI-generated

Cross-compatibility

The spread-spectrum watermark uses the same PRNG seed (0x437269737041535F), FFT parameters, and bin selection as CrispASR's C++ implementation and CrisperWeaver's native FFI path. Audio watermarked by any project in the ecosystem can be detected by the others.

API Server

CrispTTS includes an OpenAI-compatible HTTP server for integration with applications that use the OpenAI TTS SDK.

# Start the server
python main.py --server --server-port 8880

# Or run directly
python server.py --host 0.0.0.0 --port 8880

Endpoints

Method Path Description
POST /v1/audio/speech Synthesize audio (OpenAI-compatible)
GET /v1/audio/models List available models and voices
GET /health Health check

Request format (POST /v1/audio/speech)

{
  "model": "crispasr_kokoro",
  "input": "Hallo, wie geht es Ihnen?",
  "voice": "af_heart",
  "response_format": "wav",
  "speed": 1.0,
  "i_have_rights": false
}

The i_have_rights field is required (and must be true) for voice-cloning models. Omit it or set to false for non-cloning models.

Response: audio bytes with Content-Type and Content-Disposition: attachment headers. Features:

  • All output marked, in every response format. Provenance headers report what was actually applied — X-CrispTTS-Watermarked is true only when marking really happened, alongside X-CrispTTS-Watermark-Backend, X-CrispTTS-Watermark-Confidence and X-CrispTTS-Provenance-Layers
  • A response that cannot be marked is a 500, never unmarked audio
  • Voice-cloning models return 403 unless i_have_rights is set. The consent gate runs before the cache lookup, so cached audio cannot bypass it
  • Concurrent requests handled via threaded server
  • Rate limiting: 10 requests/minute/IP (configurable via --rate-limit)
  • Synthesis caching: identical requests served from cache (X-CrispTTS-Cache: hit). Cache keys include the marking mode, so unmarked audio can never be served to a marking-enabled request
  • Enhanced /health: reports loaded handlers, memory RSS, registered backends

Troubleshooting & Notes

espeak-ng for Kokoro: The Kokoro backend requires espeak-ng for phonemization. Install via:

pip install py-espeak-ng     # installs espeak-ng CLI to ~/.local/bin
# or system-wide: apt install espeak-ng

CrispASR voice paths: The CrispASR binary auto-downloads models but voice packs need full paths for older binary versions. Use the cached path directly:

python main.py --model-id crispasr_kokoro \
  --german-voice-id ~/.cache/crispasr/kokoro-voice-af_heart.gguf \
  --input-text "Test" --output-file out.wav

Missing Libraries: If a specific TTS engine fails, ensure you have installed all its required libraries via pip install -r requirements.txt and any extra steps mentioned in their documentation.

mlx-audio Bark Specifics:

  • This handler currently requires the main MLX model to be from a repository like mlx-community/bark-small (which should provide MLX-compatible .safetensors or model files)
  • The voice prompts (speaker embeddings) are fetched from suno/bark-small by default (due to an included monkey patch in mlx_audio_handler.py) which has a comprehensive set of speaker prompts as separate .npy files. This dual-source setup is necessary because mlx-community/bark-small has limited voice prompt files in the required format
  • If mlx-audio's load_model function reports "No safetensors found" for the main mlx_model_path, you may need to convert the target Bark model to MLX format using python -m mlx_audio.tts.convert and point mlx_model_path to the local converted directory. The voice prompt patch in the handler is designed to work with either an HF repo ID or a local path for mlx_model_path when determining how to fetch/locate the .npy prompts from suno/bark-small or a speaker_embeddings subfolder

API Keys/Servers: API-based models require the respective servers (LM Studio, Ollama) to be running and accessible.

Model Downloads: First-time use of a model that needs to be downloaded from Hugging Face Hub might take some time. Ensure you have an internet connection. Set HF_TOKEN for gated models.

Verbose Output: Use --loglevel DEBUG for detailed diagnostic information if you encounter issues.

RAM Usage: Local GGUF and large Transformer models can be memory-intensive. Ensure your system has sufficient RAM.

Paths: When providing paths for --input-file, --output-file, or speaker WAV files (--german-voice-id), use appropriate relative or absolute paths.

About

python command-line Text-to-Speech (TTS) tool esp. for German, leveraging numerous endpoints like orpheus, piper, outetts, kokoro, csm, edge, coqui, kartoffelbox, etc

Topics

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages