add ai job queue
This commit is contained in:
63
src/app/api/ai-jobs/route.ts
Normal file
63
src/app/api/ai-jobs/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
import { getJobQueue, getJobHistory, retryJob, cancelJob, cancelAllQueued, clearJobHistory } from '@/lib/ai-jobs'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
const queue = getJobQueue()
|
||||
const history = getJobHistory(50)
|
||||
return NextResponse.json({ queue, history })
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
let body: { action?: string; jobId?: string }
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { action, jobId } = body
|
||||
|
||||
switch (action) {
|
||||
case 'retry': {
|
||||
if (!jobId || typeof jobId !== 'string') {
|
||||
return NextResponse.json({ error: 'jobId is required' }, { status: 400 })
|
||||
}
|
||||
const ok = retryJob(jobId)
|
||||
if (!ok) {
|
||||
return NextResponse.json({ error: 'Job not found or not in failed state' }, { status: 404 })
|
||||
}
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
||||
case 'cancel': {
|
||||
if (!jobId || typeof jobId !== 'string') {
|
||||
return NextResponse.json({ error: 'jobId is required' }, { status: 400 })
|
||||
}
|
||||
const ok = cancelJob(jobId)
|
||||
if (!ok) {
|
||||
return NextResponse.json({ error: 'Job not found or not in queued state' }, { status: 404 })
|
||||
}
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
|
||||
case 'cancel-all': {
|
||||
const cancelled = cancelAllQueued()
|
||||
return NextResponse.json({ cancelled })
|
||||
}
|
||||
|
||||
case 'clear-history': {
|
||||
const cleared = clearJobHistory()
|
||||
return NextResponse.json({ cleared })
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdmin } from '@/lib/auth'
|
||||
import { getAiConfig, updateAiConfig, getPreferredLanguage, setPreferredLanguage } from '@/lib/app-settings'
|
||||
import { getAiConfig, updateAiConfig, getPreferredLanguage, setPreferredLanguage, getAiMaxRetries, setAiMaxRetries } from '@/lib/app-settings'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAdmin(request)
|
||||
@@ -8,7 +8,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const config = getAiConfig()
|
||||
const preferredLanguage = getPreferredLanguage()
|
||||
return NextResponse.json({ ...config, preferredLanguage })
|
||||
const maxRetries = getAiMaxRetries()
|
||||
return NextResponse.json({ ...config, preferredLanguage, maxRetries })
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
@@ -28,6 +29,7 @@ export async function PUT(request: NextRequest) {
|
||||
promptTagger?: string
|
||||
promptExtract?: string
|
||||
promptTranslate?: string
|
||||
maxRetries?: number
|
||||
}
|
||||
try {
|
||||
body = await request.json()
|
||||
@@ -39,6 +41,7 @@ export async function PUT(request: NextRequest) {
|
||||
endpoint, model, enabled, preferredLanguage,
|
||||
modelTagging, modelDescribe, modelExtract, modelTranslate,
|
||||
promptDescribe, promptTagger, promptExtract, promptTranslate,
|
||||
maxRetries,
|
||||
} = body
|
||||
|
||||
if (typeof endpoint !== 'string') {
|
||||
@@ -69,6 +72,10 @@ export async function PUT(request: NextRequest) {
|
||||
setPreferredLanguage(preferredLanguage.trim())
|
||||
}
|
||||
|
||||
if (typeof maxRetries === 'number' && Number.isFinite(maxRetries)) {
|
||||
setAiMaxRetries(maxRetries)
|
||||
}
|
||||
|
||||
const config = getAiConfig()
|
||||
return NextResponse.json({ ...config, preferredLanguage: getPreferredLanguage() })
|
||||
return NextResponse.json({ ...config, preferredLanguage: getPreferredLanguage(), maxRetries: getAiMaxRetries() })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { describeDirectoryItems } from '@/lib/ai-tagger'
|
||||
import { enqueueBulkJobs } from '@/lib/ai-jobs'
|
||||
|
||||
const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'])
|
||||
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.m4v', '.webm', '.flv', '.ts', '.mpg', '.mpeg'])
|
||||
const MEDIA_EXTENSIONS = new Set([...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS])
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { libraryId?: string; path?: string }
|
||||
@@ -18,21 +22,6 @@ export async function POST(request: NextRequest) {
|
||||
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 })
|
||||
}
|
||||
const jobIds = enqueueBulkJobs(libraryId, dirPath ?? '', 'describe', 'mixed_file', MEDIA_EXTENSIONS)
|
||||
return NextResponse.json({ jobIds, queued: jobIds.length }, { status: 202 })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { generateItemDescription } from '@/lib/ai-tagger'
|
||||
import { enqueueJob } from '@/lib/ai-jobs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { itemKey?: string }
|
||||
@@ -19,21 +19,6 @@ export async function POST(request: NextRequest) {
|
||||
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 })
|
||||
}
|
||||
const jobId = enqueueJob(itemKey, 'describe', libraryId)
|
||||
return NextResponse.json({ jobId }, { status: 202 })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { extractDirectoryText } from '@/lib/ai-tagger'
|
||||
import { enqueueBulkJobs } from '@/lib/ai-jobs'
|
||||
|
||||
const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'])
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { libraryId?: string; path?: string }
|
||||
@@ -18,21 +20,6 @@ export async function POST(request: NextRequest) {
|
||||
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 })
|
||||
}
|
||||
const jobIds = enqueueBulkJobs(libraryId, dirPath ?? '', 'extract', 'mixed_file', IMAGE_EXTENSIONS)
|
||||
return NextResponse.json({ jobIds, queued: jobIds.length }, { status: 202 })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { extractItemText } from '@/lib/ai-tagger'
|
||||
import { enqueueJob } from '@/lib/ai-jobs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { itemKey?: string }
|
||||
@@ -19,21 +19,6 @@ export async function POST(request: NextRequest) {
|
||||
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 })
|
||||
}
|
||||
const jobId = enqueueJob(itemKey, 'extract', libraryId)
|
||||
return NextResponse.json({ jobId }, { status: 202 })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { tagSingleItem } from '@/lib/ai-tagger'
|
||||
import { enqueueJob } from '@/lib/ai-jobs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { itemKey?: string }
|
||||
@@ -19,21 +19,6 @@ export async function POST(request: NextRequest) {
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const tagIds = await tagSingleItem(itemKey)
|
||||
return NextResponse.json({ tagIds })
|
||||
} 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] Error tagging item:', error)
|
||||
return NextResponse.json({ error: 'AI tagging failed' }, { status: 502 })
|
||||
}
|
||||
const jobId = enqueueJob(itemKey, 'tag', libraryId)
|
||||
return NextResponse.json({ jobId }, { status: 202 })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
import { translateItemText } from '@/lib/ai-tagger'
|
||||
import { enqueueJob } from '@/lib/ai-jobs'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { itemKey?: string; sourceLanguage?: string }
|
||||
@@ -19,18 +19,6 @@ export async function POST(request: NextRequest) {
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
try {
|
||||
const translatedText = await translateItemText(itemKey, sourceLanguage || undefined)
|
||||
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 })
|
||||
}
|
||||
const jobId = enqueueJob(itemKey, 'translate', libraryId, sourceLanguage || undefined)
|
||||
return NextResponse.json({ jobId }, { status: 202 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user