- Add library_ai_settings table with migration for per-library overrides - Extend AiConfig with editable prompt parts for description, tagging, extraction, and translation steps; defaults match previous hardcoded values - Add getEffectiveAiConfig(libraryId) that merges global settings with library-level overrides (empty override falls through to global) - Update all ai-tagger functions to use getEffectiveAiConfig and build prompts from configurable parts - Add GET/PUT /api/ai-settings/library/[id] for per-library overrides - Update /api/ai-settings GET/PUT to include prompt fields - Add Prompts section and Library Overrides section to admin UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
205 lines
7.6 KiB
TypeScript
205 lines
7.6 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 interface AiConfig {
|
|
endpoint: string
|
|
model: string
|
|
modelTagging: string
|
|
modelDescribe: string
|
|
modelExtract: string
|
|
modelTranslate: string
|
|
enabled: boolean
|
|
promptDescribe: string
|
|
promptTagger: string
|
|
promptExtract: string
|
|
promptTranslate: string
|
|
}
|
|
|
|
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
|
|
return {
|
|
endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled,
|
|
promptDescribe, promptTagger, promptExtract, promptTranslate,
|
|
}
|
|
}
|
|
|
|
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,
|
|
): 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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 ?? '',
|
|
}
|
|
}
|
|
|
|
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 fields: 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(fields)) {
|
|
if (val !== undefined) {
|
|
db.prepare(`UPDATE library_ai_settings SET ${col} = ? WHERE library_id = ?`).run(
|
|
val === '' ? null : 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,
|
|
}
|
|
}
|