ai starter implementation
This commit is contained in:
@@ -6,6 +6,7 @@ import { getAiConfig } from './app-settings'
|
||||
import { getTags, getCategories, addTagToItem } from './tags'
|
||||
import { getThumbnailPath } from './thumbnails'
|
||||
import { findFile } from './media-utils'
|
||||
import { getLibrary, resolveLibraryRoot } from './libraries'
|
||||
|
||||
const BATCH_LIMIT = 50
|
||||
const REQUEST_TIMEOUT_MS = 30_000
|
||||
@@ -250,3 +251,59 @@ export async function runAiTagging(library: Library, libraryRoot: string): Promi
|
||||
console.log(`[ai-tagger] Tagged ${tagged}/${untaggedItems.length} items in library "${library.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag a single item on-demand by itemKey.
|
||||
* Bypasses the ai_tagged_at check and batch limit — user explicitly requested this.
|
||||
* Throws descriptive errors so the API route can return appropriate status codes.
|
||||
*/
|
||||
export async function tagSingleItem(itemKey: string): Promise<string[]> {
|
||||
const config = getAiConfig()
|
||||
if (!config.endpoint || !config.model) {
|
||||
throw Object.assign(new Error('AI tagging endpoint and model are not configured'), { code: 'NOT_CONFIGURED' })
|
||||
}
|
||||
|
||||
const tags = getTags()
|
||||
const categories = getCategories()
|
||||
if (tags.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const validTagIds = new Set(tags.map((t) => t.id))
|
||||
const systemPrompt = buildTagPrompt(tags, categories)
|
||||
|
||||
const db = getDb()
|
||||
const item = db
|
||||
.prepare('SELECT item_key, item_type, file_path, metadata FROM media_items WHERE item_key = ?')
|
||||
.get(itemKey) as MediaItemRow | undefined
|
||||
|
||||
if (!item) {
|
||||
throw Object.assign(new Error(`Item not found: ${itemKey}`), { code: 'NOT_FOUND' })
|
||||
}
|
||||
|
||||
const libraryId = itemKey.split(':')[0]
|
||||
const library = getLibrary(libraryId)
|
||||
if (!library) {
|
||||
throw Object.assign(new Error(`Library not found: ${libraryId}`), { code: 'NOT_FOUND' })
|
||||
}
|
||||
const libraryRoot = resolveLibraryRoot(library)
|
||||
|
||||
const imagePath = resolveItemImage(libraryRoot, item)
|
||||
if (!imagePath) {
|
||||
throw Object.assign(new Error('No image available for this item'), { code: 'NO_IMAGE' })
|
||||
}
|
||||
|
||||
const thumbnailPath = await getThumbnailPath(imagePath, libraryId, 'image')
|
||||
const base64 = fs.readFileSync(thumbnailPath, 'base64')
|
||||
|
||||
const suggestedIds = await callVisionApi(config.endpoint, config.model, base64, systemPrompt)
|
||||
const validIds = suggestedIds.filter((id) => validTagIds.has(id))
|
||||
|
||||
for (const tagId of validIds) {
|
||||
addTagToItem(itemKey, tagId)
|
||||
}
|
||||
|
||||
db.prepare('UPDATE media_items SET ai_tagged_at = ? WHERE item_key = ?').run(Date.now(), itemKey)
|
||||
|
||||
return validIds
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user