add per-library AI model and prompt customization
- 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>
This commit is contained in:
@@ -39,7 +39,14 @@ export function setScanLastRan(ts: number): void {
|
||||
|
||||
// ─── AI Settings ─────────────────────────────────────────────────────────────
|
||||
|
||||
interface AiConfig {
|
||||
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
|
||||
@@ -47,6 +54,10 @@ interface AiConfig {
|
||||
modelExtract: string
|
||||
modelTranslate: string
|
||||
enabled: boolean
|
||||
promptDescribe: string
|
||||
promptTagger: string
|
||||
promptExtract: string
|
||||
promptTranslate: string
|
||||
}
|
||||
|
||||
export function getAiConfig(): AiConfig {
|
||||
@@ -57,7 +68,18 @@ export function getAiConfig(): AiConfig {
|
||||
const modelExtract = getSetting('ai_model_extract') ?? ''
|
||||
const modelTranslate = getSetting('ai_model_translate') ?? ''
|
||||
const enabled = getSetting('ai_enabled') === 'true'
|
||||
return { endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled }
|
||||
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(
|
||||
@@ -68,6 +90,10 @@ export function updateAiConfig(
|
||||
modelDescribe?: string,
|
||||
modelExtract?: string,
|
||||
modelTranslate?: string,
|
||||
promptDescribe?: string,
|
||||
promptTagger?: string,
|
||||
promptExtract?: string,
|
||||
promptTranslate?: string,
|
||||
): void {
|
||||
setSetting('ai_endpoint', endpoint)
|
||||
setSetting('ai_model', model)
|
||||
@@ -76,6 +102,10 @@ export function updateAiConfig(
|
||||
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 {
|
||||
@@ -85,3 +115,90 @@ export function getPreferredLanguage(): string {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user