[Wake/VAD/STT 与 Live runtime]:完成真实重复语音运行,包含云端ASR/TTS开关、run-live和临时上下文测试

This commit is contained in:
mkbk
2026-06-17 20:00:55 +08:00
parent ac97daa1e7
commit 4b7cd18a0f
20 changed files with 1043 additions and 68 deletions
+109 -2
View File
@@ -1,12 +1,15 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from .models import AudioFrame, AudioSegment, ErrorCode, ProviderError, VadResult
from .speech_models import vad_model_path
class EnergyVadProvider:
def __init__(self, threshold: int = 0) -> None:
def __init__(self, threshold: int = 500) -> None:
self.threshold = threshold
self.loaded = False
self._speech_ms = 0
@@ -47,12 +50,116 @@ class EnergyVadProvider:
return bool(frame.metadata["speech"])
if not frame.pcm:
return False
try:
import struct
sample_count = len(frame.pcm) // 2
if sample_count:
samples = struct.unpack("<" + "h" * sample_count, frame.pcm[: sample_count * 2])
return max(abs(sample) for sample in samples) > self.threshold
except Exception:
pass
return any(abs(byte - 128) > self.threshold for byte in frame.pcm)
class SherpaOnnxVadProvider:
def __init__(self, models_dir: str | Path, threshold: float = 0.5, sherpa_module: Any | None = None) -> None:
self.models_dir = Path(models_dir)
self.threshold = threshold
self.loaded = False
self._sherpa = sherpa_module
self._model: Any | None = None
self._speech_ms = 0
self._silence_ms = 0
def load(self) -> None:
model_path = vad_model_path(self.models_dir)
if not model_path.exists():
raise ProviderError(
ErrorCode.VAD_MODEL_LOAD_FAILED,
f"sherpa-onnx VAD model path does not exist: {model_path}",
False,
"sherpa-onnx-vad",
"vad",
)
sherpa_onnx = self._sherpa
if sherpa_onnx is None:
try:
import sherpa_onnx # type: ignore[import-not-found]
except Exception as exc:
raise ProviderError(
ErrorCode.VAD_MODEL_LOAD_FAILED,
f"sherpa_onnx is not available: {exc}",
False,
"sherpa-onnx-vad",
"vad",
) from exc
try:
config = sherpa_onnx.VadModelConfig(
silero_vad=sherpa_onnx.SileroVadModelConfig(model=str(model_path), threshold=self.threshold),
sample_rate=16000,
)
self._model = sherpa_onnx.VadModel.create(config)
except Exception as exc:
raise ProviderError(
ErrorCode.VAD_MODEL_LOAD_FAILED,
f"failed to load sherpa-onnx VAD model: {exc}",
False,
"sherpa-onnx-vad",
"vad",
) from exc
self.loaded = True
def analyze(self, frame: AudioFrame) -> VadResult:
if not self.loaded or self._model is None:
raise ProviderError(
ErrorCode.VAD_MODEL_LOAD_FAILED,
"sherpa-onnx VAD provider is not loaded",
False,
"sherpa-onnx-vad",
"vad",
)
try:
import numpy as np
samples = np.frombuffer(frame.pcm, dtype=np.int16).astype(np.float32) / 32768.0
window_size = int(self._model.window_size())
if samples.size < window_size:
samples = np.pad(samples, (0, window_size - samples.size))
elif samples.size > window_size:
samples = samples[-window_size:]
is_speech = bool(self._model.is_speech(samples))
except Exception as exc:
raise ProviderError(
ErrorCode.VAD_MODEL_LOAD_FAILED,
f"sherpa-onnx VAD analysis failed: {exc}",
True,
"sherpa-onnx-vad",
"vad",
) from exc
frame_ms = int(frame.metadata.get("duration_ms", 20))
if is_speech:
self._speech_ms += frame_ms
self._silence_ms = 0
else:
self._silence_ms += frame_ms
return VadResult(
is_speech=is_speech,
confidence=0.9 if is_speech else 0.1,
speech_ms=self._speech_ms,
silence_ms=self._silence_ms,
)
def reset(self) -> None:
self._speech_ms = 0
self._silence_ms = 0
if self._model is not None:
self._model.reset()
@dataclass(slots=True)
class VadRecorder:
provider: EnergyVadProvider
provider: Any
min_duration_ms: int = 300
end_silence_ms: int = 200
no_speech_timeout_ms: int = 1000