This repository has been archived on 2026-06-15. You can view files and clone it, but cannot push or open issues or pull requests.
Files
MediaLore/src/lib/app-settings.ts
2026-04-13 19:40:25 -04:00

285 lines
12 KiB
TypeScript

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<LibraryAiOverrides>): 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<string, string | undefined> = {
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<string, number | null | undefined> = {
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))))
}