Official code release for the EMNLP 2026 paper
βEmoTra-TTS: Smooth Intra-Utterance Emotion Transitions for Speech Synthesisβ
EmoTra-TTS is a two-stage fine-tuning framework built on CosyVoice 2 that enables continuous emotion control in speech synthesis using ValenceβArousalβDominance (VAD) representations. Instead of discrete emotion labels, it takes 3D VAD vectors for the start and end emotional states and synthesizes speech that transitions smoothly between them β all within a single utterance.
Patent Notice. The core method of EmoTra-TTS has been submitted for patent protection. Please see the License section for details.
- Aug 2026 β π The EmoTra-TTS paper is now available on arXiv:2608.23791.
- Aug 2026 β π EmoTra-TTS is accepted to EMNLP 2026.
- Aug 2026 β π Public code release: full data-prep, training, inference, and evaluation pipelines.
- Aug 2026 β π§ The demo page is live β listen to emotion-transition samples.
EmoTra-TTS adds emotion conditioning to CosyVoice 2 via two-stage supervised fine-tuning:
| Stage | Module | Base Class | What it learns |
|---|---|---|---|
| Stage I: sft_LLM | LLM with VAD conditioning | Qwen2LM |
5 VAD tokens (uniform alpha interpolation from startβend emotion) injected into the LM input sequence |
| Stage II: sft_Flow | Flow Matching with emotion injection | CausalMaskedDiffWithXvec |
Direction-Magnitude Decoupled emotion injection β MLP learns the direction, LayerNorm + fixed_scale set the magnitude (~280K trainable params, decoder frozen) |
Naming note. In the code and file names this method carries the internal codename
sft_Flow_Lynorm_fixs(Layernorm + fixed scale); it refers to exactly the Direction-Magnitude Decoupled method in our paper.
- Linux (tested on Ubuntu)
- NVIDIA GPU(s) with CUDA support
- Conda package manager
This repository does not redistribute third-party code. The third_party/
directory ships empty (aside from the Matcha-TTS
submodule pointer); every external dependency is fetched from its original upstream,
under its own license, by a setup script.
Clone the repository (with the Matcha-TTS submodule):
git clone --recurse-submodules https://github.com/Liu-Tianchi/EmoTra-TTS.git
cd EmoTra-TTS
# already cloned without submodules? one line pulls it in:
git submodule update --init --recursiveThen fetch the remaining third-party dependencies (Matcha-TTS submodule, plus the UniSpeech / s3prl / WavLM components used for SIM evaluation):
bash scripts/setup_third_party.shYou only need the evaluation dependencies if you plan to run SIM evaluation. The w2v2-vad model used by Step 1 downloads itself automatically on first use.
Two conda environments are required:
1. EmoTra (main environment) β for training and inference:
bash install_env.sh
conda activate EmoTra
install_env.shis a thin wrapper aroundconda env create -f environment_emotra.yml. It sets a pip build constraint (constraints.txt,setuptools<81) that is required to buildopenai-whisper==20231117. If you prefer to runconda env createdirectly, firstexport PIP_CONSTRAINT=constraints.txt.
2. w2v2-vad β for Step 1 VAD filtering only:
conda env create -f environment_w2v2_vad.yml
conda activate w2v2-vadDownload the CosyVoice2-0.5B pretrained model. The local_dir sets where the
weights are saved β the default pretrained_models/ lives inside the repo and
is git-ignored (models are never committed). Point it elsewhere if you prefer:
conda activate EmoTra
python -c "from modelscope import snapshot_download; snapshot_download('iic/CosyVoice2-0.5B', local_dir='pretrained_models/CosyVoice2-0.5B')"Step 0: Download EmoVoice-DB dataset
β
Step 1: Filter by VAD scores (uses w2v2-vad env)
β
Step 3: Generate transition pairs with ASR quality check (multi-GPU)
β
Stage I: Train sft_LLM (LLM fine-tuning with VAD tokens)
β
Stage II: Train sft_Flow (Flow fine-tuning with Direction-Magnitude Decoupled injection)
β
Inference: Synthesize speech with continuous emotion transition
Note: Step 2 is intentionally skipped β Step 3 reads directly from Step 1 output.
conda activate EmoTra
python step0_download_emovoice.py
python step0_prepare_dataset.pyBy default the dataset is saved to data/EmoVoice-DB inside the repo (the data/
directory is git-ignored, so datasets are never committed). Use --data_dir to
save it anywhere else:
python step0_download_emovoice.py --data_dir /path/to/datasets
python step0_prepare_dataset.py --data_dir /path/to/datasetsThis step uses the w2v2-vad environment to predict VAD scores and keep only the
samples whose VAD falls in the expected range for their emotion label.
conda activate w2v2-vad
python step1_filter_by_vad.pyOn the first run the w2v2-vad model (1 GB) is automatically downloaded from
Zenodo to third_party/models/w2v2-vad (inside the repository; git-ignored, so it
is never uploaded). Inference runs on CPU (onnxruntime), so no GPU is required for
this step.
By default it reads data/EmoVoice-DB/train.jsonl and writes one file per speaker
to data/filtered/<speaker>_filtered.jsonl (speakers are auto-detected from the
sample keys), plus data/filtered/filtering_stats.json.
Useful options:
# Quick test on 500 randomly sampled utterances
python step1_filter_by_vad.py --debug-limit 500
# Custom input / output locations
python step1_filter_by_vad.py \
--jsonl data/EmoVoice-DB/train.jsonl \
--audio-dir data/EmoVoice-DB/audio \
--output-dir data/filteredMulti-GPU parallel processing with Whisper-based ASR quality verification:
conda activate EmoTra
python step2_generate_transition_data_w_ASR_check.py \
--input-jsonl data/filtered/sage_filtered.jsonl \
--output-dir data/transition_data_asr/sage \
--model-path pretrained_models/CosyVoice2-0.5B \
--gpu-ids 0,1,2,3,4,5,6,7 \
--num-pairs 50000 \
--language enThis step is GPU-heavy (CosyVoice2 synthesis + Whisper ASR). It launches one
worker process per GPU listed in --gpu-ids, so set --gpu-ids to match the
GPUs available to the job (e.g. --gpu-ids 0,1 for 2 GPUs). Use more GPUs to
speed it up.
On first run, each worker loads the Whisper large-v3 model (~2.9 GB). To avoid
all workers downloading it simultaneously, pre-fetch it once into the shared
cache (~/.cache/whisper) before launching:
conda activate EmoTra
python -c "import whisper; whisper.load_model('large-v3')"Output: data/transition_data_asr/sage/transition_data_asr_filtered.jsonl
Speaker choice. EmoVoice-DB ships two speakers:
sage(female, the default used throughout these examples) andash(male). To build the male voice instead, replace everysagewithashβ in the--input-jsonl/--output-dirabove and in the training scripts (dataset_name,neutral_audio, and the matchingstep3_output_dir). Both speakers are fully supported.
All training scripts are in examples/libritts/cosyvoice2/.
cd examples/libritts/cosyvoice2Fine-tunes the LLM to accept 5 VAD tokens (uniform alpha: 0.0, 0.25, 0.5, 0.75, 1.0) representing the emotion trajectory from start to end.
# Stages 1-4: Data preparation (JSONL β Kaldi β embeddings β tokens β parquet)
bash run_EmoTra_sft_LLM.sh # set stage=1, stop_stage=4
# Stage 5: Train LLM
bash run_EmoTra_sft_LLM.sh # set stage=5, stop_stage=5Key hyperparameters in the script:
hidden_loss_weight=0.1β weight for hidden state reconstruction lossnum_vad_tokens=5β number of interpolated VAD tokensvad_change_threshold=0.35β minimum VAD change to qualify as a transition pair
Checkpoints are saved per epoch as epoch_N_whole.pt under
exp/cosyvoice2_EmoTra_sft_LLM/llm/torch_ddp/<run_name>/.
Selecting a checkpoint. The log reports validation loss and acc per epoch, but
these objective metrics don't always reflect perceived quality. Synthesize a few utterances with your top candidates and listen before deciding.
Fine-tunes only the Direction-Magnitude Decoupled emotion injection module (MLP + LayerNorm + fixed_scale, ~280K params) while keeping the Flow decoder frozen.
Prerequisite: Stage I must be completed (the sft_LLM checkpoint is needed).
# Stages 1-3: Extract neutral embedding β pre-compute LLM tokens β train Flow
bash run_EmoTra_sft_Flow_Lynorm_fixs.sh # set stage=1, stop_stage=3Like the sft_LLM script, options are configured by editing variables at the top of the script rather than CLI flags. Before running, set at least:
llm_checkpointβ the sft_LLM checkpoint you selected in Stage Idataset_nameβ speaker/dataset tag (must match your Stage I data dir;sage= female,ash= male)neutral_audioβ a neutral-emotion reference clip for that speaker
# Stage I
tensorboard --logdir=tensorboard/cosyvoice2_EmoTra_sft_LLM --port=6006 --bind_all
# Stage II
tensorboard --logdir=tensorboard/cosyvoice2_EmoTra_sft_Flow_Lynorm_fixs --port=6015 --bind_allThe script ships with a built-in default text, VAD start/end, and emo_scale, so you can run it with no arguments:
python simple_inference_EmoTra_TTS.pyTo override any of them (the values below are the defaults):
python simple_inference_EmoTra_TTS.py \
--text "The weather was so bad that day, and I was sadly walking home when I suddenly saw something that made me extremely angry" \
--vad_start 0.25,0.27,0.31 \
--vad_end 0.20,0.85,0.86 \
--emo_scale 0.07Where:
--textβ text to synthesize (a default sentence is built into the script)--vad_start V,A,Dβ starting emotion (Valence, Arousal, Dominance), range [0, 1]--vad_end V,A,Dβ ending emotion--emo_scaleβ emotion injection strength (default: 0.07)
Runs in the EmoTra env (uses Whisper + jiwer). Provide ground-truth text to
get WER/CER; without it the script only transcribes.
Note: Whisper decodes audio via
ffmpeg, so theffmpegbinary must be on yourPATH. If it is missing, install it (e.g.conda install -n EmoTra -c conda-forge ffmpeg).
conda activate EmoTra
python eval_asr_folder.py \
--audio-dir <output_dir> \
--text-jsonl <ground_truth.jsonl> \
--language enSIM uses a WavLM-large speaker-verification model (ECAPA-TDNN head) from
microsoft/UniSpeech, and runs in a separate
sim conda env. None of that third-party code is redistributed here β it is fetched
from upstream by scripts/setup_third_party.sh (the UniSpeech source is used as-is;
our loader neutralizes its optional fairseq import in memory, so no on-disk patch is
applied). Set it up once:
# 1. Create the sim env (CPU build shown; swap the index-url for a CUDA build to use GPU)
conda create -n sim python=3.10 -y
conda run -n sim pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu
conda run -n sim pip install librosa soundfile numpy gdown
# 2. Fetch the third-party SIM dependencies (UniSpeech source, s3prl WavLM source,
# and the ~1.3 GB WavLM-large SV checkpoint). Everything lands in git-ignored paths.
conda run -n sim bash scripts/setup_third_party.shFirst generate the multi-reference speaker embedding once, from a pool of reference audios. This averages the speaker-verification embeddings of --num-refs randomly sampled references into a single, emotion-neutral speaker centroid used as the similarity target:
# Generate the mean reference embedding (run once, before evaluation)
conda run -n sim python tools/precompute_ref_embedding.py --num-refs 200 --gpu 0
# => pretrained_models/CosyVoice2-0.5B/ref_embedding_sage.ptThen run the evaluation:
conda run -n sim python eval_sim_folder_multiref.py \
--audio-dir <output_dir>EmoTra_TTS/
βββ step0_download_emovoice.py # Download EmoVoice-DB dataset
βββ step0_prepare_dataset.py # Prepare dataset structure
βββ step1_filter_by_vad.py # Filter audio by VAD scores (w2v2-vad env)
βββ step2_generate_transition_data_w_ASR_check.py # Generate transition pairs
β
βββ simple_inference_EmoTra_TTS.py # Main inference script
βββ eval_asr_folder.py # ASR evaluation
βββ eval_sim_folder_multiref.py # Multi-reference speaker similarity evaluation
β
βββ environment_emotra.yml # Conda env: EmoTra (training & inference)
βββ environment_w2v2_vad.yml # Conda env: w2v2-vad (Step 1 only)
βββ requirements.txt # pip dependencies
β
βββ cosyvoice/ # Core model code
β βββ bin/
β β βββ train.py # Base training entry (modified: +override arg)
β β βββ train_sft_Flow_Lynorm_fixs.py # Flow SFT training entry
β βββ cli/
β β βββ cosyvoice.py # CosyVoice/CosyVoice2 interface (modified)
β β βββ frontend.py # Text frontend
β β βββ model.py # Base model
β βββ dataset/
β β βββ processor.py # Base data processor
β β βββ processor_EmoTra_sft_LLM.py # LLM SFT data processor
β β βββ processor_sft_Flow_Lynorm_fixs.py # Flow SFT data processor
β βββ flow/
β β βββ flow.py # Base Flow (CausalMaskedDiffWithXvec)
β β βββ flow_sft_Flow_Lynorm_fixs.py # Flow with Direction-Magnitude Decoupled emotion injection
β β βββ flow_matching.py # Base CFM (modified: +clamp fix)
β β βββ flow_matching_sft_Flow.py # CFM with 3D spks support
β β βββ decoder.py # Base decoder
β β βββ decoder_sft_Flow.py # Decoder with 3D spks support
β βββ llm/
β β βββ llm.py # Base LLM (Qwen2LM)
β β βββ llm_EmoTra_sft_LLM.py # LLM with VAD token conditioning
β βββ transformer/
β β βββ label_smoothing_loss_safe.py # Safe loss (prevents div-by-zero)
β βββ utils/
β βββ executor.py # Base executor
β βββ executor_sft_Flow.py # Flow SFT executor (robust DDP)
β
βββ tools/
β βββ vad_wrapper.py # Our wrapper around the w2v2-vad ONNX model (Step 1)
β βββ precompute_ref_embedding.py # Build the mean reference embedding for SIM eval
β βββ extract_neutral_embedding.py # Extract neutral speaker embedding
β βββ make_parquet_list_EmoTra_sft_LLM.py # Generate parquet for LLM SFT
β βββ precompute_llm_tokens_sft_LLM.py # Pre-compute LLM tokens for Flow SFT
β
βββ examples/libritts/cosyvoice2/
β βββ run_EmoTra_sft_LLM.sh # Stage I training pipeline
β βββ run_EmoTra_sft_Flow_Lynorm_fixs.sh # Stage II training pipeline
β βββ conf/
β β βββ cosyvoice2_EmoTra_sft_LLM.yaml # Stage I config
β β βββ cosyvoice2_EmoTra_sft_Flow_Lynorm_fixs.yaml # Stage II config
β βββ local/
β βββ prepare_transition_data_EmoTra_sft_LLM_fast.py # JSONLβKaldi
β
βββ scripts/
β βββ setup_third_party.sh # Fetch all third-party deps from upstream
β
βββ third_party/ # NOT redistributed β populated by scripts/setup_third_party.sh
β βββ Matcha-TTS/ # Flow matching dependency (submodule pointer only)
β # (UniSpeech / s3prl / WavLM ckpt fetched at setup, git-ignored)
β
βββ pretrained_models/ β (symlink) # CosyVoice2-0.5B pretrained weights
EmoTra-TTS is released under the Apache License 2.0. See the LICENSE file for the full terms and third-party attributions.
- EmoTra-TTS code: Copyright (c) 2026 LightSpeed (Author: Tianchi Liu), licensed under Apache-2.0.
- Files derived from CosyVoice 2: Built upon CosyVoice (Apache-2.0); all upstream Apache-2.0 obligations remain in effect, with additional modifications by Tianchi Liu.
- Third-party components: This repository does not redistribute third-party code β it only documents how to obtain and use each dependency. Every third-party component remains under its own original license, and users are responsible for complying with those licenses.
EmoTra-TTS is built on top of CosyVoice by Alibaba Group. We gratefully acknowledge the CosyVoice team for making their excellent TTS framework available under the Apache 2.0 license.
If you find EmoTra-TTS useful in your research, please cite our paper (arXiv:2608.23791):
@article{liu2026emotra,
author = {Liu, Tianchi and Song, Zeyang and Wang, Tianrui and Li, Zhipeng and Xu, Chenglin and Guo, Yiwen},
title = {{EmoTra-TTS}: Smooth Intra-Utterance Emotion Transitions for Speech Synthesis},
journal = {arXiv preprint arXiv:2608.23791},
year = {2026},
}