diff --git a/src/app/api/ai-settings/route.ts b/src/app/api/ai-settings/route.ts
index 593f37a..7eba49d 100644
--- a/src/app/api/ai-settings/route.ts
+++ b/src/app/api/ai-settings/route.ts
@@ -1,27 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireAdmin } from '@/lib/auth'
-import { getAiConfig, updateAiConfig } from '@/lib/app-settings'
+import { getAiConfig, updateAiConfig, getPreferredLanguage, setPreferredLanguage } from '@/lib/app-settings'
export async function GET(request: NextRequest) {
const auth = await requireAdmin(request)
if (auth instanceof NextResponse) return auth
- const { endpoint, model, enabled } = getAiConfig()
- return NextResponse.json({ endpoint, model, enabled })
+ const { endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled } = getAiConfig()
+ const preferredLanguage = getPreferredLanguage()
+ return NextResponse.json({ endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled, preferredLanguage })
}
export async function PUT(request: NextRequest) {
const auth = await requireAdmin(request)
if (auth instanceof NextResponse) return auth
- let body: { endpoint?: string; model?: string; enabled?: boolean }
+ let body: { endpoint?: string; model?: string; modelTagging?: string; modelDescribe?: string; modelExtract?: string; modelTranslate?: string; enabled?: boolean; preferredLanguage?: string }
try {
body = await request.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
}
- const { endpoint, model, enabled } = body
+ const { endpoint, model, enabled, preferredLanguage, modelTagging, modelDescribe, modelExtract, modelTranslate } = body
if (typeof endpoint !== 'string') {
return NextResponse.json({ error: 'endpoint is required' }, { status: 400 })
@@ -33,6 +34,20 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: 'enabled must be a boolean' }, { status: 400 })
}
- updateAiConfig(endpoint, model, enabled)
- return NextResponse.json({ endpoint, model, enabled })
+ updateAiConfig(
+ endpoint,
+ model,
+ enabled,
+ typeof modelTagging === 'string' ? modelTagging : undefined,
+ typeof modelDescribe === 'string' ? modelDescribe : undefined,
+ typeof modelExtract === 'string' ? modelExtract : undefined,
+ typeof modelTranslate === 'string' ? modelTranslate : undefined,
+ )
+
+ if (typeof preferredLanguage === 'string' && preferredLanguage.trim()) {
+ setPreferredLanguage(preferredLanguage.trim())
+ }
+
+ const config = getAiConfig()
+ return NextResponse.json({ ...config, preferredLanguage: getPreferredLanguage() })
}
diff --git a/src/app/api/ai-tagging/describe-bulk/route.ts b/src/app/api/ai-tagging/describe-bulk/route.ts
new file mode 100644
index 0000000..fb9d05e
--- /dev/null
+++ b/src/app/api/ai-tagging/describe-bulk/route.ts
@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { describeDirectoryItems } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { libraryId?: string; path?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { libraryId, path: dirPath } = body
+ if (!libraryId || typeof libraryId !== 'string') {
+ return NextResponse.json({ error: 'libraryId is required' }, { status: 400 })
+ }
+
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const processed = await describeDirectoryItems(libraryId, dirPath ?? '')
+ return NextResponse.json({ processed })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'INVALID_TYPE') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ console.error('[ai-tagging/describe-bulk] Error:', error)
+ return NextResponse.json({ error: 'Failed to generate descriptions' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/describe/route.ts b/src/app/api/ai-tagging/describe/route.ts
new file mode 100644
index 0000000..5121c65
--- /dev/null
+++ b/src/app/api/ai-tagging/describe/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { generateItemDescription } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { itemKey?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { itemKey } = body
+ if (!itemKey || typeof itemKey !== 'string') {
+ return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const description = await generateItemDescription(itemKey)
+ return NextResponse.json({ description })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'NO_IMAGE') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ console.error('[ai-tagging/describe] Error:', error)
+ return NextResponse.json({ error: 'Failed to generate description' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/extract-text-bulk/route.ts b/src/app/api/ai-tagging/extract-text-bulk/route.ts
new file mode 100644
index 0000000..196ca19
--- /dev/null
+++ b/src/app/api/ai-tagging/extract-text-bulk/route.ts
@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { extractDirectoryText } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { libraryId?: string; path?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { libraryId, path: dirPath } = body
+ if (!libraryId || typeof libraryId !== 'string') {
+ return NextResponse.json({ error: 'libraryId is required' }, { status: 400 })
+ }
+
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const processed = await extractDirectoryText(libraryId, dirPath ?? '')
+ return NextResponse.json({ processed })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'INVALID_TYPE') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ console.error('[ai-tagging/extract-text-bulk] Error:', error)
+ return NextResponse.json({ error: 'Failed to extract text' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/extract-text/route.ts b/src/app/api/ai-tagging/extract-text/route.ts
new file mode 100644
index 0000000..58de630
--- /dev/null
+++ b/src/app/api/ai-tagging/extract-text/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { extractItemText } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { itemKey?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { itemKey } = body
+ if (!itemKey || typeof itemKey !== 'string') {
+ return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const result = await extractItemText(itemKey)
+ return NextResponse.json(result)
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ if (error.code === 'NO_IMAGE' || error.code === 'INVALID_TYPE') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ console.error('[ai-tagging/extract-text] Error:', error)
+ return NextResponse.json({ error: 'Failed to extract text' }, { status: 502 })
+ }
+}
diff --git a/src/app/api/ai-tagging/fields/route.ts b/src/app/api/ai-tagging/fields/route.ts
new file mode 100644
index 0000000..ee647aa
--- /dev/null
+++ b/src/app/api/ai-tagging/fields/route.ts
@@ -0,0 +1,19 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { getAiFields } from '@/lib/ai-tagger'
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = request.nextUrl
+ const itemKey = searchParams.get('itemKey')
+
+ if (!itemKey) {
+ return NextResponse.json({ error: 'Missing itemKey' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ const fields = getAiFields(itemKey)
+ return NextResponse.json(fields)
+}
diff --git a/src/app/api/ai-tagging/translate/route.ts b/src/app/api/ai-tagging/translate/route.ts
new file mode 100644
index 0000000..740d9fb
--- /dev/null
+++ b/src/app/api/ai-tagging/translate/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { requireLibraryAccess } from '@/lib/auth'
+import { translateItemText } from '@/lib/ai-tagger'
+
+export async function POST(request: NextRequest) {
+ let body: { itemKey?: string }
+ try {
+ body = await request.json()
+ } catch {
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+ }
+
+ const { itemKey } = body
+ if (!itemKey || typeof itemKey !== 'string') {
+ return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
+ }
+
+ const libraryId = itemKey.split(':')[0]
+ const auth = await requireLibraryAccess(request, libraryId)
+ if (auth instanceof NextResponse) return auth
+
+ try {
+ const translatedText = await translateItemText(itemKey)
+ return NextResponse.json({ translatedText })
+ } catch (err) {
+ const error = err as Error & { code?: string }
+ if (error.code === 'NOT_CONFIGURED') {
+ return NextResponse.json({ error: error.message }, { status: 400 })
+ }
+ if (error.code === 'NOT_FOUND') {
+ return NextResponse.json({ error: error.message }, { status: 404 })
+ }
+ console.error('[ai-tagging/translate] Error:', error)
+ return NextResponse.json({ error: 'Failed to translate text' }, { status: 502 })
+ }
+}
diff --git a/src/app/manage/ai-tagging/page.tsx b/src/app/manage/ai-tagging/page.tsx
index b8bf1b0..6839360 100644
--- a/src/app/manage/ai-tagging/page.tsx
+++ b/src/app/manage/ai-tagging/page.tsx
@@ -5,11 +5,16 @@ import { useEffect, useState, useCallback } from 'react'
interface AiSettings {
endpoint: string
model: string
+ modelTagging: string
+ modelDescribe: string
+ modelExtract: string
+ modelTranslate: string
enabled: boolean
+ preferredLanguage: string
}
export default function AiTaggingPage() {
- const [settings, setSettings] = useState
- Model name to use for vision requests. + Default model used for all AI tasks unless overridden below.