285 lines
12 KiB
TypeScript
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))))
|
|
}
|