feat: move voice enable/model config to admin UI
Replace VOICE_ENABLED env var gate with DB-backed admin setting. - services/voice_config.py: reads voice_enabled + voice_stt_model from admin user's settings row (falls back to env var defaults) - routes/admin.py: GET/PUT /api/admin/voice for admin configuration - routes/voice.py, services/stt.py, services/tts.py: read enabled/model from DB via voice_config instead of Config directly - app.py: always schedule model loaders at startup; they self-gate on the DB setting so no conditional needed at the call site - SettingsView.vue: Voice section in Admin → Config tab (enable toggle + STT model dropdown); user Voice tab now points to admin panel when disabled No env var required to test — enable via Settings → Admin → Config → Voice. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,11 @@ _load_error: str | None = None
|
||||
|
||||
|
||||
async def load_stt_model() -> None:
|
||||
"""Load the Whisper model. Called once at startup when VOICE_ENABLED=true."""
|
||||
"""Load the Whisper model. Called once at startup when voice is enabled."""
|
||||
global _model, _load_error
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.voice_config import get_stt_model, is_voice_enabled
|
||||
|
||||
if not Config.VOICE_ENABLED:
|
||||
if not await is_voice_enabled():
|
||||
return
|
||||
|
||||
async with _model_lock:
|
||||
@@ -29,15 +29,16 @@ async def load_stt_model() -> None:
|
||||
try:
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
logger.info("Loading Whisper STT model '%s'...", Config.STT_MODEL)
|
||||
model_name = await get_stt_model()
|
||||
logger.info("Loading Whisper STT model '%s'...", model_name)
|
||||
loop = asyncio.get_running_loop()
|
||||
_model = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: WhisperModel(Config.STT_MODEL, device="cpu", compute_type="int8"),
|
||||
lambda: WhisperModel(model_name, device="cpu", compute_type="int8"),
|
||||
)
|
||||
logger.info("Whisper STT model '%s' loaded", Config.STT_MODEL)
|
||||
logger.info("Whisper STT model '%s' loaded", model_name)
|
||||
except Exception:
|
||||
_load_error = f"Failed to load Whisper model '{Config.STT_MODEL}'"
|
||||
_load_error = "Failed to load Whisper STT model"
|
||||
logger.exception(_load_error)
|
||||
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ _VOICES: list[dict] = [
|
||||
|
||||
|
||||
async def load_tts_model() -> None:
|
||||
"""Load the Kokoro pipeline. Called once at startup when VOICE_ENABLED=true."""
|
||||
"""Load the Kokoro pipeline. Called once at startup when voice is enabled."""
|
||||
global _pipeline, _load_error
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.voice_config import is_voice_enabled
|
||||
|
||||
if not Config.VOICE_ENABLED:
|
||||
if not await is_voice_enabled():
|
||||
return
|
||||
|
||||
async with _pipeline_lock:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""System-wide voice configuration stored in the admin user's settings row.
|
||||
|
||||
Falls back to Config env vars so operators can still seed defaults via the
|
||||
environment without touching the UI.
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.models import async_session
|
||||
from fabledassistant.models.setting import Setting
|
||||
from fabledassistant.models.user import User
|
||||
|
||||
VOICE_SETTING_KEYS = ("voice_enabled", "voice_stt_model")
|
||||
|
||||
|
||||
async def get_voice_config() -> dict[str, str]:
|
||||
"""Return voice config, preferring DB values over env-var defaults."""
|
||||
config: dict[str, str] = {
|
||||
"voice_enabled": "true" if Config.VOICE_ENABLED else "false",
|
||||
"voice_stt_model": Config.STT_MODEL,
|
||||
}
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Setting)
|
||||
.join(User, Setting.user_id == User.id)
|
||||
.where(User.role == "admin", Setting.key.in_(VOICE_SETTING_KEYS))
|
||||
)
|
||||
for setting in result.scalars().all():
|
||||
config[setting.key] = setting.value
|
||||
return config
|
||||
|
||||
|
||||
async def is_voice_enabled() -> bool:
|
||||
config = await get_voice_config()
|
||||
return config.get("voice_enabled", "false").lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
async def get_stt_model() -> str:
|
||||
config = await get_voice_config()
|
||||
return config.get("voice_stt_model", Config.STT_MODEL)
|
||||
Reference in New Issue
Block a user