[低延迟端点]:完成首句保留和快速结束修正,包含ACK缓冲策略、批量读帧和主说话人端点回归测试

This commit is contained in:
mkbk
2026-06-17 22:04:25 +08:00
parent e74ec28e7d
commit a64bb86da4
15 changed files with 136 additions and 15 deletions
+2
View File
@@ -62,6 +62,7 @@ def main(argv: list[str] | None = None) -> int:
"pipeline_mode": config.pipeline_mode,
"endpoint_mode": config.endpoint_mode,
"speaker_profile_ms": config.speaker_profile_ms,
"speaker_profile_min_ms": config.speaker_profile_min_ms,
"speaker_absent_ms": config.speaker_absent_ms,
"speaker_similarity_threshold": config.speaker_similarity_threshold,
"speaker_min_rms": config.speaker_min_rms,
@@ -164,6 +165,7 @@ def main(argv: list[str] | None = None) -> int:
pipeline_mode=config.pipeline_mode,
endpoint_mode=config.endpoint_mode,
speaker_profile_ms=config.speaker_profile_ms,
speaker_profile_min_ms=config.speaker_profile_min_ms,
speaker_absent_ms=config.speaker_absent_ms,
speaker_similarity_threshold=config.speaker_similarity_threshold,
speaker_min_rms=config.speaker_min_rms,
+5 -2
View File
@@ -25,10 +25,11 @@ class AppConfig:
wake_kws_threshold: float = 0.15
wake_kws_score: float = 1.0
wake_ack_text: str = "我在"
post_playback_drain_ms: int = 50
post_playback_drain_ms: int = 0
pipeline_mode: str = "live_turn_based"
endpoint_mode: str = "primary_speaker"
speaker_profile_ms: int = 600
speaker_profile_min_ms: int = 120
speaker_absent_ms: int = 300
speaker_similarity_threshold: float = 0.70
speaker_min_rms: float = 0.012
@@ -73,10 +74,11 @@ class AppConfig:
wake_kws_threshold=float(get("WAKE_KWS_THRESHOLD", "0.15") or "0.15"),
wake_kws_score=float(get("WAKE_KWS_SCORE", "1.0") or "1.0"),
wake_ack_text=get("WAKE_ACK_TEXT", "我在") or "我在",
post_playback_drain_ms=int(get("POST_PLAYBACK_DRAIN_MS", "50") or "50"),
post_playback_drain_ms=int(get("POST_PLAYBACK_DRAIN_MS", "0") or "0"),
pipeline_mode=(get("PIPELINE_MODE", "live_turn_based") or "live_turn_based").lower(),
endpoint_mode=(get("ENDPOINT_MODE", "primary_speaker") or "primary_speaker").lower(),
speaker_profile_ms=int(get("SPEAKER_PROFILE_MS", "600") or "600"),
speaker_profile_min_ms=int(get("SPEAKER_PROFILE_MIN_MS", "120") or "120"),
speaker_absent_ms=int(get("SPEAKER_ABSENT_MS", "300") or "300"),
speaker_similarity_threshold=float(
get("SPEAKER_SIMILARITY_THRESHOLD", "0.70") or "0.70"
@@ -216,6 +218,7 @@ class AppConfig:
)
for name, value in {
"OWNER_SPEAKER_PROFILE_MS": self.speaker_profile_ms,
"OWNER_SPEAKER_PROFILE_MIN_MS": self.speaker_profile_min_ms,
"OWNER_SPEAKER_ABSENT_MS": self.speaker_absent_ms,
}.items():
if value <= 0:
+1
View File
@@ -357,6 +357,7 @@ def build_live_runtime(config: AppConfig, reporter: RuntimeReporter | None = Non
recorder_kwargs.update(
{
"speaker_profile_ms": config.speaker_profile_ms,
"speaker_profile_min_ms": config.speaker_profile_min_ms,
"speaker_absent_ms": config.speaker_absent_ms,
"similarity_threshold": config.speaker_similarity_threshold,
"min_rms": config.speaker_min_rms,
+7 -1
View File
@@ -223,9 +223,15 @@ class SoundDeviceAudioTransport:
return []
timeout_s = max(0, timeout_ms) / 1000
try:
return [self._queue.get(timeout=timeout_s)]
frames = [self._queue.get(timeout=timeout_s)]
except queue.Empty:
return []
while True:
try:
frames.append(self._queue.get_nowait())
except queue.Empty:
break
return frames
def play_pcm(self, segment: AudioSegment, interrupt: bool = False) -> PlaybackResult:
if self._sd is None:
+3 -2
View File
@@ -287,6 +287,7 @@ class VadRecorder:
@dataclass(slots=True)
class PrimarySpeakerVadRecorder(VadRecorder):
speaker_profile_ms: int = 600
speaker_profile_min_ms: int = 120
speaker_absent_ms: int = 300
similarity_threshold: float = 0.70
min_rms: float = 0.012
@@ -336,7 +337,7 @@ class PrimarySpeakerVadRecorder(VadRecorder):
self.primary_absent_ms = 0
else:
self.primary_absent_ms += frame_ms
if self.primary_absent_ms >= self.speaker_absent_ms and duration >= self.min_duration_ms:
if self.primary_absent_ms >= self.speaker_absent_ms:
return self._build_segment("primary_speaker_absent")
if result.silence_ms >= self.end_silence_ms and duration >= self.min_duration_ms:
return self._build_segment("silence")
@@ -365,7 +366,7 @@ class PrimarySpeakerVadRecorder(VadRecorder):
self.profile.vector = tuple(averaged)
def _profile_ready(self) -> bool:
minimum_ms = min(self.speaker_profile_ms, max(120, self.min_duration_ms))
minimum_ms = min(self.speaker_profile_ms, self.speaker_profile_min_ms)
return self.profile.speech_ms >= minimum_ms and (
self.profile.speaker_id is not None or self.profile.vector is not None
)