Views
No views yet
| Output | Shape | Description |
|---|---|---|
vad_probs | [B, T, 2] | P(speaking now) — [:,0]=user [:,1]=agent |
fvad_probs | [B, T, 8] | P(future speech) at 240/480/960/2000 ms — user 0:4, agent 4:8 |
eot_probs | [B, T, 2] | P(end of turn) per channel |
bot_probs | [B, T, 2] | P(beginning of turn) per channel |
hold_probs | [B, T, 2] | P(within-turn hold/pause) per channel |
bc_probs | [B, T, 2] | P(backchannel) per channel |

pip install transformers torch torchaudio safetensors1import torch, torchaudio
2from transformers import AutoModel
3
4model = AutoModel.from_pretrained(
5 "anyreach-ai/dualturn-qwen2.5-mimi-0.5B",
6 trust_remote_code=True,
7)
8model.eval()
9
10wav, sr = torchaudio.load("conversation.wav") # [2, T] CH0=user CH1=agent
11
12with torch.no_grad():
13 out = model(wav, sr=sr)
14
15print(out.vad_probs.shape) # [1, T, 2]
16print(out.fvad_probs.shape) # [1, T, 8]
17print(out.eot_probs.shape) # [1, T, 2]past_key_values for full-context batch inference, or maintain
the cache across steps for low-latency streaming (~41 ms/step at 5 s context).pip install onnxruntime transformers torch torchaudio safetensors huggingface_hub1import numpy as np, torch, torch.nn as nn, torchaudio, os
2from transformers import MimiModel
3from safetensors.torch import load_file
4from huggingface_hub import hf_hub_download, snapshot_download
5import onnxruntime as ort
6
7N_LAYERS, N_HEADS, HEAD_DIM, D = 24, 2, 64, 896
8
9# Mimi encoder
10mimi = MimiModel.from_pretrained("kyutai/mimi").eval()
11
12@torch.no_grad()
13def encode(wav_1d):
14 x = wav_1d.unsqueeze(0).unsqueeze(0)
15 e = mimi.encoder(x)
16 et = mimi.encoder_transformer(e.transpose(1, 2))
17 if hasattr(et, "last_hidden_state"): et = et.last_hidden_state
18 return mimi.downsample(et.transpose(1, 2)).squeeze(0).T.float().numpy() # [T, 512]
19
20# Projection + heads from safetensors
21weights = load_file(hf_hub_download("anyreach-ai/dualturn-qwen2.5-mimi-0.5B", "model.safetensors"))
22
23proj = nn.Sequential(nn.Linear(1024, D), nn.GELU(), nn.Linear(D, D)).eval()
24proj[0].weight.data = weights["mimi_projection.proj.0.weight"]
25proj[0].bias.data = weights["mimi_projection.proj.0.bias"]
26proj[2].weight.data = weights["mimi_projection.proj.2.weight"]
27proj[2].bias.data = weights["mimi_projection.proj.2.bias"]
28
29@torch.no_grad()
30def project(f0, f1):
31 return proj(torch.cat([torch.from_numpy(f0),
32 torch.from_numpy(f1)], dim=-1)).unsqueeze(0).numpy()
33
34def _head(key):
35 h = nn.Linear(D, 1).eval()
36 h.weight.data = weights[f"{key}.weight"]
37 h.bias.data = weights[f"{key}.bias"]
38 return h
39
40vad0, vad1 = _head("vad_head_ch0"), _head("vad_head_ch1")
41fvad = nn.Linear(D, 8).eval()
42fvad.weight.data = weights["fvad_head.weight"]
43fvad.bias.data = weights["fvad_head.bias"]
44
45@torch.no_grad()
46def heads(h_np):
47 h = torch.from_numpy(h_np)
48 return {
49 "vad_probs": torch.sigmoid(torch.stack([vad0(h).squeeze(-1),
50 vad1(h).squeeze(-1)], -1)).numpy(),
51 "fvad_probs": torch.sigmoid(fvad(h)).numpy(),
52 }
53
54# ONNX needs model.onnx + model.onnx_data in the same directory — use snapshot_download
55repo_dir = snapshot_download(
56 "anyreach-ai/dualturn-qwen2.5-mimi-0.5B",
57 allow_patterns=["onnx/*"],
58)
59sess = ort.InferenceSession(
60 os.path.join(repo_dir, "onnx", "model.onnx"),
61 providers=["CPUExecutionProvider"])
62
63def step(embeds, pos, past_kv=None):
64 T, T_ctx = embeds.shape[1], (past_kv[0][0].shape[2] if past_kv else 0)
65 inp = {
66 "input_ids": np.ones((1, T), np.int64),
67 "attention_mask": np.ones((1, T_ctx + T), np.int64),
68 "position_ids": np.arange(pos, pos + T, dtype=np.int64).reshape(1, -1),
69 }
70 for i in range(N_LAYERS):
71 inp[f"past_key_values.{i}.key"] = past_kv[i][0] if past_kv else np.zeros((1,N_HEADS,0,HEAD_DIM),np.float32)
72 inp[f"past_key_values.{i}.value"] = past_kv[i][1] if past_kv else np.zeros((1,N_HEADS,0,HEAD_DIM),np.float32)
73 outs = sess.run(None, inp)
74 return outs[0], [(outs[1+i*2], outs[2+i*2]) for i in range(N_LAYERS)]
75
76# Run
77wav, sr = torchaudio.load("conversation.wav")
78wav = torchaudio.transforms.Resample(sr, 24000)(wav)
79f0, f1 = encode(wav[0]), encode(wav[1])
80T = min(f0.shape[0], f1.shape[0])
81
82# Batch (no KV cache)
83hidden, _ = step(project(f0, f1), pos=0)
84preds = heads(hidden)
85print(preds["vad_probs"].shape) # (1, T, 2)
86print(preds["fvad_probs"].shape) # (1, T, 8)
87
88# Streaming (KV cache, 240 ms steps)
89past, pos = None, 0
90for i in range(0, T, 3):
91 emb = project(f0[i:i+3], f1[i:i+3])
92 hidden, past = step(emb, pos, past)
93 chunk_preds = heads(hidden)
94 if past and past[0][0].shape[2] > 62: # keep 5 s context
95 past = [(k[:,:,-62:,:], v[:,:,-62:,:]) for k,v in past]
96 pos += emb.shape[1]| File | Description |
|---|---|
model.safetensors | FP32 weights — Qwen2.5-0.5B backbone + projection + all heads |
config.json | Model config with auto_map for AutoModel |
modeling_dualturn.py | DualTurnModel + DualTurnConfig (self-contained) |
onnx/model.onnx | ONNX Qwen backbone — outputs hidden_states [B,T,896] + KV cache |
1@misc{rajaa2026dualturnlearningturntakingdualchannel,
2 title={DualTurn: Learning Turn-Taking from Dual-Channel Generative Speech Pretraining},
3 author={Shangeth Rajaa},
4 year={2026},
5 eprint={2603.08216},
6 archivePrefix={arXiv},
7 primaryClass={eess.AS},
8 url={https://arxiv.org/abs/2603.08216},
9}