Smart Turn — การตรวจจับจบเทิร์นสนทนา

Smart Turn v3.2 ตอบคำถามเดียวหลังจาก VAD รายงานว่ามีช่วงหยุด: ผู้พูดพูดจบแล้ว หรือกำลังอยู่กลางประโยค? โมเดลฟังจากเสียงโดยตรง — ทำนองเสียง จังหวะ น้ำเสียง — ไม่ใช่จากข้อความถอดเสียง รองรับ 23 ภาษา และทำงานหนึ่งครั้งต่อช่วงหยุดบน 8 วินาทีสุดท้ายของเทิร์น น้ำหนักโมเดลคือ Smart Turn v3.2 รุ่นทางการจาก Pipecat (BSD-2-Clause) คอมไพล์เป็น CoreML สำหรับแพลตฟอร์ม Apple และส่งออกเป็น ONNX สำหรับ Speech Core

ทำไม VAD อย่างเดียวจึงไม่พอ

ตัวตรวจจับกิจกรรมเสียงพูดได้ยินแค่ความเงียบ คนเรามักหยุดกลางประโยคเพื่อคิด เอเจนต์ที่ใช้ VAD อย่างเดียวจึงต้องเลือกระหว่างพูดแทรก หรือรอเวลาหมดอายุที่ยาวและตายตัวหลังทุกประโยค Smart Turn ยืนยันแต่ละช่วงหยุด: ประโยคที่พูดจบได้รับคำตอบทันที ส่วนการหยุดกลางประโยคทำให้เอเจนต์ฟังต่อ

หลักการทำงาน

Smart Turn ไม่ใช่ VAD และไม่ตรวจจับเสียงพูดด้วยตัวเอง มันทำงานคู่กับ Silero VAD แบบสตรีมมิ่ง ซึ่งเป็นผู้ตัดสินว่าจะถามเมื่อใด เมื่อต่อตัวจำแนกเข้าไป ช่วงหยุดที่ยืนยันแล้วจะกลายเป็นคำถามแทนที่จะเป็นคำตัดสิน:

  1. ถามหนึ่งครั้งต่อช่วงหยุด เมื่อ Silero ยืนยันช่วงหยุด (minSilenceDuration หลังเกณฑ์ offset) ตัวประมวลผลจะส่งเสียงของเทิร์นจนถึงตอนนั้นให้ตัวจำแนก — ตั้งแต่ 0.5 วินาทีก่อนจุดเริ่มพูดที่ VAD ตรวจพบ จนถึงชังก์ปัจจุบัน สูงสุด 8 วินาทีสุดท้าย
  2. จบแล้ว ความน่าจะเป็นเท่ากับหรือสูงกว่า threshold (0.5): .speechEnded ทำงานตามปกติ
  3. รอต่อ ต่ำกว่าเกณฑ์ เซกเมนต์ยังคงเปิดอยู่ ถ้าผู้พูดพูดต่อ เซกเมนต์เดิมจะดำเนินต่อ — ไม่มี .speechStarted ครั้งที่สอง — และช่วงหยุดที่ยืนยันครั้งถัดไปจะถามอีกครั้งด้วยเทิร์นที่ยาวขึ้น
  4. เพดานความเงียบ ถ้าความเงียบยาวถึง maxSilenceDuration (2.0 วินาที นับจากจุดเริ่มหยุด) เซกเมนต์จะจบอยู่ดี ผู้พูดที่เสียงค่อยๆ หายไปจึงยังได้รับคำตอบ endTime ของเซกเมนต์คือจุดที่เสียงพูดหยุด ไม่ใช่จุดที่เพดานหมดเวลา

flush() ปิดเซกเมนต์ที่รอค้างไว้เมื่อสตรีมจบ และ reset() ล้างมันทิ้ง ตัวจำแนกที่โยนข้อผิดพลาดจะถูกนับว่า "จบแล้ว" โมเดลที่ล้มเหลวจึงไม่มีวันทำให้บทสนทนาค้าง isHoldingTurn รายงานสถานะการรอ และ lastTurnCompletionProbability ให้คำตอบล่าสุด

โมเดล

คุณสมบัติค่า
สถาปัตยกรรมตัวเข้ารหัส Whisper-Tiny + attention pooling + หัว MLP ที่มีเอาต์พุต sigmoid
พารามิเตอร์8.0M
อินพุต8 วินาทีสุดท้ายของเสียงโมโน 16 kHz (128,000 ตัวอย่าง); เทิร์นที่สั้นกว่าจะถูกเติมศูนย์ด้านหน้า อัตราสุ่มอื่นจะถูกสุ่มใหม่
เอาต์พุตความน่าจะเป็นใน [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

ไฟล์ที่ส่งออกฝัง front-end แบบ log-mel ของ Whisper ไว้ในตัว รวมถึงการปรับมาตรฐานรูปคลื่นที่โมเดลต้นทางใช้ตอนฝึก ผู้เรียกจึงส่งเสียงดิบเข้าไปได้เลย — ไม่มีการสกัดฟีเจอร์ใน Swift หรือ C++

ประสิทธิภาพ

รูปแบบส่งออกขนาดความแม่นยำความหน่วงต่อหน้าต่าง 8 วินาที
CoreML (CPU + Neural Engine)16.8 MB92.9%3.5 ms
ONNX fp32 (ONNX Runtime, CPU)33 MB92.9%~36 ms ที่ 2 เธรด, 20 ms ที่ 4 เธรด
ONNX int8 (ONNX Runtime, CPU)11.1 MB93.0%~36 ms ที่ 2 เธรด, 20 ms ที่ 4 เธรด

ความแม่นยำวัดจากคลิป 1,000 คลิปในชุดทดสอบต้นทาง; ไฟล์ส่งออก CoreML และ ONNX fp32 ให้ผลตรงกับโมเดล fp32 ต้นทางทุกประการบนคลิปเหล่านั้น ความหน่วงคือหนึ่งหน้าต่าง 8 วินาทีบน Apple M5 Pro การเรียกหนึ่งครั้งต่อช่วงหยุดไม่เพิ่มอะไรที่สังเกตได้ให้กับช่วงหยุดที่ VAD รอไปแล้ว

API Swift

ตรวจสอบครั้งเดียวกับคำพูดที่บันทึกไว้:

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()

SmartTurnModel เป็นไปตามโปรโตคอล TurnCompletionProvider ใน AudioCommon จึงสามารถต่อตัวจำแนกอื่นที่มีรูปแบบเดียวกันแทนได้ fromPretrained(cacheDir:offlineMode:) ใช้กฎแคชและออฟไลน์แบบเดียวกับโมเดลอื่น

VoicePipeline

VoicePipeline รับตัวจำแนกตัวเดียวกันนี้ได้โดยตรง เอเจนต์เสียงจึงเลิกพูดแทรกผู้ใช้โดยไม่ต้องแก้การตั้งค่า STT, TTS หรือ VAD เลย ใช้เพียงสองฟิลด์ของ PipelineConfig กับหนึ่งเมธอด และต้องใช้ speech-swift v0.0.27 ขึ้นไป (SpeechCore.xcframework จาก speech-core v0.0.14)

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.5ความน่าจะเป็นของการจบเทิร์นที่เมื่อถึงหรือสูงกว่า ช่วงหยุดของ VAD จะจบเทิร์นของผู้ใช้ ใช้ก็ต่อเมื่อต่อตัวจำแนกแล้วเท่านั้น
turnCompletionMaxSilence2.0 วินาทีจำนวนวินาทีของความเงียบนับจากจุดเริ่มช่วงหยุด ซึ่งเมื่อครบแล้ว เทิร์นที่ตัวจำแนกปฏิเสธก็จะจบอยู่ดี 0 = ไม่จบเลย
setTurnCompletion(_:)ต่อ TurnCompletionProvider ตัวใดก็ได้ เรียกก่อน start() และส่ง nil เพื่อถอดออก ตัวจำแนกที่โยนข้อผิดพลาดจะถือว่า "จบแล้ว" โมเดลที่ล้มเหลวจึงไม่ทำให้บทสนทนาค้าง

เมื่อต่อตัวจำแนกแล้ว ช่วงหยุดของ VAD จะจบเทิร์นของผู้ใช้ก็ต่อเมื่อความน่าจะเป็นถึงเกณฑ์ หากต่ำกว่านั้นไปป์ไลน์จะฟังต่อ เสียงพูดที่กลับมาจะนับเป็นเทิร์นเดิม (ไม่มี speechStarted ครั้งที่สอง) และ turnCompletionMaxSilence จะจบเทิร์นที่รอค้างอยู่ดี Eager STT ก็เคารพการปฏิเสธนี้ และตัวจำแนกทำงานครั้งเดียวต่อช่วงหยุดบนเธรดเสียง จึงควรเรียก prewarm() หลังโหลด

การปรับแต่ง

การตั้งค่าค่าเริ่มต้นผล
threshold0.5ค่าต่ำกว่าจะจบเทิร์นเร็วขึ้นและพูดแทรกบ่อยขึ้น; ค่าสูงกว่าจะรอนานขึ้นก่อนตอบ สังเกต lastTurnCompletionProbability ในบทสนทนาจริงก่อนปรับ
maxSilenceDuration2.0 วินาทีเพดานตายตัว นับจากจุดเริ่มหยุด ช่วงหยุดที่ถูกปฏิเสธไม่มีทางรั้งเทิร์นไว้นานกว่านี้ 0 ปิดเพดาน: เซกเมนต์ที่รอค้างจะรอเสียงพูดหรือ flush()
preRollDuration0.5 วินาทีเสียงก่อนจุดเริ่มพูดที่ VAD ตรวจพบ ซึ่งถูกรวมเข้าในอินพุตของตัวจำแนก เพื่อให้พยางค์แรกที่ถูกตัดยังไปถึงโมเดล

minSilenceDuration ของ Silero ยังคงเป็นตัวตัดสินว่าช่วงหยุดจะถูกยืนยันเมื่อใด และดังนั้นจึงกำหนดว่าจะถามตัวจำแนกเมื่อใด เรียก 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)
--jsonturnส่งออก probability, threshold, complete และ latency_ms เป็น JSON
--model, -mturnรีโพ HuggingFace ที่มีโครงสร้างเดียวกัน (ค่าเริ่มต้น aufklarer/Smart-Turn-v3.2-CoreML)
--model-dirturnไดเรกทอรีในเครื่องที่มี smart_turn.mlmodelc และ config.json; ข้ามการดาวน์โหลด
--smart-turnvad-streamยืนยันทุกช่วงหยุดด้วย Smart Turn ก่อนจบเซกเมนต์
--turn-thresholdvad-streamเกณฑ์การจบเทิร์นของ Smart Turn (ค่าเริ่มต้น 0.5)
--turn-max-silencevad-streamจำนวนวินาทีของความเงียบหลังช่วงหยุดที่ถูกปฏิเสธ ซึ่งจะจบเซกเมนต์อยู่ดี (ค่าเริ่มต้น 2.0)

speech turn โหลดไฟล์ (อัตราสุ่มใดก็ได้) ให้คะแนน 8 วินาทีสุดท้าย แล้วพิมพ์ความน่าจะเป็น ผ่านเกณฑ์หรือไม่ และเวลาที่ใช้อนุมาน ใช้เพื่อปรับเกณฑ์กับไฟล์บันทึกของคุณเอง รายการแฟล็กทั้งหมดอยู่ในเอกสารอ้างอิง CLI

C++ / Android

Speech Core มีโมเดลเดียวกันในชื่อ OnnxSmartTurn สำหรับ Linux, Windows และ Android (เฉพาะ ONNX Runtime; ไม่มีรุ่น LiteRT) VoicePipeline::set_turn_completion() ต่อมันเข้าไปก่อน start(); จากนั้นช่วงหยุดของ 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);

CLI speech ของ Speech Core ให้คะแนนไฟล์บันทึกแบบเดียวกับบน 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

แหล่งที่มา