agent-zero/plugins/_whisper_stt/helpers/migration.py
Alessandro 675afa8dee
Some checks are pending
Build And Publish Docker Images / plan (push) Waiting to run
Build And Publish Docker Images / build (push) Blocked by required conditions
Refactor speech stack into built-in Kokoro TTS and Whisper STT plugins
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
2026-05-21 05:41:59 +02:00

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