/ Модель

Характеристики

Лицензия
Apache 2.0
Архитектура
dualturn_endpointing · dense
Базовая модель
mimi
Библиотека
transformers
Опубликована
18 июн 2026

dualturn-endpointing

Классификация аудиоАудио → label3 профессии

Компактная аудиомодель Anyreach AI для потокового определения конца реплики пользователя и речевой активности участников в разговорах с голосовым агентом.

76скачиваний за 30 дней1лайквеса обновлены10 июл 2026

Бенчмарки

Как модель справилась с задачами пользователей ailist и что о ней публикуют открытые лидерборды

Публичные оценки

Из открытых лидербордов. Сравнивайте результаты одного теста при одинаковых условиях — отсутствие оценки не значит, что модель хуже.

В подключённых источниках результатов для этой модели пока нет.

Что умеет модель

Описание составлено по карточке модели и метаданным Hugging Face

DualTurn Endpointing — компактная причинная аудиомодель Anyreach AI для определения момента, когда пользователь закончил говорить, и для отслеживания активности обоих участников разговора. Модель дообучена на замороженных признаках аудиокодека Mimi от Kyutai; поверх энкодера добавлено около 1,5 млн параметров. Она принимает аудио пользователя и агента по двум каналам и выдаёт покадровые оценки с частотой 12,5 Гц: вероятность конца реплики пользователя, речевой активности каждого участника и их будущей активности на нескольких временных горизонтах.

Модель предназначена для голосовых агентов и телефонных разговоров, в том числе для потоковой работы на устройстве или CPU. Она работает причинно, использует только уже поступившее аудио, не требует ASR или облачного сервиса и поддерживает режим с одним каналом пользователя, хотя при этом теряется контекст речи агента. Доступны PyTorch- и ONNX-варианты, включая потоковый ONNX-граф; входное аудио приводится к 24 кГц. Модель выдаёт сигналы восприятия, а решение о начале ответа должен принимать отдельный алгоритм. Лицензия — Apache-2.0.

Сценарии применения

  • определение момента, когда голосовому агенту пора отвечать
  • отслеживание речи пользователя и агента в телефонном разговоре
  • обнаружение пауз и перебиваний в разговоре с голосовым агентом
  • потоковая обработка двухканального аудио на устройстве или CPU
  • определение конца реплики по микрофону пользователя без канала агента

Ограничения

Модель выдаёт вероятностные сигналы, а не действия агента, поэтому для выбора момента ответа нужна отдельная политика. Лучше всего подходит для разговорного аудио голосовых агентов и телефонных вызовов; режим только с аудио пользователя теряет контекст канала агента.

Для каких профессий подходит3

Разделы каталога профессий, в задачах которых используется эта модель

Все профессии

Карточка модели

README от автора на Hugging Face, как он его опубликовал

Открыть на Hugging Face

DualTurn Endpointing

A small, causal, audio-only turn-taking model for voice agents. It listens to both channels of a live conversation (user + agent) and emits, frame-by-frame at 12.5 Hz, the signals a voice agent needs to decide when to speak — with end-of-turn (endpointing) as the primary output.

Adapted from the DualTurn paper (arXiv:2603.08216) and fine-tuned for the endpointing head. It runs on frozen Mimi features, adds only ~1.5M parameters on top of the encoder, needs no ASR and no cloud, and is designed to run on-device / on CPU.

How it's meant to be used

The model sits inside a live two-channel call between a user (CH0) and the agent (CH1). It continuously listens to both channels at the same time and, every 80 ms, emits per-frame predictions: VAD and FVAD for both speakers, and the user's end-of-turn (EOT). A voice agent consumes these — chiefly the user's EOT — to decide when it's the agent's turn to speak (and VAD/FVAD to sense who's talking and who's about to talk). The agent channel is not just context: hearing the agent's own speech (and overlaps/interruptions) is what makes the user-EOT judgment reliable in a real conversation.

It can also run as a single-stream (user-only) detector: feed the user audio and silence the agent channel (pass mono, or a zeros agent channel). You still get the user's EOT + user VAD/FVAD; you simply lose the agent-context benefit. This is the setup used for benchmarking and for user-mic-only deployments.

OutputShapeChannelsMeaning
eot_probs[1, T, 1]userP(user has finished their turn) — the endpointing signal
vad_probs[1, T, 2]user, agentP(speaking)
fvad_probs[1, T, 2, 4]user, agentP(channel speaks within a future window) — 4 horizons: 0–240 / 240–640 / 640–1200 / 1200–2000 ms

Frames are 80 ms (12.5 Hz); T ≈ audio_seconds × 12.5. Streaming/causal — outputs at time t use only audio up to t.

Inference modes & latency

Four ready-to-use paths (all fp32, same weights). Offline processes a whole file at once; streaming runs per 80 ms tick for live calls. ONNX paths need only onnxruntime (no PyTorch).

#moderuntimelatency (CPU, 4 threads)deps
1OfflinePyTorch — model(wav, sr)~7.5 ms/frame (batch, ~11× real-time)torch + transformers
2StreamingPyTorch — model.streaming()~63 ms / 80 ms tick (real-time, flat)torch + transformers
3OfflineONNX — model.onnx~7.3 ms/frame (~11× real-time)onnxruntime
4StreamingONNX — stream_tick.onnx~52 ms / 80 ms tick (real-time, flat, tightest tail)onnxruntime

Offline latency is amortized batch throughput (all frames returned after the file is processed); streaming latency is the real per-tick cost in a live call — both well under the 80 ms budget. Streaming is causal + bounded (transformer KV capped at 250 frames = 10 s), so per-tick latency stays flat over arbitrarily long calls. Streaming outputs are decision-equivalent to offline — they match the batch model to within ~0.02–0.03 (fp32 op-order drift in the incremental path), which doesn't move threshold+sustain endpoint decisions.

Inference interval (compute ↔ latency)

Two streaming ONNX graphs are shipped — pick by how often you need to react vs how much CPU you want to spend. Both are strictly causal (the transformer runs frame-by-frame inside the graph — no future audio) and decision-equivalent to offline — they track the batch model to within ~0.02–0.03 (fp32 op-order drift in the streaming path; the two graphs are bit-identical to each other, Δ≈0). The 240 ms graph runs one shared encoder pass over 3 frames instead of three separate passes, so it's ~1.8× cheaper per second of audio:

  • stream_tick.onnx — 1 frame per call ([2, 1920] = 80 ms).
  • stream_tick_240.onnx — 3 frames per call ([2, 5760] = 240 ms), returns all 3 × 80 ms frames.

Measured on a real call (CPU, 4 threads, carried KV cache). Grace = idle time left in the interval after inference (interval − latency); CPU busy = per-stream duty cycle, fraction of wall-clock the inference runs to keep up with one live stream (latency × calls/sec):

graphinfer everylatency / callcalls / audio-secgrace / callCPU busy / stream
stream_tick.onnx80 ms52 ms12.528 ms~65%
stream_tick_240.onnx240 ms85 ms4.17155 ms~36%
  • 80 ms — snappiest, but heaviest. Each call eats 52 ms → only 28 ms grace, pool busy ~65% per stream. Fastest reactions, least room for anything else.
  • 240 ms — much cheaper, lots of slack. Each call takes 85 ms (vs 3 × 52 = 156 ms for three 80 ms ticks) but leaves 155 ms grace and sits at ~36% busy.
  • The real win is at scale. CPU per second of audio drops ~1.8× (650 → 354 ms/s, ≈45% less), so one machine holds roughly ~1.8× as many concurrent calls — the per-stream % looks modest, but that factor multiplies across every simultaneous call in your fleet. (CPU busy % is a single-stream duty cycle measured with 4 threads — read it as relative cost between graphs, not absolute single-core load.)
  • Accuracy is unaffected — the 240 ms graph is decision-equivalent to the 80 ms one (endpoint decisions match); the only cost is that you learn the turn ended at the next 240 ms boundary (up to one interval later).

1. Offline — PyTorch

import torch, torchaudio
from transformers import AutoModel

model = AutoModel.from_pretrained("anyreach-ai/dualturn-endpointing", trust_remote_code=True).eval()

wav, sr = torchaudio.load("conversation.wav")   # [2, T]  CH0 = user, CH1 = agent
with torch.no_grad():
    out = model(wav, sr=sr)

out.eot_probs    # [1, T, 1]      P(user end-of-turn)
out.vad_probs    # [1, T, 2]      P(speaking) — (user, agent)
out.fvad_probs   # [1, T, 2, 4]   P(speaks soon) — (user, agent) × (0-240/240-640/640-1200/1200-2000 ms)

Turn the end-of-turn stream into a decision with a thin policy — e.g. fire when eot_probs stays above a threshold for a short sustain window:

eot = out.eot_probs[0, :, 0]
thr, sustain = 0.5, 3                       # 3 frames ≈ 240 ms
fired = (eot > thr).unfold(0, sustain, 1).all(-1).nonzero()   # frame indices of detected turn-ends

Mono input [T] or [1, T] is also accepted (treated as user with a silent agent — a single-stream fallback). Any sample rate works (sr=); audio is internally resampled to 24 kHz (Mimi's input rate). The call above is the offline mode (whole file at once). For live calls, use the streaming mode below.

2. Streaming — PyTorch (real-time)

For live calls, don't re-run the whole file each tick — use the stateful streamer. It's causal (uses only past audio), keeps bounded state (transformer KV window + LSTM state), and runs ~63 ms per 80 ms tick on CPU (≥4 threads), flat regardless of call length.

from transformers import AutoModel
model = AutoModel.from_pretrained("anyreach-ai/dualturn-endpointing", trust_remote_code=True).eval()
streamer = model.streaming()

# feed audio as it arrives, in 80 ms chunks: [2, 1920] @ 24 kHz  (CH0=user, CH1=agent)
for chunk in chunks_at_24kHz:            # mono [1920] is also accepted (silent agent)
    out = streamer.push(chunk)           # None until the next 12.5 Hz frame is ready
    if out is not None:
        p_eot = out.eot_probs[0, -1, 0].item()      # latest P(user end-of-turn)
        # fire your end-of-turn policy on p_eot (threshold + short sustain)

streamer.reset()                         # between calls

push returns a DualTurnOutput (same eot_probs / vad_probs / fvad_probs) for the new frame(s). It matches the offline model to ~3e-4 mean H1. Feed 24 kHz (resample the continuous stream once, not per-chunk, to avoid chunk-edge artifacts).

3. Offline — ONNX (no PyTorch)

A self-contained model.onnx (Mimi encoder + readout in one graph) is included for CPU / on-device deployment with just onnxruntime. Input is a 2-channel waveform at 24 kHz [2, T] (CH0=user, CH1=agent); resample first with any tool.

import numpy as np, librosa, onnxruntime as ort
from huggingface_hub import hf_hub_download

onnx_path = hf_hub_download("anyreach-ai/dualturn-endpointing", "model.onnx")
sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])

wav, sr = librosa.load("conversation.wav", sr=24000, mono=False)   # [2, T] @ 24 kHz, CH0=user CH1=agent
eot, vad, fvad = sess.run(None, {"audio_24k": wav.astype("float32")})
# eot  [1, T', 1]      P(user end-of-turn)
# vad  [1, T', 2]      P(speaking) — (user, agent)
# fvad [1, T', 2, 4]   P(speaks soon) — (user, agent) × (0-240/240-640/640-1200/1200-2000 ms)

eot = eot[0, :, 0]
fired = np.where(np.convolve((eot > 0.5).astype(int), np.ones(3), "valid") == 3)[0]   # sustained ≥240 ms

Runs comfortably faster than real-time on CPU (≈1 s of compute per 10 s of audio, single-threaded). The graph is fully self-contained — no torch or transformers needed at inference.

4. Streaming — ONNX (real-time, no PyTorch)

Torch-free real-time streaming via stream_tick.onnx + the onnx_streaming.py helper (DualTurnONNXStreamer). Each 80 ms tick only computes the new frame — the graph carries all state (conv buffer, transformer KV window, downsample history, LSTM state) in/out, so latency is low (~52 ms/tick) and flat over long calls. Decision-equivalent to the PyTorch model.

from onnx_streaming import DualTurnONNXStreamer     # download onnx_streaming.py + stream_tick.onnx from the repo

streamer = DualTurnONNXStreamer()                    # auto-downloads stream_tick.onnx (via huggingface_hub)
for chunk in chunks_24k:                             # each: [2, 1920] float32 @ 24 kHz (CH0=user, CH1=agent; mono OK)
    out = streamer.push(chunk)
    p_eot = out["eot"]                               # P(user end-of-turn); also out["vad"] [2], out["fvad"] [2,4]
    # fire your end-of-turn policy on p_eot (threshold + short sustain)
streamer.reset()                                     # between calls

Needs only onnxruntime + numpy (+ huggingface_hub for the auto-download). Feed 24 kHz audio in exactly 1920-sample (80 ms) chunks.

To spend ~1.8× less CPU (at the cost of reacting every 240 ms instead of 80 ms), use the 240 ms graph stream_tick_240.onnx via DualTurnONNXStreamer240 — same helper file, [2, 5760] chunks, returns the 3 × 80 ms frames per call. Both graphs are strictly causal and decision-equivalent to offline; see Inference interval.

from onnx_streaming import DualTurnONNXStreamer240      # downloads stream_tick_240.onnx

streamer = DualTurnONNXStreamer240()
for chunk in chunks_240ms:                               # each: [2, 5760] float32 @ 24 kHz
    for fr in streamer.push(chunk):                      # list of 3 frames: fr["eot"], fr["vad"], fr["fvad"]
        ...                                              # apply your end-of-turn policy on fr["eot"]
streamer.reset()

Performance

Benchmarked on LiveKit's eot-bench (a neutral end-of-turn benchmark). Metric is a latency / false-cutoff Pareto: FC@N ms = % of mid-turn pauses falsely fired on at an N-ms latency budget (lower is better); Lat@X% = ms of dead-air after a true turn-end at an X% false-cutoff budget (lower is better). Our harness reproduces LiveKit's published SmartTurn numbers exactly, so rows are directly comparable; DualTurn is scored single-stream (agent = silence) — an honest handicap. English slice:

#ModelParamsFC@300 ↓FC@600 ↓Lat@5% ↓Lat@10% ↓Runs on
1LiveKit Turn Detector v1undisclosed9.9%4.5%543 ms295 ms☁️ Cloud
2Deepgram Fluxundisclosed12.9%9.9%1151 ms548 ms☁️ Cloud
3DualTurn Endpointing (this model)~1.5M21.8%10.2%1115 ms610 ms✅ CPU / GPU
4ultraVAD (Ultravox / Llama-8B)~8B27.7%11.9%899 ms663 ms🖥️ GPU
5LiveKit Turn Detector v1-miniundisclosed27.8%12.1%1070 ms698 ms✅ CPU
6SmartTurn v3.2~8M35.2%14.8%1051 ms739 ms✅ CPU
7AssemblyAIundisclosed49.4%14.6%1049 ms713 ms☁️ Cloud
8VAD baseline (Silero)~0.3–1.5M55.6%21.7%1600 ms1000 ms✅ CPU

DualTurn Endpointing is the best on-device / audio-only turn detector on the board — beating LiveKit's own on-device v1-mini, SmartTurn v3.2, and the open 8B ultraVAD, at ~1.5M params without transcription or cloud. Only closed cloud end-of-turn services lead. It holds the same #1-on-device rank on zero-shot Spanish (not trained on Spanish), evidence that turn-taking here is language-agnostic acoustics.

How it works

Adapted from the DualTurn paper (arXiv:2603.08216) and fine-tuned for endpointing: both channels are encoded by a frozen Mimi speech encoder into 12.5 Hz continuous features; a small causal readout (~1.5M params) produces the per-frame turn-taking probabilities above. Audio-only (no ASR), streaming, and light enough for CPU / on-device inference. The Mimi encoder is downloaded automatically from kyutai/mimi on first use.

Training & evaluation

  • Method. Built on the approach introduced in the DualTurn paper (frozen dual-channel Mimi features → small causal turn-taking readout), specialized here for user end-of-turn endpointing.
  • Training data. The two public dual-channel (human–human) corpora used in the DualTurn paper — Switchboard and OtoSpeech — plus a large private dual-channel dataset of real AI-agent phone calls (the production domain this model targets).
  • Evaluation. Measured on our internal private test set (held-out real agent calls; the recall / interval and latency numbers above) and, for an external neutral comparison, on LiveKit's eot-bench (reported in Performance above; other systems' rows are LiveKit's published numbers).

Files

  • model.safetensors — the ~1.5M-param readout + input-standardization stats (PyTorch paths).
  • config.json — with auto_map for trust_remote_code.
  • modeling_dualturn.py — PyTorch model + both PyTorch modes: offline (model(wav, sr)) and streaming (model.streaming()).
  • model.onnx — self-contained offline ONNX graph (Mimi encoder + readout).
  • stream_tick.onnx — self-contained streaming ONNX graph (one 80 ms tick, state in/out).
  • stream_tick_240.onnx — 240 ms streaming graph (3 frames/tick, ~1.8× cheaper/sec of audio, strictly causal).
  • onnx_streaming.py — DualTurnONNXStreamer (80 ms) + DualTurnONNXStreamer240 (240 ms) helpers (pure onnxruntime + numpy).

Notes & limitations

  • This is a perception model (it emits turn-taking signals, not agent actions) — pair it with a thin decision policy.
  • eot_probs is the user's end-of-turn (this is an endpointing model). vad_probs and fvad_probs cover both channels (user, agent).
  • Best in conversational voice-agent (telephone) audio, the domain it was trained on.
  • Deps by path: PyTorch modes (1, 2) need torch + torchaudio + transformers; ONNX modes (3, 4) need only onnxruntime + numpy (+ librosa/soundfile for loading, huggingface_hub for download).

Author

Shangeth Rajaa — Senior ML Research Scientist, Anyreach AI.

Citation

@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},
}

Как запустить

На своей GPU в ailist. Цена — по текущим предложениям провайдеров

GPU в ailist

Универсальный рантайм · transformers · audio-classification

Нельзя запустить

Модели нужен код из репозитория; автор пока не в списке доверенных организаций.

Класс архитектуры
DualTurnEndpointingModel
Ревизия весов
c3860ed

0 моделей