Turn Detector设计
class TurnDetectionAction(Enum):
DO_NOTHING = 1
STOP_SPEAKING = 2
START_GENERATION = 3
class TurnDetectionSemantic(Enum):
IDLE = "idle"
INCOMPLETE = "incomplete"
COMPLETE = "complete"
WAIT = "wait"
BACKCHANNEL = "backchannel"
SHOULD_BACKCHANNEL = "should_backchannel"
class TurnVADResult(Enum):
SPEECH = 1
SILENCE = 2
@dataclass(frozen=True)
class TurnDetectionResult:
action: TurnDetectionAction
semantic: TurnDetectionSemantic
vad_result: TurnVADResult | None = None
class TurnDetector(ABC):
"""Abstract interface for turn-taking detectors."""
@property
def listening(self) -> bool:
...
@listening.setter
def listening(self, value: bool) -> None:
...
def listening_lock(self, is_async: bool = True):
...
@abstractmethod
def detect(
self,
audio: Optional[bytes] = None,
text: Optional[str] = None,
speech_start: bool = False,
speech_pause: Optional[bool] = None,
) -> TurnDetectionResult:
...
async def async_detect(
self,
audio: Optional[bytes] = None,
text: Optional[str] = None,
speech_start: bool = False,
speech_pause: Optional[bool] = None,
) -> TurnDetectionResult:
...
@abstractmethod
def clone(self) -> "TurnDetector":
...
detect实现的最佳实践
框架中实际调用的是 async_detect。因此,实现新的 turn detector 时,最佳实践是首先实现 async_detect,再在 detect 中对其进行同步包装。
import asyncio
def detect(
self,
audio: Optional[bytes] = None,
text: Optional[str] = None,
speech_start: bool = False,
speech_pause: Optional[bool] = None,
) -> TurnDetectionResult:
return asyncio.run(
self.async_detect(audio, text, speech_start, speech_pause)
)
如果底层实现本身已经是同步的,也可以直接实现 detect,并复用基类提供的 async_detect 默认包装。
async_detect说明
TurnDetector 支持同时消费音频信号、ASR 文本和 VAD 侧信号。每次调用都应返回一个 TurnDetectionResult,表示当前时刻的轮次判断结果。
输入参数
audio:当前音频帧,格式为 PCM 16-bit、单声道、16 kHz 字节流。text:当前轮次截至目前的 ASR 文本。speech_start:VAD 刚检测到说话开始时传入的信号。speech_pause:用户当前可能出现停顿时传入的信号,通常与text一起使用。
这些参数同时出现的组合如下:
- 仅传入
audio,走纯音频判定路径(音频路径) - 仅传入
text和speech_pause,走文本语义判定路径(文本路径) - 仅传入
speech_start=True,通知 detector 当前说话轮次开始(辅助信号)
当前仓库中的两个典型实现分别代表了两种路径:
SoulxDuplug:以音频路径为主,并在文本停顿信号上提供 fallbackLLMTurnDetector:以文本语义路径为主,主要依赖text与speech_pause
返回值
返回值为 TurnDetectionResult,由三部分组成:
action:服务层应立即执行的动作semantic:当前会话状态的语义解释vad_result:可选的 VAD 结果,仅在 detector 需要代理输出 VAD 状态时使用
TurnDetectionAction语义
DO_NOTHING:当前不触发额外动作STOP_SPEAKING:当前应中断系统正在播放的语音START_GENERATION:当前应开始生成系统回复
其中:
STOP_SPEAKING一般用于用户打断系统说话START_GENERATION一般用于确认用户已经说完,已可开始回答
TurnDetectionSemantic语义
IDLE:当前没有明确的轮次推进信号INCOMPLETE:用户仍在继续当前轮次,尚未说完COMPLETE:用户当前输入在语义上已完整WAIT:用户明确表达了等待语义BACKCHANNEL:用户输入属于短促附和,不应作为正式轮次完成SHOULD_BACKCHANNEL:当前状态提示系统可以产生 backchannel
semantic 主要用于表达 detector 的语义判断(未来用于LLM的精细语义控制),而 action 决定服务层的即时行为。两者相关但不等价。
vad_result语义
vad_result 为可选字段,仅在 turn detector 同时承担 VAD 代理职责时使用。
TurnVADResult.SPEECH:当前处于说话状态TurnVADResult.SILENCE:当前处于静音状态
当 pipeline 中未配置独立 VAD 时,系统使用该字段触发VAD。 如果前端或后端配置了独立 VAD,该字段无效。
listening语义
TurnDetector 基类内置了 listening 状态及其锁。该状态用于区分 detector 当前是在“监听用户完成输入”,还是在“监听用户是否打断系统输出”。
常见约定如下:
listening = True:系统正在等待用户说完,应判断何时START_GENERATIONlistening = False:系统正在播放输出,应判断用户输入是否会触发STOP_SPEAKING
服务层中的 TurnDetectorManager 会在 TTS 开始播放时将 listening 置为 False,并在播放结束或被中断后恢复为 True。
实现建议
- 每次返回结果时,应保证
action与semantic语义一致。 - 如果 detector 内部维护会话状态,应确保该状态只属于当前实例,不应跨 session 共享。
- 若实现使用
speech_pause,应将其视为“当前出现停顿”的提示,而不是 turn 结束后的重置信号。