All checks were successful
Release / Bump version and tag (push) Successful in 7s
Svelte 5 runes ($state, $derived, $effect) are only compiled in .svelte and .svelte.ts files. The stores used runes in plain .ts files, which meant $state was treated as an undefined function at runtime, crashing the JS before anything rendered. - Renamed backend.ts -> backend.svelte.ts - Renamed config.ts -> config.svelte.ts - Renamed transcriptions.ts -> transcriptions.svelte.ts - Added .svelte.ts to Vite resolve extensions - Added missing obsUrl/syncUrl getters to backend store Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
244 lines
5.9 KiB
TypeScript
244 lines
5.9 KiB
TypeScript
/**
|
|
* 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<AppConfig>(getDefaultConfig());
|
|
let loading = $state(false);
|
|
let error = $state("");
|
|
|
|
/**
|
|
* Fetch the full configuration tree from the backend.
|
|
* GET /api/config
|
|
*/
|
|
async function fetchConfig(): Promise<void> {
|
|
loading = true;
|
|
error = "";
|
|
|
|
try {
|
|
const data = await backendStore.apiGet<Record<string, unknown>>("/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<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
|
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<string, unknown>,
|
|
source[key] as Record<string, unknown>
|
|
);
|
|
} 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<string, unknown>,
|
|
): 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,
|
|
};
|