/** * Config store - manages application configuration loaded from * and saved to the Python backend via the backend store's API helpers. * * The backend accepts PUT /api/config with `{ settings: { "dot.key": value } }`. */ import { backendStore } from "$lib/stores/backend"; export interface AppConfig { user: { name: string; id: string; }; audio: { input_device: string; sample_rate: number; }; transcription: { model: string; device: string; language: string; compute_type: string; enable_realtime_transcription: boolean; realtime_model: string; realtime_processing_pause: number; silero_sensitivity: number; silero_use_onnx: boolean; webrtc_sensitivity: number; post_speech_silence_duration: number; min_length_of_recording: number; min_gap_between_recordings: number; pre_recording_buffer_duration: number; beam_size: number; initial_prompt: string; no_log_file: boolean; continuous_mode: boolean; }; server_sync: { enabled: boolean; url: string; room: string; passphrase: string; }; display: { show_timestamps: boolean; max_lines: number; font_source: string; font_family: string; websafe_font: string; google_font: string; custom_font_file: string; font_size: number; theme: string; fade_after_seconds: number; user_color: string; text_color: string; background_color: string; }; web_server: { port: number; host: string; }; remote: { mode: string; server_url: string; auth_token: string; byok_api_key: string; deepgram_model: string; language: string; fallback_to_local: boolean; }; updates: { auto_check: boolean; gitea_url: string; owner: string; repo: string; skipped_versions: string[]; last_check: string; check_interval_hours: number; }; } function getDefaultConfig(): AppConfig { return { user: { name: "User", id: "" }, audio: { input_device: "default", sample_rate: 16000 }, transcription: { model: "base.en", device: "auto", language: "en", compute_type: "default", enable_realtime_transcription: false, realtime_model: "tiny.en", realtime_processing_pause: 0.1, silero_sensitivity: 0.4, silero_use_onnx: true, webrtc_sensitivity: 3, post_speech_silence_duration: 0.3, min_length_of_recording: 0.5, min_gap_between_recordings: 0, pre_recording_buffer_duration: 0.2, beam_size: 5, initial_prompt: "", no_log_file: true, continuous_mode: false, }, server_sync: { enabled: false, url: "http://localhost:3000/api/send", room: "default", passphrase: "", }, display: { show_timestamps: true, max_lines: 100, font_source: "System Font", font_family: "Courier", websafe_font: "Arial", google_font: "Roboto", custom_font_file: "", font_size: 12, theme: "dark", fade_after_seconds: 10, user_color: "#4CAF50", text_color: "#FFFFFF", background_color: "#000000B3", }, web_server: { port: 8080, host: "127.0.0.1" }, remote: { mode: "local", server_url: "", auth_token: "", byok_api_key: "", deepgram_model: "nova-2", language: "en-US", fallback_to_local: true, }, updates: { auto_check: true, gitea_url: "https://repo.anhonesthost.net", owner: "streamer-tools", repo: "local-transcription", skipped_versions: [], last_check: "", check_interval_hours: 24, }, }; } let config = $state(getDefaultConfig()); let loading = $state(false); let error = $state(""); /** * Fetch the full configuration tree from the backend. * GET /api/config */ async function fetchConfig(): Promise { loading = true; error = ""; try { const data = await backendStore.apiGet>("/api/config"); // Deep merge with defaults to ensure all keys exist config = deepMerge(getDefaultConfig(), data) as AppConfig; } catch (err) { error = err instanceof Error ? err.message : String(err); console.error("[config] fetchConfig failed:", error); } finally { loading = false; } } function deepMerge(target: Record, source: Record): Record { const result = { ...target }; for (const key of Object.keys(source)) { if ( source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key]) ) { result[key] = deepMerge( target[key] as Record, source[key] as Record ); } else { result[key] = source[key]; } } return result; } /** * Send a batch of setting updates to the backend. * PUT /api/config with body `{ settings: { "dot.key": value, ... } }` * * Keys use dot-notation, e.g. `{ "transcription.model": "small.en" }`. * * Returns the response payload on success, or throws on failure. */ async function updateConfig( settings: Record, ): Promise<{ status: string; message: string; engine_reloaded: boolean }> { loading = true; error = ""; try { const result = await backendStore.apiPut<{ status: string; message: string; engine_reloaded: boolean; }>("/api/config", { settings }); // Refresh the local config tree so the UI stays in sync await fetchConfig(); return result; } catch (err) { error = err instanceof Error ? err.message : String(err); console.error("[config] updateConfig failed:", error); throw err; } finally { loading = false; } } export const configStore = { get config() { return config; }, get loading() { return loading; }, get error() { return error; }, fetchConfig, updateConfig, };