mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Split the legacy core speech stack into two built-in, independently toggleable plugins: `_kokoro_tts` for TTS and `_whisper_stt` for STT. This refactor keeps dependency installation and bootstrap concerns in Docker/bootstrap/preload, while moving speech-specific tooling, APIs, prompts, UI, and runtime behavior into the plugins. Core now exposes engine-agnostic `tts-service` and `stt-service` brokers, with browser-native TTS preserved as the fallback when Kokoro is disabled. Included in this change: - add built-in `_kokoro_tts` plugin with plugin-owned synth API, config, status UI, and provider registration - add built-in `_whisper_stt` plugin with plugin-owned transcribe API, mic runtime, device UI, prompt injection, and provider registration - remove legacy core speech APIs/helpers/settings/UI and delete unused `webui/js/speech_browser.js` - replace the old hardcoded speech settings section with a generic voice surface backed by plugin extensions - update preload/docs/tests to match the new plugin-owned speech architecture Behavioral intent: - both plugins are built-in but not `always_enabled` - users can now hot-switch TTS and STT independently - browser TTS remains available when `_kokoro_tts` is off - Whisper mic UI only appears when `_whisper_stt` is enabled
98 lines
2.4 KiB
Python
98 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from helpers import files, plugins
|
|
|
|
|
|
PLUGIN_NAME = "_whisper_stt"
|
|
LEGACY_SETTINGS_FILE = files.get_abs_path("usr/settings.json")
|
|
DEFAULT_CONFIG = {
|
|
"model_size": "base",
|
|
"language": "en",
|
|
"message_mode": "send",
|
|
"silence_threshold": 0.3,
|
|
"silence_duration": 1000,
|
|
"waiting_timeout": 2000,
|
|
}
|
|
|
|
|
|
def ensure_config_seeded() -> bool:
|
|
config_path = get_config_path()
|
|
if files.exists(config_path):
|
|
return False
|
|
|
|
config = build_seed_config(_read_legacy_settings())
|
|
files.write_file(config_path, json.dumps(config, indent=2))
|
|
return True
|
|
|
|
|
|
def get_config_path() -> str:
|
|
return plugins.determine_plugin_asset_path(
|
|
PLUGIN_NAME, "", "", plugins.CONFIG_FILE_NAME
|
|
)
|
|
|
|
|
|
def read_saved_config() -> dict[str, Any]:
|
|
config_path = get_config_path()
|
|
if not files.exists(config_path):
|
|
return {}
|
|
|
|
try:
|
|
return json.loads(files.read_file(config_path))
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def build_seed_config(legacy_settings: dict[str, Any]) -> dict[str, Any]:
|
|
seeded = dict(DEFAULT_CONFIG)
|
|
|
|
model_size = str(
|
|
legacy_settings.get("stt_model_size", seeded["model_size"]) or ""
|
|
).strip()
|
|
if model_size:
|
|
seeded["model_size"] = model_size
|
|
|
|
language = str(legacy_settings.get("stt_language", seeded["language"]) or "").strip()
|
|
if language:
|
|
seeded["language"] = language
|
|
|
|
seeded["silence_threshold"] = _coerce_float(
|
|
legacy_settings.get("stt_silence_threshold"),
|
|
seeded["silence_threshold"],
|
|
)
|
|
seeded["silence_duration"] = _coerce_int(
|
|
legacy_settings.get("stt_silence_duration"),
|
|
seeded["silence_duration"],
|
|
)
|
|
seeded["waiting_timeout"] = _coerce_int(
|
|
legacy_settings.get("stt_waiting_timeout"),
|
|
seeded["waiting_timeout"],
|
|
)
|
|
|
|
return seeded
|
|
|
|
|
|
def _read_legacy_settings() -> dict[str, Any]:
|
|
if not files.exists(LEGACY_SETTINGS_FILE):
|
|
return {}
|
|
|
|
try:
|
|
return json.loads(files.read_file(LEGACY_SETTINGS_FILE))
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _coerce_float(value: Any, default: float) -> float:
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _coerce_int(value: Any, default: int) -> int:
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return default
|