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:
2026-03-29 20:22:18 -04:00
parent 6f84d90dff
commit eaf70500b8
7 changed files with 163 additions and 28 deletions
+64 -3
View File
@@ -445,6 +445,29 @@ const baseUrl = ref("");
const savingBaseUrl = ref(false);
const baseUrlSaved = ref(false);
// Voice config (admin only)
const adminVoiceEnabled = ref(false);
const adminVoiceSttModel = ref("base.en");
const savingAdminVoice = ref(false);
const adminVoiceSaved = ref(false);
async function saveAdminVoice() {
savingAdminVoice.value = true;
adminVoiceSaved.value = false;
try {
await apiPut("/api/admin/voice", {
voice_enabled: adminVoiceEnabled.value,
voice_stt_model: adminVoiceSttModel.value,
});
adminVoiceSaved.value = true;
setTimeout(() => { adminVoiceSaved.value = false; }, 2000);
// Refresh the user-facing voice status after toggling
voiceTabLoaded.value = false;
} finally {
savingAdminVoice.value = false;
}
}
// Search test (SearXNG)
const searxngConfigured = ref(false);
const searxngUrl = ref("");
@@ -565,6 +588,13 @@ onMounted(async () => {
} catch {
// base URL not configured yet
}
try {
const vc = await apiGet<Record<string, string>>("/api/admin/voice");
adminVoiceEnabled.value = vc.voice_enabled === "true";
adminVoiceSttModel.value = vc.voice_stt_model ?? "base.en";
} catch {
// voice not configured yet
}
}
_loadTabContent(activeTab.value);
});
@@ -1888,9 +1918,8 @@ function formatUserDate(iso: string): string {
</div>
<div v-if="!voiceStatus?.enabled" class="field-hint" style="margin-top:0.75rem;">
Set <code>VOICE_ENABLED=true</code> in your server environment to activate voice features.
Optionally set <code>STT_MODEL</code> (tiny.en / base.en / small.en / medium.en) and
install <code>pip install fabledassistant[voice]</code>.
Voice is disabled. An administrator can enable it under
<strong>Admin → Config → Voice</strong>.
</div>
</section>
@@ -2146,6 +2175,38 @@ FABLE_API_KEY=&lt;your-api-key&gt;</pre>
</div>
</section>
<section class="settings-section full-width">
<h2>Voice (Speech-to-Speech)</h2>
<p class="section-desc">
Enable self-hosted voice using faster-whisper (STT) and Kokoro (TTS).
Models load at startup — a server restart is required after changing these settings.
Install dependencies first: <code>pip install faster-whisper kokoro soundfile</code>
</p>
<div class="checkbox-field">
<label>
<input type="checkbox" v-model="adminVoiceEnabled" />
Enable Voice
</label>
<p class="field-hint">Shows the PTT button and voice settings to all users.</p>
</div>
<div v-if="adminVoiceEnabled" class="field" style="max-width:280px; margin-top:0.75rem;">
<label for="stt-model">STT Model</label>
<select id="stt-model" v-model="adminVoiceSttModel" class="input">
<option value="tiny.en">tiny.en — fastest, lowest accuracy (~75 MB)</option>
<option value="base.en">base.en — balanced (recommended, ~145 MB)</option>
<option value="small.en">small.en — better accuracy (~465 MB)</option>
<option value="medium.en">medium.en — best accuracy (~1.5 GB)</option>
</select>
<p class="field-hint">Larger models are more accurate but use more RAM and take longer to load.</p>
</div>
<div class="actions" style="margin-top:0.85rem;">
<button class="btn-save" @click="saveAdminVoice" :disabled="savingAdminVoice">
{{ adminVoiceSaved ? 'Saved ' : savingAdminVoice ? 'Saving' : 'Save' }}
</button>
<span v-if="adminVoiceSaved" class="field-hint">Restart the server to apply model changes.</span>
</div>
</section>
</div>
<!-- ── Users ── -->