Smart Turn — 발화 종료 감지

Smart Turn v3.2는 VAD가 멈춤을 보고한 뒤 한 가지 질문에 답합니다. 화자가 말을 끝냈는가, 아니면 문장 중간인가? 전사 텍스트가 아니라 음성 자체(운율, 속도, 억양)를 듣고 판단하며, 23개 언어를 지원하고, 멈춤마다 한 번씩 발화의 마지막 8초에 대해 실행됩니다. 가중치는 Pipecat의 공식 Smart Turn v3.2 릴리스(BSD-2-Clause)이며, Apple 플랫폼용으로 CoreML로 컴파일하고 Speech Core용으로 ONNX로 내보냈습니다.

VAD만으로는 부족한 이유

음성 활동 감지기는 침묵만 듣습니다. 사람은 문장 중간에 생각하느라 멈추기 때문에, VAD만 쓰는 에이전트는 말을 끊거나, 모든 문장 뒤에 길고 고정된 타임아웃을 기다려야 합니다. Smart Turn은 각 멈춤을 확인합니다. 끝난 문장에는 즉시 답하고, 문장 중간의 멈춤에서는 에이전트가 계속 듣습니다.

동작 방식

Smart Turn은 VAD가 아니며 스스로 음성을 감지하지 않습니다. 스트리밍 Silero VAD와 짝을 이루며, 언제 물어볼지는 VAD가 결정합니다. 분류기를 붙이면 확정된 멈춤은 판정이 아니라 질문이 됩니다.

  1. 멈춤마다 한 번 묻기. Silero가 멈춤을 확정하면(오프셋 임계값 이후 minSilenceDuration 경과), 프로세서는 지금까지의 발화 음성(VAD 시작 0.5초 전부터 현재 청크까지, 최대 마지막 8초)을 분류기에 넘깁니다.
  2. 완료. 확률이 threshold(0.5) 이상이면 평소처럼 .speechEnded가 발생합니다.
  3. 유지. 임계값 미만이면 세그먼트가 열린 채로 남습니다. 화자가 다시 말하면 같은 세그먼트가 이어지고(두 번째 .speechStarted 없음), 다음 확정된 멈춤에서 더 길어진 발화로 다시 묻습니다.
  4. 침묵 상한. 침묵이 maxSilenceDuration(2.0초, 멈춤 시작부터 측정)에 이르면 세그먼트는 어쨌든 끝나므로, 말끝이 흐려지는 화자도 답을 받습니다. 세그먼트의 endTime은 상한이 만료된 시점이 아니라 음성이 멈춘 위치입니다.

flush()는 스트림 끝에서 유지 중인 세그먼트를 정리하고 reset()은 이를 지웁니다. 분류기가 예외를 던지면 "완료"로 간주하므로 모델 장애가 대화를 멈추게 하지 않습니다. isHoldingTurn은 유지 상태를, lastTurnCompletionProbability는 가장 최근 답을 알려줍니다.

모델

속성
아키텍처Whisper-Tiny 인코더 + 어텐션 풀링 + sigmoid 출력의 MLP 헤드
파라미터8.0M
입력16 kHz 모노 오디오의 마지막 8초(128,000 샘플). 짧은 발화는 앞쪽을 0으로 채우고, 다른 샘플 레이트는 리샘플링
출력발화가 완료되었을 확률 [0, 1]. 기본 임계값 0.5
언어23개 — Arabic, Bengali, Chinese, Danish, Dutch, English, Finnish, French, German, Hindi, Indonesian, Italian, Japanese, Korean, Marathi, Norwegian, Polish, Portuguese, Russian, Spanish, Turkish, Ukrainian, Vietnamese
라이선스BSD-2-Clause

내보낸 모델에는 업스트림 모델 학습에 쓰인 파형 정규화를 포함한 Whisper log-mel 프런트엔드가 내장되어 있어, 호출자는 원본 오디오를 그대로 넘깁니다. Swift나 C++에서 특징 추출은 없습니다.

성능

내보내기크기정확도8초 윈도우당 지연
CoreML(CPU + Neural Engine)16.8 MB92.9%3.5 ms
ONNX fp32(ONNX Runtime, CPU)33 MB92.9%2 스레드 약 36 ms, 4 스레드 20 ms
ONNX int8(ONNX Runtime, CPU)11.1 MB93.0%2 스레드 약 36 ms, 4 스레드 20 ms

정확도는 업스트림 테스트 세트의 클립 1,000개로 측정했으며, CoreML과 fp32 ONNX 내보내기는 이 클립들에서 업스트림 fp32 모델과 정확히 일치합니다. 지연은 Apple M5 Pro에서 8초 윈도우 한 번의 시간입니다. 멈춤마다 한 번만 호출하므로, VAD가 이미 기다린 멈춤에 체감할 만한 시간이 더해지지 않습니다.

Swift API

녹음된 발화를 한 번만 판정하기:

import SpeechVAD

let turn = try await SmartTurnModel.fromPretrained()
try turn.prewarm()   // compile the graph before the first real pause

let probability = try turn.turnCompleteProbability(
    audio: utterance, sampleRate: 16_000)
if probability >= SmartTurnModel.defaultThreshold {
    // finished turn — reply now
}

스트리밍 VAD에 붙이면 확정된 모든 멈춤이 세그먼트가 끝나기 전에 확인됩니다:

let vad = try await SileroVADModel.fromPretrained(engine: .coreml)
let turn = try await SmartTurnModel.fromPretrained()

let processor = StreamingVADProcessor(
    model: vad,
    config: .sileroDefault,
    turnCompletion: turn,
    turnCompletionConfig: TurnCompletionConfig(threshold: 0.5, maxSilenceDuration: 2.0))

for chunk in microphoneChunks {            // Float32 @ 16 kHz
    for event in processor.process(samples: chunk) {
        switch event {
        case .speechStarted(let time): print("speech at \(time)s")
        case .speechEnded(let segment): print("turn \(segment.startTime)–\(segment.endTime)s")
        }
    }
}
let remaining = processor.flush()

SmartTurnModelAudioCommonTurnCompletionProvider 프로토콜을 따르므로, 같은 형태의 다른 분류기를 대신 붙일 수 있습니다. fromPretrained(cacheDir:offlineMode:)는 다른 모델과 같은 캐시 및 오프라인 규칙을 따릅니다.

VoicePipeline

VoicePipeline은 같은 분류기를 그대로 받으므로, STT·TTS·VAD 설정을 바꾸지 않고도 음성 에이전트가 사용자의 말을 끊지 않게 됩니다. PipelineConfig의 필드 두 개와 메서드 하나로 연결하며, speech-swift v0.0.27 이상(speech-core v0.0.14 기반 SpeechCore.xcframework)이 필요합니다.

import SpeechCore
import SpeechVAD

let smartTurn = try await SmartTurnModel.fromPretrained()
try smartTurn.prewarm()

var config = PipelineConfig()
config.turnCompletionThreshold = 0.5   // pause ends the turn when p >= this
config.turnCompletionMaxSilence = 2.0  // seconds of silence that end a held turn anyway; 0 = never

let pipeline = VoicePipeline(stt: stt, tts: tts, vad: vad, config: config, onEvent: { print($0) })
pipeline.setTurnCompletion(smartTurn)   // before start(); nil detaches
pipeline.start()
API기본값효과
turnCompletionThreshold0.5VAD 멈춤이 사용자의 발화를 끝내기 위한 완료 확률 하한. 분류기를 붙인 뒤에만 사용됩니다.
turnCompletionMaxSilence2.0초멈춤 시작부터 잰 침묵 초. 이를 넘기면 분류기가 거부한 발화도 어쨌든 끝납니다. 0이면 끝내지 않음.
setTurnCompletion(_:)임의의 TurnCompletionProvider를 붙입니다. start() 전에 호출하고, nil을 넘기면 분리됩니다. 오류를 던지는 분류기는 "완료"로 간주되므로 모델 장애가 대화를 멈추지 않습니다.

분류기를 붙이면 VAD 멈춤은 확률이 임계값에 도달할 때만 사용자의 발화를 끝냅니다. 그 미만이면 파이프라인은 계속 듣고, 다시 시작된 음성은 같은 발화로 이어지며(두 번째 speechStarted 없음), turnCompletionMaxSilence가 유지 중인 발화를 어쨌든 끝냅니다. Eager STT도 이 거부를 따르며, 분류기는 오디오 스레드에서 멈춤마다 한 번만 실행되므로 로드 후 prewarm()을 호출하세요.

튜닝

설정기본값효과
threshold0.5낮을수록 발화가 더 빨리 끝나고 끼어듦이 잦아지며, 높을수록 답하기 전에 더 오래 기다립니다. 바꾸기 전에 실제 대화에서 lastTurnCompletionProbability를 관찰하세요.
maxSilenceDuration2.0초멈춤 시작부터 측정하는 강제 상한입니다. 거부된 멈춤이 발화를 이보다 오래 유지할 수는 없습니다. 0이면 상한이 꺼지고, 유지 중인 세그먼트는 음성 또는 flush()를 기다립니다.
preRollDuration0.5초분류기 입력에 포함되는 VAD 시작 이전의 오디오로, 잘린 첫 음절도 모델에 전달됩니다.

멈춤이 언제 확정되는지, 즉 분류기에 언제 묻는지는 여전히 Silero의 minSilenceDuration이 결정합니다. 첫 실제 멈춤이 그래프 컴파일 비용을 치르지 않도록 로드 후 prewarm()을 호출하세요.

CLI 사용법

# Probability, complete/incomplete verdict and inference time for one utterance
speech turn utterance.wav

# Custom threshold, JSON output
speech turn utterance.wav --threshold 0.7 --json

# Local bundle, no download
speech turn utterance.wav --model-dir ~/smart-turn-coreml

# Streaming VAD with Smart Turn confirming each pause; mid-sentence pauses merge
speech vad-stream call.wav --smart-turn
speech vad-stream call.wav --smart-turn --turn-threshold 0.6 --turn-max-silence 1.5
옵션명령설명
--thresholdturn이 확률 이상이면 발화를 완료로 간주(기본 0.5)
--jsonturnprobability, threshold, complete, latency_ms를 JSON으로 출력
--model, -mturn같은 구조의 HuggingFace 저장소(기본 aufklarer/Smart-Turn-v3.2-CoreML)
--model-dirturnsmart_turn.mlmodelcconfig.json이 있는 로컬 디렉터리. 다운로드 건너뜀
--smart-turnvad-stream세그먼트를 끝내기 전에 각 멈춤을 Smart Turn으로 확인
--turn-thresholdvad-streamSmart Turn 완료 임계값(기본 0.5)
--turn-max-silencevad-stream거부된 멈춤 이후 어쨌든 세그먼트를 끝내는 침묵 초(기본 2.0)

speech turn은 파일(모든 샘플 레이트)을 불러와 마지막 8초를 채점하고, 확률과 임계값 통과 여부, 추론 시간을 출력합니다. 자신의 녹음으로 임계값을 조정할 때 사용하세요. 전체 플래그 목록은 CLI 레퍼런스에 있습니다.

C++ / Android

Speech Core는 같은 모델을 OnnxSmartTurn으로 Linux, Windows, Android에 제공합니다(ONNX Runtime 전용, LiteRT 버전 없음). start() 전에 VoicePipeline::set_turn_completion()으로 붙이면, VAD 멈춤은 확률이 turn_completion_threshold(0.5)에 도달할 때만 사용자의 발화를 끝내고, turn_completion_max_silence(2.0초)가 유지 중인 발화를 어쨌든 끝냅니다. C API는 같은 훅을 sc_turn_completion_vtable_t + sc_pipeline_set_turn_completion()으로 노출하므로, Kotlin이나 Swift 호스트는 VAD와 같은 방식으로 자체 분류기를 연결할 수 있습니다.

#include <speech_core/models/onnx_smart_turn.h>

speech_core::OnnxSmartTurn model("/models/smart-turn-v3.2-int8.onnx");

// Audio of the turn so far at any rate (resampled to 16 kHz internally).
float p = model.turn_complete_probability(turn.data(), turn.size(), rate);

// Or let the pipeline ask it on every VAD pause (before start()).
pipeline.set_turn_completion(&model);

Speech Core의 speech CLI는 macOS와 같은 방식으로 녹음을 채점합니다:

speech download-models        # fetches the int8 graph with the other ONNX models
speech turn recording.wav --threshold 0.5 --json

모델 파일, C API 계약, CLI는 Speech Core 페이지와 저장소의 docs/models.md에 문서화되어 있습니다.

모델 다운로드

모델백엔드크기HuggingFace
Smart-Turn-v3.2CoreML16.8 MBaufklarer/Smart-Turn-v3.2-CoreML
Smart-Turn-v3.2ONNX(fp32 + int8)33 MB / 11.1 MBsoniqo/Smart-Turn-v3.2-ONNX

소스