feature: speech to text settings

- initial commit: voice settings - Settings section for STT

Alessandro committed Nov 28, 2024 at 16:07 UTC 89b848312b6f553fe22a6c0039bc7ab93b716384
4 files changed +122 -13
python/helpers/settings.py
+89 -3
@@ -39,6 +39,13 @@ class Settings(TypedDict):
39 rfc_url: str
40 rfc_password: str
41
42 + stt_model_size: str
43 + stt_language: str
44 + stt_silence_threshold: float
45 + stt_silence_duration: int
46 + stt_waiting_timeout: int
47 + stt_min_speech_duration: int
48 +
49
50 class PartialSettings(Settings, total=False):
51 pass
@@ -413,12 +420,82 @@ def convert_out(settings: Settings) -> SettingsOutput:
420 "fields": dev_fields,
421 }
422
423 + # Speech to text section
424 + stt_fields: list[SettingsField] = []
425 +
426 + stt_fields.append({
427 + "id": "stt_model_size",
428 + "title": "Model Size",
429 + "description": "Select the speech recognition model size",
430 + "type": "select",
431 + "value": settings["stt_model_size"],
432 + "options": [
433 + {"value": "tiny", "label": "Tiny (39M, English)"},
434 + {"value": "base", "label": "Base (74M, English)"},
435 + {"value": "small", "label": "Small (244M, English)"},
436 + {"value": "medium", "label": "Medium (769M, English)"},
437 + {"value": "large", "label": "Large (1.5B, Multilingual)"},
438 + {"value": "turbo", "label": "Turbo (Multilingual)"}
439 + ]
440 + })
441 +
442 + stt_fields.append({
443 + "id": "stt_language",
444 + "title": "Language Code",
445 + "description": "Language code (e.g. en, fr, it)",
446 + "type": "input",
447 + "value": settings["stt_language"]
448 + })
449 +
450 + stt_fields.append({
451 + "id": "stt_silence_threshold",
452 + "title": "Silence threshold",
453 + "description": "Silence detection threshold. Lower values are more sensitive.",
454 + "type": "range",
455 + "min": 0,
456 + "max": 1,
457 + "step": 0.01,
458 + "value": settings["stt_silence_threshold"]
459 + })
460 +
461 + stt_fields.append({
462 + "id": "stt_silence_duration",
463 + "title": "Silence duration (ms)",
464 + "description": "Duration of silence before the server considers speaking to have ended.",
465 + "type": "input",
466 + "value": settings["stt_silence_duration"]
467 + })
468 +
469 + stt_fields.append({
470 + "id": "stt_waiting_timeout",
471 + "title": "Waiting timeout (ms)",
472 + "description": "Duration before the server closes the microphone.",
473 + "type": "input",
474 + "value": settings["stt_waiting_timeout"]
475 + })
476 +
477 + stt_fields.append({
478 + "id": "stt_min_speech_duration",
479 + "title": "Prefix padding (ms)",
480 + "description": "Minimum duration of audio to be included in the stream before speech was recognized.",
481 + "type": "input",
482 + "value": settings["stt_min_speech_duration"]
483 + })
484 +
485 + stt_section: SettingsSection = {
486 + "title": "Speech to Text",
487 + "description": "Voice transcription preferences and server turn detection settings.",
488 + "fields": stt_fields
489 + }
490 +
491 + # Add the section to the result
492 result: SettingsOutput = {
493 "sections": [
494 agent_section,
495 chat_model_section,
496 util_model_section,
497 embed_model_section,
498 + stt_section,
499 api_keys_section,
500 auth_section,
501 dev_section,
@@ -443,9 +520,7 @@ def convert_in(settings: dict) -> Settings:
520 if "fields" in section:
521 for field in section["fields"]:
522 if field["id"].endswith("_kwargs"):
446 - current[field["id"]] = _env_to_dict(
447 - field["value"]
448 - ) # parse KWARGS from env format
523 + current[field["id"]] = _env_to_dict(field["value"])
524 elif field["id"].startswith("api_key_"):
525 current["api_keys"][field["id"]] = field["value"]
526 else:
@@ -518,6 +593,11 @@ def get_embedding_model(settings: Settings | None = None) -> Embeddings:
593 **settings["embed_model_kwargs"],
594 )
595
596 +def get_speech_settings(settings: Settings | None = None) -> dict:
597 + if not settings:
598 + settings = get_settings()
599 + return settings["speech_to_text"]
600 +
601
602 def _read_settings_file() -> Settings | None:
603 if os.path.exists(SETTINGS_FILE):
@@ -573,6 +653,12 @@ def _get_default_settings() -> Settings:
653 agent_knowledge_subdir="custom",
654 rfc_url="http://localhost:55080",
655 rfc_password="",
656 + stt_model_size="tiny",
657 + stt_language="en",
658 + stt_silence_threshold=0.15,
659 + stt_silence_duration=1000,
660 + stt_waiting_timeout=2000,
661 + stt_min_speech_duration=500
662 )
663
664
webui/js/settings.js
+1
@@ -91,6 +91,7 @@ function getIconName(title) {
91 'Chat Model': 'chat-model',
92 'Utility model': 'utility-model',
93 'Embedding Model': 'embed-model',
94 + 'Speech to Text': 'voice',
95 'API Keys': 'api-keys',
96 'Authentication': 'auth',
97 'Development': 'dev'
webui/js/speech.js
+31 -10
@@ -35,15 +35,36 @@ class MicrophoneInput {
35 this.hasStartedRecording = false;
36 this.analysisFrame = null;
37
38 + // Initialize with defaults
39 this.options = {
40 modelSize: 'tiny',
41 language: 'en',
42 silenceThreshold: 0.15,
43 silenceDuration: 1000,
44 waitingTimeout: 2000,
44 - minSpeechDuration: 500,
45 - ...options
45 + minSpeechDuration: 500
46 };
47 +
48 + // Fetch settings from server
49 + this.loadSettings();
50 + }
51 +
52 + async loadSettings() {
53 + try {
54 + const response = await fetch('/settings_get');
55 + const data = await response.json();
56 + const sttSettings = data.settings.sections.find(s => s.title === 'Speech to Text');
57 +
58 + if (sttSettings) {
59 + // Update options from server settings
60 + sttSettings.fields.forEach(field => {
61 + const key = field.id.split('.')[1]; // speech_to_text.model_size -> model_size
62 + this.options[key] = field.value;
63 + });
64 + }
65 + } catch (error) {
66 + console.error('Failed to load speech settings:', error);
67 + }
68 }
69
70 get status() {
@@ -314,13 +335,6 @@ async function initializeMicrophoneInput() {
335 await sendMessage();
336 }
337 }
317 - },
318 - {
319 - modelSize: 'tiny',
320 - language: 'en',
321 - silenceThreshold: 0.07,
322 - silenceDuration: 1000,
323 - waitingTimeout: 1500
338 }
339 );
340 microphoneInput.status = Status.ACTIVATING;
@@ -439,4 +453,11 @@ class Speech {
453 }
454
455 export const speech = new Speech();
442 -window.speech = speech
\ No newline at end of file
456 +window.speech = speech
457 +
458 +// Add event listener for settings changes
459 +document.addEventListener('settings-updated', async () => {
460 + if (microphoneInput) {
461 + await microphoneInput.loadSettings();
462 + }
463 +});
\ No newline at end of file
webui/public/voice.svg new
+1
@@ -0,0 +1 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 22.6 21.2"><style>.st0{fill:none;stroke:#000;stroke-width:.5;stroke-linecap:round;stroke-miterlimit:10}</style><g id="XMLID_00000031915645942353647240000017236973318540577665_"><path d="m11.8 7.9 2.1 4.6c.2.3-.1.7-.5.7h-1.7v3.4c0 .7-.6 1.3-1.3 1.3H4.2M7.7 20.8V18M4.2.3c4.2 0 7.6 3.4 7.6 7.6" class="st0"/><path d="M11.8 15.8h-1.4l-.4-.4" class="st0"/></g><path d="M14.2 15.7c.8.8.8 2 0 2.8M15.6 14.8c1.3 1.3 1.3 3.3 0 4.6M17 13.8c1.8 1.8 1.8 4.7 0 6.5" class="st0"/></svg>
\ No newline at end of file