[实时字幕端点]:完成无新文字快速结束录音,包含1.5秒停滞配置、Capture端点和回归测试
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user