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:
@@ -19,6 +19,7 @@ from fabledassistant.services.backup import (
|
||||
restore_full_backup,
|
||||
)
|
||||
from fabledassistant.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email
|
||||
from fabledassistant.services.voice_config import get_voice_config
|
||||
from fabledassistant.services.logging import get_logs, get_log_stats, log_audit
|
||||
from fabledassistant.services.notifications import send_invitation_email
|
||||
from fabledassistant.services.settings import set_setting, set_settings_batch
|
||||
@@ -200,6 +201,33 @@ async def update_base_url():
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@admin_bp.route("/voice", methods=["GET"])
|
||||
@admin_required
|
||||
async def get_voice_config_route():
|
||||
config = await get_voice_config()
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
@admin_bp.route("/voice", methods=["PUT"])
|
||||
@admin_required
|
||||
async def update_voice_config():
|
||||
data = await request.get_json()
|
||||
uid = get_current_user_id()
|
||||
valid_models = {"tiny.en", "base.en", "small.en", "medium.en"}
|
||||
settings: dict[str, str] = {}
|
||||
if "voice_enabled" in data:
|
||||
settings["voice_enabled"] = "true" if data["voice_enabled"] else "false"
|
||||
if "voice_stt_model" in data:
|
||||
model = str(data["voice_stt_model"])
|
||||
if model not in valid_models:
|
||||
return jsonify({"error": f"Invalid STT model. Choose from: {', '.join(sorted(valid_models))}"}), 400
|
||||
settings["voice_stt_model"] = model
|
||||
if settings:
|
||||
await set_settings_batch(uid, settings)
|
||||
await log_audit("voice_config", user_id=uid, username=g.user.username, ip_address=request.remote_addr, details=settings)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@admin_bp.route("/invitations", methods=["POST"])
|
||||
@admin_required
|
||||
async def create_invite():
|
||||
|
||||
@@ -5,7 +5,6 @@ import time
|
||||
from quart import Blueprint, jsonify, request
|
||||
|
||||
from fabledassistant.auth import login_required
|
||||
from fabledassistant.config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,18 +15,22 @@ voice_bp = Blueprint("voice", __name__, url_prefix="/api/voice")
|
||||
@login_required
|
||||
async def voice_status():
|
||||
"""Return availability of STT and TTS services."""
|
||||
if not Config.VOICE_ENABLED:
|
||||
return jsonify({"enabled": False, "stt": False, "tts": False})
|
||||
|
||||
from fabledassistant.services.voice_config import get_voice_config
|
||||
from fabledassistant.services.stt import stt_available
|
||||
from fabledassistant.services.tts import tts_available
|
||||
|
||||
config = await get_voice_config()
|
||||
enabled = config.get("voice_enabled", "false").lower() in ("1", "true", "yes")
|
||||
|
||||
if not enabled:
|
||||
return jsonify({"enabled": False, "stt": False, "tts": False})
|
||||
|
||||
return jsonify({
|
||||
"enabled": True,
|
||||
"stt": stt_available(),
|
||||
"tts": tts_available(),
|
||||
"stt_model": Config.STT_MODEL,
|
||||
"tts_backend": Config.TTS_BACKEND,
|
||||
"stt_model": config.get("voice_stt_model", "base.en"),
|
||||
"tts_backend": "kokoro",
|
||||
})
|
||||
|
||||
|
||||
@@ -35,7 +38,8 @@ async def voice_status():
|
||||
@login_required
|
||||
async def list_voices():
|
||||
"""Return available Kokoro voice IDs and labels."""
|
||||
if not Config.VOICE_ENABLED:
|
||||
from fabledassistant.services.voice_config import is_voice_enabled
|
||||
if not await is_voice_enabled():
|
||||
return jsonify({"error": "Voice feature is disabled"}), 503
|
||||
|
||||
from fabledassistant.services.tts import list_voices, tts_available
|
||||
@@ -54,7 +58,8 @@ async def transcribe_audio():
|
||||
Request: multipart/form-data with field 'audio' (WebM/Opus blob)
|
||||
Response: {"transcript": "...", "duration_ms": 123}
|
||||
"""
|
||||
if not Config.VOICE_ENABLED:
|
||||
from fabledassistant.services.voice_config import is_voice_enabled
|
||||
if not await is_voice_enabled():
|
||||
return jsonify({"error": "Voice feature is disabled"}), 503
|
||||
|
||||
from fabledassistant.services.stt import stt_available, transcribe
|
||||
@@ -95,7 +100,8 @@ async def synthesise_speech():
|
||||
Request body: {"text": "...", "voice": "af_heart", "speed": 1.0}
|
||||
Response: audio/wav bytes
|
||||
"""
|
||||
if not Config.VOICE_ENABLED:
|
||||
from fabledassistant.services.voice_config import is_voice_enabled
|
||||
if not await is_voice_enabled():
|
||||
return jsonify({"error": "Voice feature is disabled"}), 503
|
||||
|
||||
from fabledassistant.services.tts import synthesise, tts_available
|
||||
|
||||
Reference in New Issue
Block a user