--- language: - tr license: apache-2.0 library_name: voxcpm pipeline_tag: text-to-speech base_model: openbmb/VoxCPM2 base_model_relation: finetune tags: - turkish - voxcpm2 - merged-lora - speech-synthesis - audio --- # Kizagan-TTS-v1.0 Kızagan is a Turkish text-to-speech adaptation of [OpenBMB's VoxCPM2](https://huggingface.co/openbmb/VoxCPM2), released by Alican Kiraz. It produces mono **48 kHz** speech and includes a reference-conditioned inference script for explicit sentence-by-sentence generation. **v1.0 packages the already evaluated, merged Step 1500 checkpoint.** No additional fine-tuning or weight changes were performed for this release. The release adds a documented inference recipe, runnable commands, and source-derived evaluation results. ## Model and training provenance | Item | Value | |---|---| | Base model | [openbmb/VoxCPM2](https://huggingface.co/openbmb/VoxCPM2) | | Fine-tuned checkpoint | [AlicanKiraz0/Kizagan-TTS-step_1500](https://huggingface.co/AlicanKiraz0/Kizagan-TTS-step_1500), revision `458112a0fc3d850de38951796290e48ef710bc26` | | Dataset | [AlicanKiraz0/TR-Voice-TTS](https://huggingface.co/datasets/AlicanKiraz0/TR-Voice-TTS), private Turkish single-speaker corpus | | Adaptation | LoRA, rank 64, alpha 64, LM + DiT attention projections | | Checkpoint step | 1500 | | Release format | Full merged weights; load directly without applying the old adapter again | | Inference dtype / output | BF16 / mono 48 kHz | The original [merge manifest](merge_manifest.json) records the base snapshot and merge settings. The original training input manifest and optimizer state were not recovered during this evaluation, so this card does not claim an independently reproduced training run. Dataset recordings and raw listener response files are not redistributed in this model repository. ## Install and download The supplied command targets Linux with an NVIDIA CUDA GPU and Python 3.12. Install a CUDA-enabled PyTorch/torchaudio pair appropriate for your driver; the following uses the official CUDA 13.0 wheel index. VoxCPM is pinned to the source revision used in our evaluations. ```bash python3.12 -m venv .venv source .venv/bin/activate python -m pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu130 python -m pip install "voxcpm @ git+https://github.com/OpenBMB/VoxCPM.git@f772e498a45fbb5fb8e13fbf9b9c48be9fe33e69" "huggingface_hub==1.30.0" # Authenticate with an account that can access the repository if it is private. hf auth login hf download AlicanKiraz0/Kizagan-TTS-v1.0 --local-dir ./Kizagan-TTS-v1.0 ``` The benchmark software versions below describe the existing measured environment; these installation commands do not freeze every transitive dependency. Timing and output can vary across hardware, drivers and package versions. The packaged inference command is validated separately on the recorded CUDA environment. ## Inference Supply a clean, mono reference WAV for the intended voice. Each non-empty line in a sentences file is one complete utterance; prepare the boundaries yourself. The script preserves the line order and uses the same reference afresh for every line. ```bash # Two short utterances. Replace speaker.wav with your reference recording. python ./Kizagan-TTS-v1.0/inference.py \ --model ./Kizagan-TTS-v1.0 \ --reference ./speaker.wav \ --sentences-file ./Kizagan-TTS-v1.0/examples/quickstart.sentences.txt \ --output-dir ./Kizagan-quickstart # A single short utterance. python ./Kizagan-TTS-v1.0/inference.py \ --model ./Kizagan-TTS-v1.0 \ --reference ./speaker.wav \ --text "Merhaba, bugün sana nasıl yardımcı olabilirim?" \ --output-dir ./Kizagan-single ``` The packaged command passed a two-utterance CUDA smoke check on 7 September 2026: **3.36 seconds of 48 kHz audio**, no length-cap failure. [Listen to that generated sample](examples/quickstart.wav) or inspect the [validation record](evaluation/release_smoke.json). This verifies the command; it is separate from the older development benchmark below. Each command writes `audio.wav`, individual `sentence_*.wav` files and `metrics.json`. Use a new output directory for each run. The script fixes CFG `2.0`, `16` diffusion steps and seed `42`, disables compilation/denoising/automatic retries, and concatenates the generated PCM without adding silence, trimming, crossfading or changing speed. It loads the model once and builds a fresh reference cache for each utterance. `--text` is one call: it does not automatically split a paragraph. For long answers, use a sentences file. The script does not automatically resolve Turkish abbreviations, numbers or ambiguous sentence boundaries. Spell out numbers as they should be spoken and keep each line a natural utterance. ## Voice cloning guide **Yes: Kızagan accepts a voice reference and generates new text conditioned on that recording.** This happens during inference; supplying a new recording does not train another adapter or change the model weights. A transcript is optional: the packaged `inference.py` uses reference audio alone. Kızagan was fine-tuned on a Turkish single-speaker corpus. Its recorded listening evaluation used that speaker, so it does **not** establish cloning similarity for unfamiliar speakers, accents or languages. The API can accept another speaker's recording, but speaker similarity, style adherence and intelligibility need listening checks with that recording. | Capability | How to use it | Evidence for this release | |---|---|---| | Reference-only voice cloning | `inference.py --reference` or Python `reference_wav_path` | Packaged command tested; limited same-speaker listening results below | | Long text with the same reference | `--sentences-file`, one utterance per line | Preferred in the two recorded long-text listening trials | | Reference plus its transcript | Python `reference_wav_path` + `prompt_wav_path` + `prompt_text` | Inherited VoxCPM2 API; separate functional examples below | | Emotion/pace guidance with a reference | A parenthesized instruction at the start of Python `text` | Inherited control format; style accuracy not evaluated | | Voice design without a reference | A voice description at the start of Python `text` | Inherited control format; designed-voice quality not evaluated | | TTS without a reference | Python `generate(text=...)` | Inherited API; default-voice quality not evaluated | | Stream audio chunks | Python `generate_streaming(...)` | Python yields chunks; packaged CLI saves completed WAV files | ### 1. Prepare a reference recording Use your own voice or a recording you have permission to use. Choose one speaker, a quiet room, clear pronunciation and a natural speaking style. Avoid background music, overlapping speakers, clipped peaks and long stretches of silence. Cut at word boundaries and keep the beginning and end of the spoken phrase intact. Start with a short, representative utterance. Our measured reference was **6.175 seconds**; this is an evaluated example length, not a minimum or maximum accepted by the API. A longer recording is not automatically a better reference and consumes more model context. The style in the recording can influence the result as well as the voice identity. The packaged CLI requires **non-empty, finite, mono audio**. A mono WAV is the simplest input. VoxCPM2 internally resamples the input to **16 kHz**; the output is **48 kHz**. The measured 24 kHz reference therefore did not require manual conversion to 48 kHz. If FFmpeg is installed, this optional conversion turns a phone recording into a mono 16 kHz PCM WAV: ```bash ffmpeg -i ./my-recording.m4a -ac 1 -ar 16000 -c:a pcm_s16le ./speaker.wav ``` Conversion does not remove noise or repair clipping. Listen to the converted file before using it. Neither the supplied CLI nor the Python examples below enable denoising. ### 2. Clone the voice without a transcript Run this after the installation and model download above: ```bash python ./Kizagan-TTS-v1.0/inference.py \ --model ./Kizagan-TTS-v1.0 \ --reference ./speaker.wav \ --text "Merhaba, yeni bir metni referans sesle okuyorum." \ --output-dir ./Kizagan-voice-clone ``` Listen to `./Kizagan-voice-clone/audio.wav`. The reference may contain different words from `--text`: it supplies voice conditioning, while `--text` supplies the new words to synthesize. No `speaker.txt`, speaker ID, speaker embedding export or additional fine-tuning is required for this mode. To try another voice, pass its recording to `--reference` and choose a new output directory. Compare the outputs using the same short text before moving to a longer passage. This is voice-conditioned TTS, not a speech-to-speech converter: it generates speech from the supplied text rather than replacing the voice in an existing recording. ### 3. Keep the reference consistent across a long text Create a UTF-8 file with one complete utterance on each line: ```bash cat > ./my-sentences.txt <<'TXT' Merhaba, bugün birlikte kısa bir yolculuğa çıkacağız. Önce planımızı gözden geçirelim. Ardından her adımı sakin ve anlaşılır biçimde anlatalım. TXT python ./Kizagan-TTS-v1.0/inference.py \ --model ./Kizagan-TTS-v1.0 \ --reference ./speaker.wav \ --sentences-file ./my-sentences.txt \ --output-dir ./Kizagan-long-clone ``` Every line starts with a fresh cache from the **same original reference**. Generated sentences are not fed back as the next sentence's voice prompt. This is the evaluated long-text recipe. It can still produce imperfect sentence joins; the script adds no pauses or crossfades. Listen to transitions as well as individual sentences. ## Advanced Python examples These use the pinned [VoxCPM Python API](https://github.com/OpenBMB/VoxCPM/blob/f772e498a45fbb5fb8e13fbf9b9c48be9fe33e69/src/voxcpm/core.py), with **Kızagan's merged weights**. They do not require reapplying the old LoRA. Parameters such as `reference_wav_path` and `prompt_text` are Python arguments, not additional flags accepted by the packaged `inference.py`. Run this setup once, then run the examples you want in the **same Python session or script**, from the directory containing `Kizagan-TTS-v1.0` and your reference files. Use a new output folder when starting another run. ```python from pathlib import Path import soundfile as sf from voxcpm import VoxCPM out = Path("Kizagan-python-examples") out.mkdir(exist_ok=False) model = VoxCPM.from_pretrained( "./Kizagan-TTS-v1.0", device="cuda", load_denoiser=False, optimize=False, ) sample_rate = model.tts_model.sample_rate settings = dict( cfg_value=2.0, inference_timesteps=16, seed=42, min_len=2, max_len=1024, normalize=False, denoise=False, retry_badcase=False, ) ``` The examples keep the release's CFG, diffusion-step count and seed. The high-level API examples are separate from the packaged command's measured streaming recipe: matching these settings does not imply identical PCM or timing. Keep targets short for initial checks. The Python API returns waveforms; it does not create the wrapper's `metrics.json` or provide its length-cap error report. ### Reference-only cloning in Python ```python wav = model.generate( text="Merhaba, yeni bir metni referans sesle okuyorum.", reference_wav_path="./speaker.wav", **settings, ) sf.write(out / "reference-clone.wav", wav, sample_rate, subtype="FLOAT") ``` ### Cloning with the reference's exact transcript Create `speaker.txt` in UTF-8 containing the **words actually spoken in `speaker.wav`**, in their original order. This is the reference transcript, not the new target sentence. Do not paste example words unless they match your recording. ```python reference_transcript = " ".join( Path("speaker.txt").read_text(encoding="utf-8").split() ) if not reference_transcript: raise ValueError("speaker.txt must contain the reference recording's transcript") wav = model.generate( text="Şimdi yeni bir cümleyle devam ediyorum.", reference_wav_path="./speaker.wav", prompt_wav_path="./speaker.wav", prompt_text=reference_transcript + " ", **settings, ) sf.write(out / "transcript-clone.wav", wav, sample_rate, subtype="FLOAT") ``` This combines a voice reference with audio/text continuation conditioning. The upstream documentation calls this **Ultimate Cloning**. Both audio arguments deliberately point to the same recording. `prompt_wav_path` and `prompt_text` must always be supplied together; the generated WAV contains the new output, not a file concatenation of the reference recording and the output. The trailing space after `reference_transcript` preserves the boundary before the new target text: the pinned implementation concatenates prompt text and target text directly. The API also accepts the audio/transcript pair without `reference_wav_path` for continuation-only conditioning. That variation is not part of our example validation. Transcript-conditioned generation is a different mode from the evaluated long-text reset recipe; higher similarity is not guaranteed for Kızagan. Keep style instructions out of the reference transcript and do not combine the transcript mode with the style-control recipe below. ### Guide emotion or pace while keeping a reference ```python wav = model.generate( text="(Calm, warm tone, speaking slowly)Merhaba, birlikte sakin bir başlangıç yapalım.", reference_wav_path="./speaker.wav", **settings, ) sf.write(out / "controlled-clone.wav", wav, sample_rate, subtype="FLOAT") ``` The parenthesized prefix is the upstream format for a natural-language style instruction. It is not a `speed` multiplier or a guaranteed emotion label. Here the instruction is in English and the target speech is Turkish. Whether the requested pace and emotion are expressed correctly must be checked by listening; Kızagan has not been separately evaluated for instruction adherence. ### Design a voice without reference audio ```python wav = model.generate( text="(An adult man with a warm, gentle voice)Merhaba, bugün sana nasıl yardımcı olabilirim?", **settings, ) sf.write(out / "voice-design.wav", wav, sample_rate, subtype="FLOAT") ``` This requests a voice through a description. It does not clone a particular person. Single-speaker fine-tuning may constrain the variety retained from the base model, so this example is an API usage demonstration rather than evidence of broad voice-design quality. For ordinary TTS without a reference or voice description: ```python wav = model.generate( text="Merhaba, bugün sana nasıl yardımcı olabilirim?", **settings, ) sf.write(out / "no-reference.wav", wav, sample_rate, subtype="FLOAT") ``` ### Consume streaming audio chunks ```python import numpy as np chunks = [] stream = model.generate_streaming( text="Merhaba, bu ses küçük parçalar halinde üretiliyor.", reference_wav_path="./speaker.wav", **settings, ) try: for chunk in stream: # A mono float32 NumPy array on CPU, at sample_rate Hz. # An application can send each chunk to its audio playback queue here. chunks.append(chunk) finally: stream.close() if not chunks: raise RuntimeError("The model returned no audio chunks") sf.write(out / "streaming-clone.wav", np.concatenate(chunks), sample_rate, subtype="FLOAT") ``` This example collects chunks into a WAV. It demonstrates the generator interface; it does not implement a live player, HTTP endpoint or streaming server. The packaged `inference.py` also streams internally, but its user-facing outputs are files. ### Troubleshooting and validation boundaries | Symptom | Check | |---|---| | `--reference must contain finite, non-empty mono audio` | Convert stereo to mono; check that the file contains readable samples. | | `--output-dir already exists` | Use a new directory; the packaged command preserves existing outputs. | | `prompt_wav_path and prompt_text must both be provided` | Supply both the recording and its transcript, or use reference-only mode. | | Voice differs from the intended speaker | Check reference quality and compare short targets first; unfamiliar-speaker similarity has not been established for this fine-tune. | | Output drifts toward the end of a paragraph | Use explicit sentence lines with the same original reference for every line. | | A style instruction is ineffective or spoken aloud | Remove the prefix and compare reference-only generation; instruction adherence is unmeasured. | | Transcript-conditioned output repeats or omits words | Check the reference transcript and phrase boundary; compare against reference-only mode. | | A generation is cut short | Inspect the packaged command's `metrics.json` and shorten the utterance/reference context. API examples do not implement that cap check. | On **7 September 2026**, all six generation examples above produced finite, non-empty, mono **48 kHz** audio on the recorded RTX PRO 6000 environment, using Kızagan weights from release commit `90d34bee01d7ffaf3d7cffce3fbabdb8f3329dde`. For this functional check, the published synthetic quickstart audio and its source text supplied the reference and transcript. The revised transcript example also passed with a transcript lacking terminal punctuation, verifying the explicit word boundary. The streaming example yielded multiple chunks whose concatenation matched the saved waveform. These are execution checks, not listening tests of the advanced modes. API availability, successful waveform generation and perceived cloning quality are different checks. The recorded human ratings below apply to the original same-speaker reference recipe, not to new voices, transcript conditioning, voice design, style control or other languages. The published quickstart WAV is generated speech, not a replacement for a clean recording of the voice you want to use. ## Measured inference examples Measurements below come from the fixed **5 September 2026** development run on an **NVIDIA RTX PRO 6000 Blackwell Workstation Edition, 96 GB**, CUDA BF16, CFG 2.0, 16 diffusion steps, seed 42 and the same reference clip. They are observations from two texts, not a throughput SLA. | Example | Sentences | Generated audio | Sum of call times | Aggregate RTF | Peak PyTorch reserved | |---|---:|---:|---:|---:|---:| | Library, 151 words | 12 | 68.16 s | 24.521 s | 0.360 | 6,114 MiB | | Train platform, 149 words | 13 | 66.72 s | 24.042 s | 0.360 | 6,114 MiB | `Aggregate RTF = sum(call elapsed seconds) / sum(audio seconds)`. Calls include reference-cache preparation and generation; these totals exclude model loading, file I/O, gaps between calls and final assembly. Peak reserved memory is a PyTorch allocator measurement, not total GPU process memory. First-chunk measurements and whole-paragraph comparisons are in [evaluation/README.md](evaluation/README.md); the first whole-paragraph call was cold relative to later calls, so their first-chunk timings are not an equal-warmup comparison. Run the same texts and record your own measurements: ```bash python ./Kizagan-TTS-v1.0/inference.py \ --model ./Kizagan-TTS-v1.0 \ --reference ./speaker.wav \ --sentences-file ./Kizagan-TTS-v1.0/examples/library.sentences.txt \ --output-dir ./Kizagan-library python ./Kizagan-TTS-v1.0/inference.py \ --model ./Kizagan-TTS-v1.0 \ --reference ./speaker.wav \ --sentences-file ./Kizagan-TTS-v1.0/examples/platform.sentences.txt \ --output-dir ./Kizagan-platform ``` In the new script, `call_elapsed_s` and `call_rtf` include fresh reference-cache preparation through generator completion; `generation_elapsed_s` and `rtf` cover generation only. Download/path resolution and model load are reported separately. These are generation measurements, not network-serving latency. The measured reference was `r2`, a **6.175 s**, mono **24 kHz** validation recording, SHA-256 `5f36097c430ae461dc55d76ebe4ef897442e5a34f19c48cfdb9e909afd85c18d`. It is not included because it belongs to the private dataset. Using your own `speaker.wav` reproduces the procedure, not the exact benchmark conditions or voice. Audio durations and results may differ. | Recorded environment | Version | |---|---| | Python | 3.12.13 | | PyTorch | 2.14.0+cu130 | | torchaudio | 2.11.0+cu130 | | transformers | 5.16.1 | | VoxCPM source | `f772e498a45fbb5fb8e13fbf9b9c48be9fe33e69` | | NumPy / SoundFile | 2.5.2 / 0.14.0 | Detailed measurements, error counts and source hashes are in [evaluation/metrics.json](evaluation/metrics.json). ## Listening results and limits One Turkish listener compared sentence-by-sentence and whole-paragraph generation on two new development texts, with balanced A/B positions and matched listening loudness: | Example | Sentence-by-sentence naturalness / similarity | Whole-paragraph naturalness / similarity | Preference | |---|---|---|---| | Library | 5/5 and 5/5 | 1/5 and 1/5 | Sentence-by-sentence | | Train platform | 4/5 and 5/5 | 1/5 and 1/5 | Sentence-by-sentence | The listener reported deterioration toward the end of both whole-paragraph outputs. The second sentence-by-sentence artifact checkbox was unanswered, so it is not labelled artifact-free. A separate earlier development paragraph also favored sentence-by-sentence generation; it is not pooled into these two trials. These observations support the supplied recipe for development use, not population MOS, an isolated causal explanation or guaranteed quality on arbitrary text. Fixed ASR word-error counts for whole-paragraph / sentence-by-sentence audio were **1/151 / 0/151** on the library text and **2/149 / 2/149** on the platform text. ASR differences include orthographic and recognition ambiguity; they are not substitutes for listening or confirmed pronunciation mistakes. This release does not establish Step 1500 as universally better than Step 2000, nor does it claim that an earlier final-evaluation gate was passed by the new sentence-based method. Evaluate your own text, reference and serving environment before depending on its behavior. ## Acknowledgments and attribution A heartfelt thank you to the **OpenBMB team and the VoxCPM contributors** for making VoxCPM2, its weights, training tools and implementation available. Kızagan builds directly on their work; this Turkish adaptation and its evaluation would not have been possible without that foundation. **OpenBMB ekibine ve VoxCPM2'ye emek veren tüm araştırmacılara çok teşekkür ederiz.** - Base model: [openbmb/VoxCPM2](https://huggingface.co/openbmb/VoxCPM2) - Upstream implementation: [OpenBMB/VoxCPM](https://github.com/OpenBMB/VoxCPM) - Technical report: [Zhou et al., VoxCPM2 Technical Report (2026)](https://arxiv.org/abs/2606.06928) - Earlier work: [VoxCPM (2025)](https://arxiv.org/abs/2509.24650) This is an independent community adaptation; OpenBMB authorship applies to the foundation model and upstream implementation. See [LICENSE](LICENSE) and [NOTICE](NOTICE) for preserved licensing and attribution.