[依赖、模型和配置]:完成本地语音模型准备,包含.env配置、模型下载脚本和model-check
This commit is contained in:
@@ -12,6 +12,7 @@ from .conversation import ConversationContext
|
||||
from .llm import MockLlmProvider, OpenAICompatibleLlmProvider
|
||||
from .models import AudioFrame, ProviderError
|
||||
from .pipeline import VoicePipeline
|
||||
from .speech_models import check_speech_models, model_status_errors
|
||||
from .stt import MetadataSttProvider
|
||||
from .transport import MemoryAudioTransport
|
||||
from .tts import SineTtsProvider
|
||||
@@ -27,6 +28,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
subparsers.add_parser("acceptance", help="Run deterministic end-to-end pipeline acceptance")
|
||||
subparsers.add_parser("validate-assets", help="Validate project pet assets")
|
||||
subparsers.add_parser("security-check", help="Scan tracked files for leaked API keys")
|
||||
model_check = subparsers.add_parser("model-check", help="Validate local speech model files")
|
||||
model_check.add_argument("--models-dir", default=None, help="Speech models directory. Defaults to .env or models")
|
||||
smoke = subparsers.add_parser("llm-smoke", help="Call configured OpenAI/NewAPI endpoint")
|
||||
smoke.add_argument("--message", default="用一句中文回复:小杰在线。")
|
||||
smoke.add_argument("--no-stream", action="store_true")
|
||||
@@ -46,6 +49,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"llm_stream": config.llm_stream,
|
||||
"llm_api_key_present": bool(config.llm_api_key),
|
||||
"asset_dir": str(config.asset_dir),
|
||||
"speech_models_dir": str(config.speech_models_dir),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
@@ -63,6 +67,16 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(json.dumps({"secret_leaks": leaks, "valid": not leaks}, ensure_ascii=False, sort_keys=True))
|
||||
return 1 if leaks else 0
|
||||
|
||||
if args.command == "model-check":
|
||||
config = AppConfig.from_dotenv(args.env_file)
|
||||
models_dir = Path(args.models_dir) if args.models_dir else config.speech_models_dir
|
||||
status = check_speech_models(models_dir, require_sherpa=True)
|
||||
errors = model_status_errors(status)
|
||||
data = status.to_json()
|
||||
data["errors"] = [str(error) for error in errors]
|
||||
print(json.dumps(data, ensure_ascii=False, sort_keys=True))
|
||||
return 1 if errors else 0
|
||||
|
||||
if args.command == "acceptance":
|
||||
result = run_acceptance()
|
||||
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
||||
@@ -84,6 +98,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
audio_output_device=config.audio_output_device,
|
||||
asset_dir=config.asset_dir,
|
||||
log_dir=config.log_dir,
|
||||
speech_models_dir=config.speech_models_dir,
|
||||
context_max_messages=config.context_max_messages,
|
||||
context_max_chars=config.context_max_chars,
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ class AppConfig:
|
||||
audio_output_device: str | None = None
|
||||
asset_dir: Path = Path("assets/pet")
|
||||
log_dir: Path = Path("logs")
|
||||
speech_models_dir: Path = Path("models")
|
||||
context_max_messages: int = 12
|
||||
context_max_chars: int = 12000
|
||||
|
||||
@@ -44,6 +45,7 @@ class AppConfig:
|
||||
audio_output_device=get("AUDIO_OUTPUT_DEVICE"),
|
||||
asset_dir=Path(get("ASSET_DIR", "assets/pet") or "assets/pet"),
|
||||
log_dir=Path(get("LOG_DIR", "logs") or "logs"),
|
||||
speech_models_dir=Path(get("SPEECH_MODELS_DIR", "models") or "models"),
|
||||
context_max_messages=int(get("CONTEXT_MAX_MESSAGES", "12") or "12"),
|
||||
context_max_chars=int(get("CONTEXT_MAX_CHARS", "12000") or "12000"),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .models import ErrorCode, ProviderError
|
||||
|
||||
DEFAULT_VAD_URL = "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx"
|
||||
DEFAULT_STT_URL = (
|
||||
"https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/"
|
||||
"sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23.tar.bz2"
|
||||
)
|
||||
DEFAULT_STT_DIR = "sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23"
|
||||
|
||||
REQUIRED_MODEL_FILES = (
|
||||
"vad/silero_vad.onnx",
|
||||
f"stt/{DEFAULT_STT_DIR}/tokens.txt",
|
||||
f"stt/{DEFAULT_STT_DIR}/encoder-epoch-99-avg-1.int8.onnx",
|
||||
f"stt/{DEFAULT_STT_DIR}/decoder-epoch-99-avg-1.onnx",
|
||||
f"stt/{DEFAULT_STT_DIR}/joiner-epoch-99-avg-1.int8.onnx",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SpeechModelStatus:
|
||||
models_dir: Path
|
||||
manifest_path: Path
|
||||
manifest_present: bool
|
||||
required_files: tuple[str, ...]
|
||||
missing_files: tuple[str, ...]
|
||||
sherpa_onnx_available: bool
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return not self.missing_files and self.sherpa_onnx_available
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
return {
|
||||
"ok": self.ok,
|
||||
"models_dir": str(self.models_dir),
|
||||
"manifest_path": str(self.manifest_path),
|
||||
"manifest_present": self.manifest_present,
|
||||
"required_files": list(self.required_files),
|
||||
"missing_files": list(self.missing_files),
|
||||
"sherpa_onnx_available": self.sherpa_onnx_available,
|
||||
}
|
||||
|
||||
|
||||
def default_manifest() -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "owner_voice_pet.speech_models.v1",
|
||||
"sample_rate": 16000,
|
||||
"sources": {
|
||||
"vad": DEFAULT_VAD_URL,
|
||||
"stt": DEFAULT_STT_URL,
|
||||
},
|
||||
"providers": {
|
||||
"vad": {
|
||||
"type": "silero-vad",
|
||||
"path": "vad/silero_vad.onnx",
|
||||
},
|
||||
"stt": {
|
||||
"type": "sherpa-onnx-streaming-transducer",
|
||||
"model_dir": f"stt/{DEFAULT_STT_DIR}",
|
||||
"tokens": f"stt/{DEFAULT_STT_DIR}/tokens.txt",
|
||||
"encoder": f"stt/{DEFAULT_STT_DIR}/encoder-epoch-99-avg-1.int8.onnx",
|
||||
"decoder": f"stt/{DEFAULT_STT_DIR}/decoder-epoch-99-avg-1.onnx",
|
||||
"joiner": f"stt/{DEFAULT_STT_DIR}/joiner-epoch-99-avg-1.int8.onnx",
|
||||
},
|
||||
},
|
||||
"required_files": list(REQUIRED_MODEL_FILES),
|
||||
}
|
||||
|
||||
|
||||
def write_default_manifest(models_dir: str | Path) -> Path:
|
||||
root = Path(models_dir)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = root / "manifest.json"
|
||||
manifest_path.write_text(
|
||||
json.dumps(default_manifest(), ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest_path
|
||||
|
||||
|
||||
def load_manifest(models_dir: str | Path) -> dict[str, Any]:
|
||||
manifest_path = Path(models_dir) / "manifest.json"
|
||||
if not manifest_path.exists():
|
||||
return default_manifest()
|
||||
return json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def required_model_files(models_dir: str | Path) -> tuple[str, ...]:
|
||||
manifest = load_manifest(models_dir)
|
||||
files = manifest.get("required_files", list(REQUIRED_MODEL_FILES))
|
||||
return tuple(str(item) for item in files)
|
||||
|
||||
|
||||
def check_speech_models(models_dir: str | Path, require_sherpa: bool = True) -> SpeechModelStatus:
|
||||
root = Path(models_dir)
|
||||
manifest_path = root / "manifest.json"
|
||||
required = required_model_files(root)
|
||||
missing = tuple(relative for relative in required if not (root / relative).exists())
|
||||
sherpa_available = True
|
||||
if require_sherpa:
|
||||
sherpa_available = importlib.util.find_spec("sherpa_onnx") is not None
|
||||
return SpeechModelStatus(
|
||||
models_dir=root,
|
||||
manifest_path=manifest_path,
|
||||
manifest_present=manifest_path.exists(),
|
||||
required_files=required,
|
||||
missing_files=missing,
|
||||
sherpa_onnx_available=sherpa_available,
|
||||
)
|
||||
|
||||
|
||||
def model_status_errors(status: SpeechModelStatus) -> list[ProviderError]:
|
||||
errors: list[ProviderError] = []
|
||||
if not status.sherpa_onnx_available:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.STT_TRANSCRIBE_FAILED,
|
||||
"sherpa_onnx is not installed in the active Python environment",
|
||||
False,
|
||||
"sherpa-onnx",
|
||||
"model-check",
|
||||
)
|
||||
)
|
||||
if status.missing_files:
|
||||
errors.append(
|
||||
ProviderError(
|
||||
ErrorCode.STT_MODEL_MISSING,
|
||||
"missing speech model files: " + ", ".join(status.missing_files),
|
||||
False,
|
||||
"speech-models",
|
||||
"model-check",
|
||||
)
|
||||
)
|
||||
return errors
|
||||
Reference in New Issue
Block a user