[实时字幕端点]:完成无新文字快速结束录音,包含1.5秒停滞配置、Capture端点和回归测试

This commit is contained in:
mkbk
2026-06-18 11:23:20 +08:00
parent 39da9dd09f
commit 1e956f5eb6
13 changed files with 151 additions and 5 deletions
+23 -4
View File
@@ -163,6 +163,7 @@ class TurnController:
self.vad_recorder.provider.reset()
self.audio_preprocessor.reset()
realtime_session = self._start_realtime_transcript()
last_partial_ms: int | None = None
self._event(CAPTURE_STARTED, PipelineState.RECORDING, state_message, turn_id=turn_id)
while True:
frames = self.transport.read_frames(timeout_ms=100)
@@ -180,7 +181,8 @@ class TurnController:
if isinstance(result, ProviderError):
return result
if self.vad_recorder.started and realtime_session is not None:
self._emit_realtime_transcript(realtime_session, frame, turn_id)
if self._emit_realtime_transcript(realtime_session, frame, turn_id):
last_partial_ms = frame.timestamp_ms
if isinstance(result, AudioSegment):
if realtime_session is not None:
self._finish_realtime_transcript(realtime_session, turn_id)
@@ -192,6 +194,18 @@ class TurnController:
payload={"end_reason": result.metadata.get("end_reason", "")},
)
return result
if self._should_end_after_realtime_idle(last_partial_ms, frame.timestamp_ms):
if realtime_session is not None:
self._finish_realtime_transcript(realtime_session, turn_id)
result = self.vad_recorder.finish("partial_transcript_idle")
self._event(
SPEECH_ENDED,
PipelineState.RECORDING,
"用户语音结束",
turn_id=turn_id,
payload={"end_reason": result.metadata.get("end_reason", "")},
)
return result
def _start_realtime_transcript(self) -> RealtimeTranscriptSession | None:
if not self.config.realtime_transcript_enabled or self.realtime_stt is None:
@@ -203,14 +217,19 @@ class TurnController:
realtime_session: RealtimeTranscriptSession,
frame: AudioFrame,
turn_id: int,
) -> None:
) -> bool:
transcript = realtime_session.accept_frame(frame)
if transcript is None:
return
return False
text = transcript.normalized_text
if not is_valid_transcript_text(text):
return
return False
self._event(TRANSCRIPT_PARTIAL, PipelineState.RECORDING, "", turn_id=turn_id, payload={"text": text})
return True
def _should_end_after_realtime_idle(self, last_partial_ms: int | None, current_ms: int) -> bool:
timeout_ms = self.config.realtime_transcript_idle_timeout_ms
return timeout_ms > 0 and last_partial_ms is not None and current_ms - last_partial_ms >= timeout_ms
def _finish_realtime_transcript(
self,
+2
View File
@@ -65,6 +65,7 @@ def main(argv: list[str] | None = None) -> int:
"llm_api_style": config.llm_api_style,
"llm_stream": config.llm_stream,
"realtime_transcript_enabled": config.realtime_transcript_enabled,
"realtime_transcript_idle_timeout_ms": config.realtime_transcript_idle_timeout_ms,
"llm_api_key_present": bool(config.llm_api_key),
"asset_dir": str(config.asset_dir),
"wake_provider": config.wake_provider,
@@ -201,6 +202,7 @@ def main(argv: list[str] | None = None) -> int:
llm_api_style=config.llm_api_style,
llm_stream=False,
realtime_transcript_enabled=config.realtime_transcript_enabled,
realtime_transcript_idle_timeout_ms=config.realtime_transcript_idle_timeout_ms,
audio_input_device=config.audio_input_device,
audio_output_device=config.audio_output_device,
asset_dir=config.asset_dir,
+14
View File
@@ -17,6 +17,7 @@ class AppConfig:
llm_api_style: str = "chat_completions"
llm_stream: bool = True
realtime_transcript_enabled: bool = True
realtime_transcript_idle_timeout_ms: int = 1500
audio_input_device: str | None = None
audio_output_device: str | None = None
asset_dir: Path = Path("assets/pet")
@@ -71,6 +72,9 @@ class AppConfig:
llm_stream=(get("LLM_STREAM", "1") or "1").lower() not in {"0", "false", "no"},
realtime_transcript_enabled=(get("REALTIME_TRANSCRIPT_ENABLED", "1") or "1").lower()
not in {"0", "false", "no"},
realtime_transcript_idle_timeout_ms=int(
get("REALTIME_TRANSCRIPT_IDLE_TIMEOUT_MS", "1500") or "1500"
),
audio_input_device=get("AUDIO_INPUT_DEVICE"),
audio_output_device=get("AUDIO_OUTPUT_DEVICE"),
asset_dir=Path(get("ASSET_DIR", "assets/pet") or "assets/pet"),
@@ -165,6 +169,16 @@ class AppConfig:
"startup",
)
)
if self.realtime_transcript_idle_timeout_ms < 0:
errors.append(
ProviderError(
ErrorCode.CONFIG_MISSING_VALUE,
"OWNER_REALTIME_TRANSCRIPT_IDLE_TIMEOUT_MS must be non-negative",
False,
"config",
"startup",
)
)
if self.wake_provider not in {"local_kws"}:
errors.append(
ProviderError(
+3
View File
@@ -283,6 +283,9 @@ class VadRecorder:
self.provider.reset()
return segment
def finish(self, end_reason: str) -> AudioSegment:
return self._build_segment(end_reason)
@dataclass(slots=True)
class PrimarySpeakerVadRecorder(VadRecorder):