import { getDb } from './db' interface ScanConfig { schedule: string enabled: boolean lastScanAt: number | null } function getSetting(key: string): string | null { const db = getDb() const row = db .prepare('SELECT value FROM app_settings WHERE key = ?') .get(key) as { value: string } | undefined return row?.value ?? null } function setSetting(key: string, value: string): void { const db = getDb() db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run(key, value) } export function getScanConfig(): ScanConfig { const schedule = getSetting('scan_schedule') ?? '0 * * * *' const enabled = getSetting('scan_enabled') !== 'false' const lastRanRaw = getSetting('scan_last_ran') const lastScanAt = lastRanRaw && lastRanRaw.length > 0 ? parseInt(lastRanRaw, 10) : null return { schedule, enabled, lastScanAt } } export function updateScanConfig(schedule: string, enabled: boolean): void { setSetting('scan_schedule', schedule) setSetting('scan_enabled', enabled ? 'true' : 'false') } export function setScanLastRan(ts: number): void { setSetting('scan_last_ran', String(ts)) } // ─── AI Settings ───────────────────────────────────────────────────────────── const DEFAULT_PROMPT_DESCRIBE = 'Focus on the visual content, subjects, setting, and mood. Do not speculate about context outside the image. Do not preface the description with any phrases like "This image shows" or "This image features". Return only the description text with no additional commentary.' const DEFAULT_PROMPT_TAGGER = '' const DEFAULT_PROMPT_EXTRACT = 'Be mindful of different colors of text that may indicate different speakers or emphasis.' const DEFAULT_PROMPT_TRANSLATE = 'Return ONLY the translated text with no additional commentary.' export type OcrMode = 'hybrid' | 'tesseract' | 'llm' export interface AiConfig { endpoint: string model: string modelTagging: string modelDescribe: string modelExtract: string modelTranslate: string enabled: boolean promptDescribe: string promptTagger: string promptExtract: string promptTranslate: string maxTokensTag: number maxTokensDescribe: number maxTokensExtract: number maxTokensTranslate: number ocrMode: OcrMode ocrLanguages: string ocrConfidenceThreshold: number } export function getAiConfig(): AiConfig { const endpoint = getSetting('ai_endpoint') ?? '' const model = getSetting('ai_model') ?? '' const modelTagging = getSetting('ai_model_tagging') ?? '' const modelDescribe = getSetting('ai_model_describe') ?? '' const modelExtract = getSetting('ai_model_extract') ?? '' const modelTranslate = getSetting('ai_model_translate') ?? '' const enabled = getSetting('ai_enabled') === 'true' const promptDescribeRaw = getSetting('ai_prompt_describe') const promptDescribe = promptDescribeRaw !== null ? promptDescribeRaw : DEFAULT_PROMPT_DESCRIBE const promptTaggerRaw = getSetting('ai_prompt_tagger') const promptTagger = promptTaggerRaw !== null ? promptTaggerRaw : DEFAULT_PROMPT_TAGGER const promptExtractRaw = getSetting('ai_prompt_extract') const promptExtract = promptExtractRaw !== null ? promptExtractRaw : DEFAULT_PROMPT_EXTRACT const promptTranslateRaw = getSetting('ai_prompt_translate') const promptTranslate = promptTranslateRaw !== null ? promptTranslateRaw : DEFAULT_PROMPT_TRANSLATE const maxTokensTag = parseInt(getSetting('ai_max_tokens_tag') ?? '8192', 10) || 8192 const maxTokensDescribe = parseInt(getSetting('ai_max_tokens_describe') ?? '8192', 10) || 8192 const maxTokensExtract = parseInt(getSetting('ai_max_tokens_extract') ?? '8192', 10) || 8192 const maxTokensTranslate = parseInt(getSetting('ai_max_tokens_translate') ?? '8192', 10) || 8192 const rawOcrMode = getSetting('ai_ocr_mode') ?? 'hybrid' const ocrMode: OcrMode = rawOcrMode === 'tesseract' || rawOcrMode === 'llm' ? rawOcrMode : 'hybrid' const ocrLanguages = getSetting('ai_ocr_languages') ?? 'eng' const ocrConfidenceThreshold = parseInt(getSetting('ai_ocr_confidence_threshold') ?? '70', 10) || 70 return { endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled, promptDescribe, promptTagger, promptExtract, promptTranslate, maxTokensTag, maxTokensDescribe, maxTokensExtract, maxTokensTranslate, ocrMode, ocrLanguages, ocrConfidenceThreshold, } } export function updateAiConfig( endpoint: string, model: string, enabled: boolean, modelTagging?: string, modelDescribe?: string, modelExtract?: string, modelTranslate?: string, promptDescribe?: string, promptTagger?: string, promptExtract?: string, promptTranslate?: string, maxTokensTag?: number, maxTokensDescribe?: number, maxTokensExtract?: number, maxTokensTranslate?: number, ocrMode?: OcrMode, ocrLanguages?: string, ocrConfidenceThreshold?: number, ): void { setSetting('ai_endpoint', endpoint) setSetting('ai_model', model) setSetting('ai_enabled', enabled ? 'true' : 'false') if (modelTagging !== undefined) setSetting('ai_model_tagging', modelTagging) if (modelDescribe !== undefined) setSetting('ai_model_describe', modelDescribe) if (modelExtract !== undefined) setSetting('ai_model_extract', modelExtract) if (modelTranslate !== undefined) setSetting('ai_model_translate', modelTranslate) if (promptDescribe !== undefined) setSetting('ai_prompt_describe', promptDescribe) if (promptTagger !== undefined) setSetting('ai_prompt_tagger', promptTagger) if (promptExtract !== undefined) setSetting('ai_prompt_extract', promptExtract) if (promptTranslate !== undefined) setSetting('ai_prompt_translate', promptTranslate) if (maxTokensTag !== undefined) setSetting('ai_max_tokens_tag', String(Math.max(1, Math.floor(maxTokensTag)))) if (maxTokensDescribe !== undefined) setSetting('ai_max_tokens_describe', String(Math.max(1, Math.floor(maxTokensDescribe)))) if (maxTokensExtract !== undefined) setSetting('ai_max_tokens_extract', String(Math.max(1, Math.floor(maxTokensExtract)))) if (maxTokensTranslate !== undefined) setSetting('ai_max_tokens_translate', String(Math.max(1, Math.floor(maxTokensTranslate)))) if (ocrMode !== undefined) setSetting('ai_ocr_mode', ocrMode) if (ocrLanguages !== undefined) setSetting('ai_ocr_languages', ocrLanguages.trim() || 'eng') if (ocrConfidenceThreshold !== undefined) setSetting('ai_ocr_confidence_threshold', String(Math.max(0, Math.min(100, Math.floor(ocrConfidenceThreshold))))) } export function getPreferredLanguage(): string { return getSetting('preferred_language') ?? 'English' } export function setPreferredLanguage(language: string): void { setSetting('preferred_language', language) } // ─── Per-library AI overrides ───────────────────────────────────────────────── export interface LibraryAiOverrides { modelTagging: string modelDescribe: string modelExtract: string modelTranslate: string promptDescribe: string promptTagger: string promptExtract: string promptTranslate: string maxTokensTag: number | null maxTokensDescribe: number | null maxTokensExtract: number | null maxTokensTranslate: number | null } interface LibraryAiSettingsRow { model_tagging: string | null model_describe: string | null model_extract: string | null model_translate: string | null prompt_describe: string | null prompt_tagger: string | null prompt_extract: string | null prompt_translate: string | null max_tokens_tag: number | null max_tokens_describe: number | null max_tokens_extract: number | null max_tokens_translate: number | null } export function getLibraryAiOverrides(libraryId: string): LibraryAiOverrides { const db = getDb() const row = db .prepare('SELECT * FROM library_ai_settings WHERE library_id = ?') .get(libraryId) as LibraryAiSettingsRow | undefined return { modelTagging: row?.model_tagging ?? '', modelDescribe: row?.model_describe ?? '', modelExtract: row?.model_extract ?? '', modelTranslate: row?.model_translate ?? '', promptDescribe: row?.prompt_describe ?? '', promptTagger: row?.prompt_tagger ?? '', promptExtract: row?.prompt_extract ?? '', promptTranslate: row?.prompt_translate ?? '', maxTokensTag: row?.max_tokens_tag ?? null, maxTokensDescribe: row?.max_tokens_describe ?? null, maxTokensExtract: row?.max_tokens_extract ?? null, maxTokensTranslate: row?.max_tokens_translate ?? null, } } export function setLibraryAiOverrides(libraryId: string, overrides: Partial): void { const db = getDb() // Ensure a row exists db.prepare( 'INSERT OR IGNORE INTO library_ai_settings (library_id) VALUES (?)' ).run(libraryId) const stringFields: Record = { model_tagging: overrides.modelTagging, model_describe: overrides.modelDescribe, model_extract: overrides.modelExtract, model_translate: overrides.modelTranslate, prompt_describe: overrides.promptDescribe, prompt_tagger: overrides.promptTagger, prompt_extract: overrides.promptExtract, prompt_translate: overrides.promptTranslate, } for (const [col, val] of Object.entries(stringFields)) { if (val !== undefined) { db.prepare(`UPDATE library_ai_settings SET ${col} = ? WHERE library_id = ?`).run( val === '' ? null : val, libraryId, ) } } const numberFields: Record = { max_tokens_tag: overrides.maxTokensTag, max_tokens_describe: overrides.maxTokensDescribe, max_tokens_extract: overrides.maxTokensExtract, max_tokens_translate: overrides.maxTokensTranslate, } for (const [col, val] of Object.entries(numberFields)) { if (val !== undefined) { db.prepare(`UPDATE library_ai_settings SET ${col} = ? WHERE library_id = ?`).run( val === null ? null : Math.max(1, Math.floor(val)), libraryId, ) } } } export function getEffectiveAiConfig(libraryId: string): AiConfig { const global = getAiConfig() const overrides = getLibraryAiOverrides(libraryId) return { endpoint: global.endpoint, model: global.model, enabled: global.enabled, modelTagging: overrides.modelTagging || global.modelTagging, modelDescribe: overrides.modelDescribe || global.modelDescribe, modelExtract: overrides.modelExtract || global.modelExtract, modelTranslate: overrides.modelTranslate || global.modelTranslate, promptDescribe: overrides.promptDescribe || global.promptDescribe, promptTagger: overrides.promptTagger || global.promptTagger, promptExtract: overrides.promptExtract || global.promptExtract, promptTranslate: overrides.promptTranslate || global.promptTranslate, maxTokensTag: overrides.maxTokensTag ?? global.maxTokensTag, maxTokensDescribe: overrides.maxTokensDescribe ?? global.maxTokensDescribe, maxTokensExtract: overrides.maxTokensExtract ?? global.maxTokensExtract, maxTokensTranslate: overrides.maxTokensTranslate ?? global.maxTokensTranslate, ocrMode: global.ocrMode, ocrLanguages: global.ocrLanguages, ocrConfidenceThreshold: global.ocrConfidenceThreshold, } } // ─── AI Max Retries ────────────────────────────────────────────────────────── export function getAiMaxRetries(): number { const raw = getSetting('ai_max_retries') const parsed = parseInt(raw ?? '3', 10) return Number.isFinite(parsed) && parsed >= 0 ? parsed : 3 } export function setAiMaxRetries(n: number): void { setSetting('ai_max_retries', String(Math.max(0, Math.floor(n)))) }