Submitted to Interspeech 2026 | Paper (arXiv) | Checkpoints (HuggingFace)
Official implementation of DualTurn, a turn-taking prediction model for voice agent pipelines. DualTurn uses generative pretraining on dual-channel conversational audio to learn turn-taking dynamics, then predicts interpretable signals that map directly to agent actions.
| Model | wF1 | BC F1 | Ant@-240 |
|---|---|---|---|
| VAP (native) | .276 | --- | .785 |
| VAP (LR-6) | .389 | .000 | .780 |
| A -- LoRA (main) | .633 | .349 | .874 |
| B -- LoRA + CB | .613 | .077 | .876 |
| C -- No Pretrain | .604 | .079 | .863 |
| D -- LSTM | .602 | .077 | .868 |
| E -- Full FT | .626 | .337 | .879 |
| F -- Discrete | .602 | .072 | .867 |
| G -- Text-Aware PT | .605 | .085 | .873 |
| Model | S/H | S/L | S-P | BC-P |
|---|---|---|---|---|
| VAP (our eval) | .843 | .916 | .720 | .838 |
| A -- LoRA | .985 | .979 | .764 | .864 |
| E -- Full FT | .982 | .985 | .771 | .869 |
| Model | AUC(C) | AUC(B) | AUC(T) | Avg | EER |
|---|---|---|---|---|---|
| GPT-2 (text) | .851 | .774 | .862 | .829 | 24.5 |
| RP+HuBERT+hist (3.1B) | .903 | .818 | .920 | .880 | 19.3 |
| A -- EOT only | .918 | .904 | .919 | .914 | 15.2 |
| A -- Heuristic | .940 | .925 | .924 | .930 | 13.2 |
| A -- LR-probe | .961 | .979 | .950 | .963 | 9.7 |
git clone https://github.com/anyreach-ai/dualturn.git
cd dualturn
pip install -e .
DualTurn uses two datasets:
otoSpeech is a gated dataset. To download it:
# Option A: Copy .env.example and fill in your token
cp .env.example .env
# Edit .env and set HF_TOKEN=hf_your_token_here
# Option B: Set environment variable directly
export HF_TOKEN=hf_your_token_here
# Option C: Login via CLI
huggingface-cli login
bash scripts/prepare_data.sh
Already have processed data elsewhere? The configs assume data lives at
data/inside the repo root. If your data is in a different location you have two options:Option A — symlink (simplest, zero config changes):
ln -s /path/to/your/data dataOption B —
--data-rootflag (no filesystem changes):python train.py --config configs/stage1/pretrain_audio.yaml --data-root /path/to/your/dataThis remaps all
data/...paths in the config to/path/to/your/data/...without touching any files.
# Step 1: Download otoSpeech (requires HF access, see above)
python -m dualturn.data.download_otospeech --output-dir data/otospeech/audio
# Step 2: Extract metadata
python -m dualturn.data.enumerate_otospeech
# Step 3: Process audio (Mimi encoding + Silero VAD)
python -m dualturn.data.process_otospeech
python -m dualturn.data.process_switchboard
# Step 4: Extract continuous Mimi encoder features
python -m dualturn.data.extract_mimi_features --dataset otospeech
python -m dualturn.data.extract_mimi_features --dataset switchboard
# Step 5: Generate labels
python -m dualturn.data.add_start_end_labels data/otospeech_processed_npy
python -m dualturn.data.add_start_end_labels data/switchboard_processed_npy
python -m dualturn.data.relabel_context_aware --dataset both
# Step 6: Create train/val/test splits (otoSpeech + Switchboard)
python -m dualturn.data.create_splits --dataset both
Place Switchboard audio and transcripts as follows:
data/switchboard/
audio_wav/ # Mono WAVs: sw02001A.wav, sw02001B.wav, ...
transcripts/ # Word-level alignments (for Table 3 evaluation)
Trains the backbone to predict next audio tokens for both speakers simultaneously.
# Audio-only pretraining (for models A, B, C, E, F)
bash scripts/train_stage1.sh
# Text-aware pretraining (for model G only)
bash scripts/train_stage1.sh --text
Or run individual phases:
python train.py --config configs/stage1/pretrain_audio.yaml # LoRA phase
python train.py --config configs/stage1/pretrain_audio_ft.yaml # Full fine-tune phase
Fine-tunes the pretrained backbone with 12 classification heads.
# Train all model variants
bash scripts/train_stage2.sh
# Train specific models
bash scripts/train_stage2.sh A E
# Train a single model
python train.py --config configs/stage2/model_A.yaml
| Paper | Config | Description | Stage-1 | Trainable Params |
|---|---|---|---|---|
| A | model_A.yaml | LoRA, pretrained, continuous Mimi (main) | audio-only | 9M |
| B | model_B.yaml | A + auxiliary codebook loss | audio-only | 9M |
| C | model_C.yaml | No Stage-1 pretraining | none | 9M |
| D | model_D.yaml | 8M LSTM backbone | none | 8M |
| E | model_E.yaml | Full fine-tune (all params unfrozen) | audio-only | 500M |
| F | model_F.yaml | Discrete codebook indices | audio-only | 9M |
| G | model_G.yaml | Text-aware Stage-1 pretraining | text-aware | 9M |
Run all eval scripts from the repo root with python -m (this puts the repo root on sys.path so the evaluation package is importable):
cd dualturn # repo root
bash scripts/eval_all.sh --device cuda
# Table 1: Agent action prediction (5-class wF1, BC F1, Anticipation)
python -m evaluation.eval_table1 --models A B C D E F G --device cuda
# Table 2: VAP protocol frame-level (S/H, S/L, S-P, BC-P)
python -m evaluation.eval_table2 --models A E --device cuda
# Table 3: Word-level turn prediction (AUC, EER)
python -m evaluation.eval_table3 --models A --device cuda
# VAP baseline comparison
python -m evaluation.eval_vap_baseline --device cuda
Results are saved to results/table1/, results/table2/, results/table3/.
All eval scripts share these flags:
| Flag | Description | Scripts |
|---|---|---|
--models A B ... | Model IDs to evaluate (default: all available) | all |
--device cuda | Device (cuda or cpu) | all |
--data-dir /path/to/data | Data directory if not at <repo>/data/ | all |
--checkpoint /path/to/best.pt | Override checkpoint path for a single model | table1–3 |
--transcript-dir /path/to/transcripts | Switchboard word-level transcript directory | table3 only |
Example — evaluate a specific checkpoint with data in a custom location:
python -m evaluation.eval_table1 --models A --device cuda \
--data-dir /path/to/data \
--checkpoint /path/to/checkpoints/model_A/best.pt
# Table 3 also needs transcripts (word-level alignments from LDC Switchboard release)
python -m evaluation.eval_table3 --models A --device cuda \
--data-dir /path/to/data \
--checkpoint /path/to/checkpoints/model_A/best.pt \
--transcript-dir /path/to/switchboard/transcripts
Audio encoding: Frozen Mimi codec encodes 24kHz dual-channel audio into continuous 512-dim features at 12.5 Hz. Channel-specific MLPs project to 896-dim, concatenated and fed to the backbone.
Backbone: Qwen2.5-0.5B with LoRA adapters (rank 16, targeting attention + MLP layers). 9M trainable parameters.
Classification heads: 12 lightweight heads (6 per channel):
Agent action inference: Six per-channel signals mapped to five agent actions via logistic regression probe or heuristic thresholds.
| Signal | Definition |
|---|---|
| EOT | Speech offset where other speaker takes floor within 4s |
| HOLD | All other speech offsets (not EOT) |
| BOT | Speech onset (>=1s) following the other speaker |
| BC | Isolated utterance <=1s, >=1s silence before/after |
| VAD | Binary voice activity per frame |
| FVAD | Mean voice activity over 4 future horizons |
| Action | Description |
|---|---|
| ST (Start-Talking) | User offset; agent speaks within 4s |
| CL (Continue-Listening) | User offset; user resumes within 2s |
| SL (Start-Listening) | Overlap onset; incoming speech >1s |
| CT (Continue-Talking) | Overlap onset; incoming speech <1s |
| BC (Backchannel) | Agent vocalization <1s during user speech |
All checkpoints are available on HuggingFace: anyreach-ai/dualturn-qwen2.5-mimi-0.5B
| Checkpoint | Description |
|---|---|
| Stage-1 (audio) | Generative pretraining on 453h |
| Model A | Main model (best overall) |
| Model E | Full fine-tune variant |
dualturn/
model/ # TurnTakingModel, backbones, embeddings, losses
data/ # Data download, processing, labeling, dataset classes
config/ # ExperimentConfig dataclasses
utils/ # Trainer, checkpoint management
configs/
stage1/ # Stage-1 pretraining configs
stage2/ # Stage-2 model variant configs (A-G)
evaluation/ # Paper evaluation scripts (Tables 1-3, Figure 2)
baselines/vap/ # VAP baseline (vendored)
scripts/ # Shell scripts for data prep, training, evaluation
train.py # Main training entry point
@misc{rajaa2026dualturnlearningturntakingdualchannel,
title={DualTurn: Learning Turn-Taking from Dual-Channel Generative Speech Pretraining},
author={Shangeth Rajaa},
year={2026},
eprint={2603.08216},
archivePrefix={arXiv},
primaryClass={eess.AS},
url={https://arxiv.org/abs/2603.08216},
}
This project is licensed under the MIT License. See LICENSE for details.
6 commits
Python
98.5%
Shell
1.5%
Submitted to Interspeech 2026 | Paper (arXiv) | Checkpoints (HuggingFace)
Official implementation of DualTurn, a turn-taking prediction model for voice agent pipelines. DualTurn uses generative pretraining on dual-channel conversational audio to learn turn-taking dynamics, then predicts interpretable signals that map directly to agent actions.
| Model | wF1 | BC F1 | Ant@-240 |
|---|---|---|---|
| VAP (native) | .276 | --- | .785 |
| VAP (LR-6) | .389 | .000 | .780 |
| A -- LoRA (main) | .633 | .349 | .874 |
| B -- LoRA + CB | .613 | .077 | .876 |
| C -- No Pretrain | .604 | .079 | .863 |
| D -- LSTM | .602 | .077 | .868 |
| E -- Full FT | .626 | .337 | .879 |
| F -- Discrete | .602 | .072 | .867 |
| G -- Text-Aware PT | .605 | .085 | .873 |
| Model | S/H | S/L | S-P | BC-P |
|---|---|---|---|---|
| VAP (our eval) | .843 | .916 | .720 | .838 |
| A -- LoRA | .985 | .979 | .764 | .864 |
| E -- Full FT | .982 | .985 | .771 | .869 |
| Model | AUC(C) | AUC(B) | AUC(T) | Avg | EER |
|---|---|---|---|---|---|
| GPT-2 (text) | .851 | .774 | .862 | .829 | 24.5 |
| RP+HuBERT+hist (3.1B) | .903 | .818 | .920 | .880 | 19.3 |
| A -- EOT only | .918 | .904 | .919 | .914 | 15.2 |
| A -- Heuristic | .940 | .925 | .924 | .930 | 13.2 |
| A -- LR-probe | .961 | .979 | .950 | .963 | 9.7 |
git clone https://github.com/anyreach-ai/dualturn.git
cd dualturn
pip install -e .
DualTurn uses two datasets:
otoSpeech is a gated dataset. To download it:
# Option A: Copy .env.example and fill in your token
cp .env.example .env
# Edit .env and set HF_TOKEN=hf_your_token_here
# Option B: Set environment variable directly
export HF_TOKEN=hf_your_token_here
# Option C: Login via CLI
huggingface-cli login
bash scripts/prepare_data.sh
Already have processed data elsewhere? The configs assume data lives at
data/inside the repo root. If your data is in a different location you have two options:Option A — symlink (simplest, zero config changes):
ln -s /path/to/your/data dataOption B —
--data-rootflag (no filesystem changes):python train.py --config configs/stage1/pretrain_audio.yaml --data-root /path/to/your/dataThis remaps all
data/...paths in the config to/path/to/your/data/...without touching any files.
# Step 1: Download otoSpeech (requires HF access, see above)
python -m dualturn.data.download_otospeech --output-dir data/otospeech/audio
# Step 2: Extract metadata
python -m dualturn.data.enumerate_otospeech
# Step 3: Process audio (Mimi encoding + Silero VAD)
python -m dualturn.data.process_otospeech
python -m dualturn.data.process_switchboard
# Step 4: Extract continuous Mimi encoder features
python -m dualturn.data.extract_mimi_features --dataset otospeech
python -m dualturn.data.extract_mimi_features --dataset switchboard
# Step 5: Generate labels
python -m dualturn.data.add_start_end_labels data/otospeech_processed_npy
python -m dualturn.data.add_start_end_labels data/switchboard_processed_npy
python -m dualturn.data.relabel_context_aware --dataset both
# Step 6: Create train/val/test splits (otoSpeech + Switchboard)
python -m dualturn.data.create_splits --dataset both
Place Switchboard audio and transcripts as follows:
data/switchboard/
audio_wav/ # Mono WAVs: sw02001A.wav, sw02001B.wav, ...
transcripts/ # Word-level alignments (for Table 3 evaluation)
Trains the backbone to predict next audio tokens for both speakers simultaneously.
# Audio-only pretraining (for models A, B, C, E, F)
bash scripts/train_stage1.sh
# Text-aware pretraining (for model G only)
bash scripts/train_stage1.sh --text
Or run individual phases:
python train.py --config configs/stage1/pretrain_audio.yaml # LoRA phase
python train.py --config configs/stage1/pretrain_audio_ft.yaml # Full fine-tune phase
Fine-tunes the pretrained backbone with 12 classification heads.
# Train all model variants
bash scripts/train_stage2.sh
# Train specific models
bash scripts/train_stage2.sh A E
# Train a single model
python train.py --config configs/stage2/model_A.yaml
| Paper | Config | Description | Stage-1 | Trainable Params |
|---|---|---|---|---|
| A | model_A.yaml | LoRA, pretrained, continuous Mimi (main) | audio-only | 9M |
| B | model_B.yaml | A + auxiliary codebook loss | audio-only | 9M |
| C | model_C.yaml | No Stage-1 pretraining | none | 9M |
| D | model_D.yaml | 8M LSTM backbone | none | 8M |
| E | model_E.yaml | Full fine-tune (all params unfrozen) | audio-only | 500M |
| F | model_F.yaml | Discrete codebook indices | audio-only | 9M |
| G | model_G.yaml | Text-aware Stage-1 pretraining | text-aware | 9M |
Run all eval scripts from the repo root with python -m (this puts the repo root on sys.path so the evaluation package is importable):
cd dualturn # repo root
bash scripts/eval_all.sh --device cuda
# Table 1: Agent action prediction (5-class wF1, BC F1, Anticipation)
python -m evaluation.eval_table1 --models A B C D E F G --device cuda
# Table 2: VAP protocol frame-level (S/H, S/L, S-P, BC-P)
python -m evaluation.eval_table2 --models A E --device cuda
# Table 3: Word-level turn prediction (AUC, EER)
python -m evaluation.eval_table3 --models A --device cuda
# VAP baseline comparison
python -m evaluation.eval_vap_baseline --device cuda
Results are saved to results/table1/, results/table2/, results/table3/.
All eval scripts share these flags:
| Flag | Description | Scripts |
|---|---|---|
--models A B ... | Model IDs to evaluate (default: all available) | all |
--device cuda | Device (cuda or cpu) | all |
--data-dir /path/to/data | Data directory if not at <repo>/data/ | all |
--checkpoint /path/to/best.pt | Override checkpoint path for a single model | table1–3 |
--transcript-dir /path/to/transcripts | Switchboard word-level transcript directory | table3 only |
Example — evaluate a specific checkpoint with data in a custom location:
python -m evaluation.eval_table1 --models A --device cuda \
--data-dir /path/to/data \
--checkpoint /path/to/checkpoints/model_A/best.pt
# Table 3 also needs transcripts (word-level alignments from LDC Switchboard release)
python -m evaluation.eval_table3 --models A --device cuda \
--data-dir /path/to/data \
--checkpoint /path/to/checkpoints/model_A/best.pt \
--transcript-dir /path/to/switchboard/transcripts
Audio encoding: Frozen Mimi codec encodes 24kHz dual-channel audio into continuous 512-dim features at 12.5 Hz. Channel-specific MLPs project to 896-dim, concatenated and fed to the backbone.
Backbone: Qwen2.5-0.5B with LoRA adapters (rank 16, targeting attention + MLP layers). 9M trainable parameters.
Classification heads: 12 lightweight heads (6 per channel):
Agent action inference: Six per-channel signals mapped to five agent actions via logistic regression probe or heuristic thresholds.
| Signal | Definition |
|---|---|
| EOT | Speech offset where other speaker takes floor within 4s |
| HOLD | All other speech offsets (not EOT) |
| BOT | Speech onset (>=1s) following the other speaker |
| BC | Isolated utterance <=1s, >=1s silence before/after |
| VAD | Binary voice activity per frame |
| FVAD | Mean voice activity over 4 future horizons |
| Action | Description |
|---|---|
| ST (Start-Talking) | User offset; agent speaks within 4s |
| CL (Continue-Listening) | User offset; user resumes within 2s |
| SL (Start-Listening) | Overlap onset; incoming speech >1s |
| CT (Continue-Talking) | Overlap onset; incoming speech <1s |
| BC (Backchannel) | Agent vocalization <1s during user speech |
All checkpoints are available on HuggingFace: anyreach-ai/dualturn-qwen2.5-mimi-0.5B
| Checkpoint | Description |
|---|---|
| Stage-1 (audio) | Generative pretraining on 453h |
| Model A | Main model (best overall) |
| Model E | Full fine-tune variant |
dualturn/
model/ # TurnTakingModel, backbones, embeddings, losses
data/ # Data download, processing, labeling, dataset classes
config/ # ExperimentConfig dataclasses
utils/ # Trainer, checkpoint management
configs/
stage1/ # Stage-1 pretraining configs
stage2/ # Stage-2 model variant configs (A-G)
evaluation/ # Paper evaluation scripts (Tables 1-3, Figure 2)
baselines/vap/ # VAP baseline (vendored)
scripts/ # Shell scripts for data prep, training, evaluation
train.py # Main training entry point
@misc{rajaa2026dualturnlearningturntakingdualchannel,
title={DualTurn: Learning Turn-Taking from Dual-Channel Generative Speech Pretraining},
author={Shangeth Rajaa},
year={2026},
eprint={2603.08216},
archivePrefix={arXiv},
primaryClass={eess.AS},
url={https://arxiv.org/abs/2603.08216},
}
This project is licensed under the MIT License. See LICENSE for details.
6 commits
Python
98.5%
Shell
1.5%