add configurable max_tokens per AI activity
Allows users to configure the max_tokens sent to the AI endpoint for each activity (tagging, description, extraction, translation) individually, with per-library overrides following the same pattern as model overrides. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,10 @@ export async function PUT(
|
|||||||
promptTagger: typeof body.promptTagger === 'string' ? body.promptTagger : undefined,
|
promptTagger: typeof body.promptTagger === 'string' ? body.promptTagger : undefined,
|
||||||
promptExtract: typeof body.promptExtract === 'string' ? body.promptExtract : undefined,
|
promptExtract: typeof body.promptExtract === 'string' ? body.promptExtract : undefined,
|
||||||
promptTranslate: typeof body.promptTranslate === 'string' ? body.promptTranslate : undefined,
|
promptTranslate: typeof body.promptTranslate === 'string' ? body.promptTranslate : undefined,
|
||||||
|
maxTokensTag: typeof body.maxTokensTag === 'number' ? body.maxTokensTag : (body.maxTokensTag === null ? null : undefined),
|
||||||
|
maxTokensDescribe: typeof body.maxTokensDescribe === 'number' ? body.maxTokensDescribe : (body.maxTokensDescribe === null ? null : undefined),
|
||||||
|
maxTokensExtract: typeof body.maxTokensExtract === 'number' ? body.maxTokensExtract : (body.maxTokensExtract === null ? null : undefined),
|
||||||
|
maxTokensTranslate: typeof body.maxTokensTranslate === 'number' ? body.maxTokensTranslate : (body.maxTokensTranslate === null ? null : undefined),
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json(getLibraryAiOverrides(id))
|
return NextResponse.json(getLibraryAiOverrides(id))
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
promptExtract?: string
|
promptExtract?: string
|
||||||
promptTranslate?: string
|
promptTranslate?: string
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
|
maxTokensTag?: number
|
||||||
|
maxTokensDescribe?: number
|
||||||
|
maxTokensExtract?: number
|
||||||
|
maxTokensTranslate?: number
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
@@ -42,6 +46,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
modelTagging, modelDescribe, modelExtract, modelTranslate,
|
modelTagging, modelDescribe, modelExtract, modelTranslate,
|
||||||
promptDescribe, promptTagger, promptExtract, promptTranslate,
|
promptDescribe, promptTagger, promptExtract, promptTranslate,
|
||||||
maxRetries,
|
maxRetries,
|
||||||
|
maxTokensTag, maxTokensDescribe, maxTokensExtract, maxTokensTranslate,
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
if (typeof endpoint !== 'string') {
|
if (typeof endpoint !== 'string') {
|
||||||
@@ -66,6 +71,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
typeof promptTagger === 'string' ? promptTagger : undefined,
|
typeof promptTagger === 'string' ? promptTagger : undefined,
|
||||||
typeof promptExtract === 'string' ? promptExtract : undefined,
|
typeof promptExtract === 'string' ? promptExtract : undefined,
|
||||||
typeof promptTranslate === 'string' ? promptTranslate : undefined,
|
typeof promptTranslate === 'string' ? promptTranslate : undefined,
|
||||||
|
typeof maxTokensTag === 'number' ? maxTokensTag : undefined,
|
||||||
|
typeof maxTokensDescribe === 'number' ? maxTokensDescribe : undefined,
|
||||||
|
typeof maxTokensExtract === 'number' ? maxTokensExtract : undefined,
|
||||||
|
typeof maxTokensTranslate === 'number' ? maxTokensTranslate : undefined,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (typeof preferredLanguage === 'string' && preferredLanguage.trim()) {
|
if (typeof preferredLanguage === 'string' && preferredLanguage.trim()) {
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ interface AiSettings {
|
|||||||
promptExtract: string
|
promptExtract: string
|
||||||
promptTranslate: string
|
promptTranslate: string
|
||||||
maxRetries: number
|
maxRetries: number
|
||||||
|
maxTokensTag: number
|
||||||
|
maxTokensDescribe: number
|
||||||
|
maxTokensExtract: number
|
||||||
|
maxTokensTranslate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AiJob {
|
interface AiJob {
|
||||||
@@ -47,6 +51,10 @@ interface LibraryOverride {
|
|||||||
promptTagger: string
|
promptTagger: string
|
||||||
promptExtract: string
|
promptExtract: string
|
||||||
promptTranslate: string
|
promptTranslate: string
|
||||||
|
maxTokensTag: number | null
|
||||||
|
maxTokensDescribe: number | null
|
||||||
|
maxTokensExtract: number | null
|
||||||
|
maxTokensTranslate: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatElapsed(startedAt: number): string {
|
function formatElapsed(startedAt: number): string {
|
||||||
@@ -67,6 +75,7 @@ export default function AiTaggingPage() {
|
|||||||
enabled: false, preferredLanguage: 'English',
|
enabled: false, preferredLanguage: 'English',
|
||||||
promptDescribe: '', promptTagger: '', promptExtract: '', promptTranslate: '',
|
promptDescribe: '', promptTagger: '', promptExtract: '', promptTranslate: '',
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
|
maxTokensTag: 8192, maxTokensDescribe: 8192, maxTokensExtract: 8192, maxTokensTranslate: 8192,
|
||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -296,7 +305,7 @@ export default function AiTaggingPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateLibraryOverride = (libraryId: string, field: keyof LibraryOverride, value: string) => {
|
const updateLibraryOverride = (libraryId: string, field: keyof LibraryOverride, value: string | number | null) => {
|
||||||
setLibraryOverrides((prev) => ({
|
setLibraryOverrides((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[libraryId]: { ...(prev[libraryId] ?? emptyOverride()), [field]: value },
|
[libraryId]: { ...(prev[libraryId] ?? emptyOverride()), [field]: value },
|
||||||
@@ -544,6 +553,25 @@ export default function AiTaggingPage() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Tagging Max Tokens">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={settings.maxTokensTag}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({ ...s, maxTokensTag: Math.max(1, parseInt(e.target.value) || 8192) }))
|
||||||
|
}
|
||||||
|
className="w-32 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')}
|
||||||
|
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field label="Description Model">
|
<Field label="Description Model">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -561,6 +589,25 @@ export default function AiTaggingPage() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Description Max Tokens">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={settings.maxTokensDescribe}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({ ...s, maxTokensDescribe: Math.max(1, parseInt(e.target.value) || 8192) }))
|
||||||
|
}
|
||||||
|
className="w-32 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')}
|
||||||
|
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field label="Text Extraction Model">
|
<Field label="Text Extraction Model">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -578,6 +625,25 @@ export default function AiTaggingPage() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Text Extraction Max Tokens">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={settings.maxTokensExtract}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({ ...s, maxTokensExtract: Math.max(1, parseInt(e.target.value) || 8192) }))
|
||||||
|
}
|
||||||
|
className="w-32 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')}
|
||||||
|
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field label="Translation Model">
|
<Field label="Translation Model">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -595,6 +661,25 @@ export default function AiTaggingPage() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Translation Max Tokens">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={settings.maxTokensTranslate}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((s) => ({ ...s, maxTokensTranslate: Math.max(1, parseInt(e.target.value) || 8192) }))
|
||||||
|
}
|
||||||
|
className="w-32 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')}
|
||||||
|
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field label="Automatic Tagging">
|
<Field label="Automatic Tagging">
|
||||||
<label className="flex items-center gap-3 cursor-pointer select-none">
|
<label className="flex items-center gap-3 cursor-pointer select-none">
|
||||||
<div
|
<div
|
||||||
@@ -890,7 +975,7 @@ export default function AiTaggingPage() {
|
|||||||
<Field key={field} label={label}>
|
<Field key={field} label={label}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={overrides[field]}
|
value={overrides[field] as string}
|
||||||
onChange={(e) => updateLibraryOverride(lib.id, field, e.target.value)}
|
onChange={(e) => updateLibraryOverride(lib.id, field, e.target.value)}
|
||||||
placeholder={`Leave blank to use global default${settings[field as keyof AiSettings] ? ` (${settings[field as keyof AiSettings]})` : ''}`}
|
placeholder={`Leave blank to use global default${settings[field as keyof AiSettings] ? ` (${settings[field as keyof AiSettings]})` : ''}`}
|
||||||
className="w-full rounded-lg px-3 py-2 text-sm font-mono outline-none focus:ring-2"
|
className="w-full rounded-lg px-3 py-2 text-sm font-mono outline-none focus:ring-2"
|
||||||
@@ -906,6 +991,39 @@ export default function AiTaggingPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-xs font-medium uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>Max Tokens</p>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
['maxTokensTag', 'Tagging', 'maxTokensTag'] as const,
|
||||||
|
['maxTokensDescribe', 'Description', 'maxTokensDescribe'] as const,
|
||||||
|
['maxTokensExtract', 'Text Extraction', 'maxTokensExtract'] as const,
|
||||||
|
['maxTokensTranslate', 'Translation', 'maxTokensTranslate'] as const,
|
||||||
|
]
|
||||||
|
).map(([field, label, globalField]) => (
|
||||||
|
<Field key={field} label={label}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={overrides[field] ?? ''}
|
||||||
|
placeholder={`Leave blank to use global default (${settings[globalField]})`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
updateLibraryOverride(lib.id, field, raw === '' ? null : Math.max(1, parseInt(raw) || 1))
|
||||||
|
}}
|
||||||
|
className="w-40 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--background)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)')}
|
||||||
|
onBlur={(e) => ((e.currentTarget as HTMLElement).style.borderColor = 'var(--border)')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<p className="text-xs font-medium uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>Prompts</p>
|
<p className="text-xs font-medium uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>Prompts</p>
|
||||||
{(
|
{(
|
||||||
@@ -919,7 +1037,7 @@ export default function AiTaggingPage() {
|
|||||||
<Field key={field} label={label}>
|
<Field key={field} label={label}>
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={overrides[field]}
|
value={overrides[field] as string}
|
||||||
onChange={(e) => updateLibraryOverride(lib.id, field, e.target.value)}
|
onChange={(e) => updateLibraryOverride(lib.id, field, e.target.value)}
|
||||||
placeholder={globalValue ? `Leave blank to use global default:\n${globalValue}` : 'Leave blank to use global default'}
|
placeholder={globalValue ? `Leave blank to use global default:\n${globalValue}` : 'Leave blank to use global default'}
|
||||||
className="w-full rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 resize-y"
|
className="w-full rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 resize-y"
|
||||||
@@ -1010,6 +1128,7 @@ function emptyOverride(): LibraryOverride {
|
|||||||
return {
|
return {
|
||||||
modelTagging: '', modelDescribe: '', modelExtract: '', modelTranslate: '',
|
modelTagging: '', modelDescribe: '', modelExtract: '', modelTranslate: '',
|
||||||
promptDescribe: '', promptTagger: '', promptExtract: '', promptTranslate: '',
|
promptDescribe: '', promptTagger: '', promptExtract: '', promptTranslate: '',
|
||||||
|
maxTokensTag: null, maxTokensDescribe: null, maxTokensExtract: null, maxTokensTranslate: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ async function callVisionApi(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
model: string,
|
model: string,
|
||||||
base64Images: string[],
|
base64Images: string[],
|
||||||
systemPrompt: string
|
systemPrompt: string,
|
||||||
|
maxTokens: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const url = endpoint.replace(/\/+$/, '') + '/chat/completions'
|
const url = endpoint.replace(/\/+$/, '') + '/chat/completions'
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ async function callVisionApi(
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_tokens: 8192,
|
max_tokens: maxTokens,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -338,7 +339,7 @@ export async function tagSingleItem(itemKey: string): Promise<string[]> {
|
|||||||
customInstruction: config.promptTagger || undefined,
|
customInstruction: config.promptTagger || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const suggestedIds = await callVisionApi(config.endpoint, taggingModel, base64Images, systemPromptWithContext)
|
const suggestedIds = await callVisionApi(config.endpoint, taggingModel, base64Images, systemPromptWithContext, config.maxTokensTag)
|
||||||
const validIds = suggestedIds.filter((id) => validTagIds.has(id))
|
const validIds = suggestedIds.filter((id) => validTagIds.has(id))
|
||||||
|
|
||||||
for (const tagId of validIds) {
|
for (const tagId of validIds) {
|
||||||
@@ -359,7 +360,8 @@ async function callVisionApiText(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
model: string,
|
model: string,
|
||||||
base64Images: string[],
|
base64Images: string[],
|
||||||
systemPrompt: string
|
systemPrompt: string,
|
||||||
|
maxTokens: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const url = endpoint.replace(/\/+$/, '') + '/chat/completions'
|
const url = endpoint.replace(/\/+$/, '') + '/chat/completions'
|
||||||
|
|
||||||
@@ -383,7 +385,7 @@ async function callVisionApiText(
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_tokens: 8192,
|
max_tokens: maxTokens,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -410,7 +412,8 @@ async function callChatApiText(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
model: string,
|
model: string,
|
||||||
systemPrompt: string,
|
systemPrompt: string,
|
||||||
userMessage: string
|
userMessage: string,
|
||||||
|
maxTokens: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const url = endpoint.replace(/\/+$/, '') + '/chat/completions'
|
const url = endpoint.replace(/\/+$/, '') + '/chat/completions'
|
||||||
|
|
||||||
@@ -428,7 +431,7 @@ async function callChatApiText(
|
|||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: userMessage },
|
{ role: 'user', content: userMessage },
|
||||||
],
|
],
|
||||||
max_tokens: 8192,
|
max_tokens: maxTokens,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -496,7 +499,7 @@ export async function generateItemDescription(itemKey: string): Promise<string>
|
|||||||
: ''
|
: ''
|
||||||
const systemPrompt = `You are a media cataloging assistant. Describe the given image briefly and objectively in 1-3 sentences.${config.promptDescribe ? ' ' + config.promptDescribe : ''}${tagContext}`
|
const systemPrompt = `You are a media cataloging assistant. Describe the given image briefly and objectively in 1-3 sentences.${config.promptDescribe ? ' ' + config.promptDescribe : ''}${tagContext}`
|
||||||
|
|
||||||
const description = await callVisionApiText(config.endpoint, describeModel, base64Images, systemPrompt)
|
const description = await callVisionApiText(config.endpoint, describeModel, base64Images, systemPrompt, config.maxTokensDescribe)
|
||||||
|
|
||||||
db.prepare('UPDATE media_items SET ai_description = ? WHERE item_key = ?').run(description, itemKey)
|
db.prepare('UPDATE media_items SET ai_description = ? WHERE item_key = ?').run(description, itemKey)
|
||||||
|
|
||||||
@@ -585,7 +588,7 @@ Rules:
|
|||||||
systemPrompt = `You are an OCR assistant. Extract ALL text visible in the image exactly as it appears. Preserve line breaks and formatting.${customInstruction} If there is no text in the image, respond with exactly: [NO TEXT]`
|
systemPrompt = `You are an OCR assistant. Extract ALL text visible in the image exactly as it appears. Preserve line breaks and formatting.${customInstruction} If there is no text in the image, respond with exactly: [NO TEXT]`
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawResponse = await callVisionApiText(config.endpoint, extractModel, base64Images, systemPrompt)
|
const rawResponse = await callVisionApiText(config.endpoint, extractModel, base64Images, systemPrompt, config.maxTokensExtract)
|
||||||
|
|
||||||
// Parse the response — structured JSON when a preferred language is set, plain text otherwise
|
// Parse the response — structured JSON when a preferred language is set, plain text otherwise
|
||||||
let extractedText: string
|
let extractedText: string
|
||||||
@@ -618,7 +621,7 @@ Rules:
|
|||||||
if (preferredLanguage && needsTranslation) {
|
if (preferredLanguage && needsTranslation) {
|
||||||
const translateModel = config.modelTranslate || config.model
|
const translateModel = config.modelTranslate || config.model
|
||||||
try {
|
try {
|
||||||
translatedText = await translateText(config.endpoint, translateModel, extractedText, preferredLanguage, config.promptTranslate)
|
translatedText = await translateText(config.endpoint, translateModel, extractedText, preferredLanguage, config.promptTranslate, config.maxTokensTranslate)
|
||||||
if (translatedText) {
|
if (translatedText) {
|
||||||
db.prepare('UPDATE media_items SET extracted_text_translated = ? WHERE item_key = ?').run(translatedText, itemKey)
|
db.prepare('UPDATE media_items SET extracted_text_translated = ? WHERE item_key = ?').run(translatedText, itemKey)
|
||||||
}
|
}
|
||||||
@@ -656,7 +659,7 @@ export async function translateItemText(itemKey: string, sourceLanguage?: string
|
|||||||
const preferredLanguage = getPreferredLanguage()
|
const preferredLanguage = getPreferredLanguage()
|
||||||
if (!preferredLanguage) return null
|
if (!preferredLanguage) return null
|
||||||
|
|
||||||
const translatedText = await translateText(config.endpoint, translateModel, row.extracted_text, preferredLanguage, config.promptTranslate, sourceLanguage)
|
const translatedText = await translateText(config.endpoint, translateModel, row.extracted_text, preferredLanguage, config.promptTranslate, config.maxTokensTranslate, sourceLanguage)
|
||||||
if (translatedText) {
|
if (translatedText) {
|
||||||
db.prepare('UPDATE media_items SET extracted_text_translated = ? WHERE item_key = ?').run(translatedText, itemKey)
|
db.prepare('UPDATE media_items SET extracted_text_translated = ? WHERE item_key = ?').run(translatedText, itemKey)
|
||||||
}
|
}
|
||||||
@@ -682,6 +685,7 @@ async function translateText(
|
|||||||
text: string,
|
text: string,
|
||||||
targetLanguage: string,
|
targetLanguage: string,
|
||||||
customInstruction = '',
|
customInstruction = '',
|
||||||
|
maxTokens = 8192,
|
||||||
sourceLanguage?: string,
|
sourceLanguage?: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
let systemPrompt: string
|
let systemPrompt: string
|
||||||
@@ -691,7 +695,7 @@ async function translateText(
|
|||||||
systemPrompt = `You are a translator. Determine if the following text is already in ${targetLanguage}. If it is, respond with exactly: [ALREADY_TARGET_LANGUAGE]. If it is not, translate it to ${targetLanguage}.${customInstruction ? ' ' + customInstruction : ''}`
|
systemPrompt = `You are a translator. Determine if the following text is already in ${targetLanguage}. If it is, respond with exactly: [ALREADY_TARGET_LANGUAGE]. If it is not, translate it to ${targetLanguage}.${customInstruction ? ' ' + customInstruction : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await callChatApiText(endpoint, model, systemPrompt, text)
|
const result = await callChatApiText(endpoint, model, systemPrompt, text, maxTokens)
|
||||||
|
|
||||||
if (!sourceLanguage && (result === '[ALREADY_TARGET_LANGUAGE]' || !result)) {
|
if (!sourceLanguage && (result === '[ALREADY_TARGET_LANGUAGE]' || !result)) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export interface AiConfig {
|
|||||||
promptTagger: string
|
promptTagger: string
|
||||||
promptExtract: string
|
promptExtract: string
|
||||||
promptTranslate: string
|
promptTranslate: string
|
||||||
|
maxTokensTag: number
|
||||||
|
maxTokensDescribe: number
|
||||||
|
maxTokensExtract: number
|
||||||
|
maxTokensTranslate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAiConfig(): AiConfig {
|
export function getAiConfig(): AiConfig {
|
||||||
@@ -76,9 +80,14 @@ export function getAiConfig(): AiConfig {
|
|||||||
const promptExtract = promptExtractRaw !== null ? promptExtractRaw : DEFAULT_PROMPT_EXTRACT
|
const promptExtract = promptExtractRaw !== null ? promptExtractRaw : DEFAULT_PROMPT_EXTRACT
|
||||||
const promptTranslateRaw = getSetting('ai_prompt_translate')
|
const promptTranslateRaw = getSetting('ai_prompt_translate')
|
||||||
const promptTranslate = promptTranslateRaw !== null ? promptTranslateRaw : DEFAULT_PROMPT_TRANSLATE
|
const promptTranslate = promptTranslateRaw !== null ? promptTranslateRaw : DEFAULT_PROMPT_TRANSLATE
|
||||||
|
const maxTokensTag = parseInt(getSetting('ai_max_tokens_tag') ?? '8192', 10) || 8192
|
||||||
|
const maxTokensDescribe = parseInt(getSetting('ai_max_tokens_describe') ?? '8192', 10) || 8192
|
||||||
|
const maxTokensExtract = parseInt(getSetting('ai_max_tokens_extract') ?? '8192', 10) || 8192
|
||||||
|
const maxTokensTranslate = parseInt(getSetting('ai_max_tokens_translate') ?? '8192', 10) || 8192
|
||||||
return {
|
return {
|
||||||
endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled,
|
endpoint, model, modelTagging, modelDescribe, modelExtract, modelTranslate, enabled,
|
||||||
promptDescribe, promptTagger, promptExtract, promptTranslate,
|
promptDescribe, promptTagger, promptExtract, promptTranslate,
|
||||||
|
maxTokensTag, maxTokensDescribe, maxTokensExtract, maxTokensTranslate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +103,10 @@ export function updateAiConfig(
|
|||||||
promptTagger?: string,
|
promptTagger?: string,
|
||||||
promptExtract?: string,
|
promptExtract?: string,
|
||||||
promptTranslate?: string,
|
promptTranslate?: string,
|
||||||
|
maxTokensTag?: number,
|
||||||
|
maxTokensDescribe?: number,
|
||||||
|
maxTokensExtract?: number,
|
||||||
|
maxTokensTranslate?: number,
|
||||||
): void {
|
): void {
|
||||||
setSetting('ai_endpoint', endpoint)
|
setSetting('ai_endpoint', endpoint)
|
||||||
setSetting('ai_model', model)
|
setSetting('ai_model', model)
|
||||||
@@ -106,6 +119,10 @@ export function updateAiConfig(
|
|||||||
if (promptTagger !== undefined) setSetting('ai_prompt_tagger', promptTagger)
|
if (promptTagger !== undefined) setSetting('ai_prompt_tagger', promptTagger)
|
||||||
if (promptExtract !== undefined) setSetting('ai_prompt_extract', promptExtract)
|
if (promptExtract !== undefined) setSetting('ai_prompt_extract', promptExtract)
|
||||||
if (promptTranslate !== undefined) setSetting('ai_prompt_translate', promptTranslate)
|
if (promptTranslate !== undefined) setSetting('ai_prompt_translate', promptTranslate)
|
||||||
|
if (maxTokensTag !== undefined) setSetting('ai_max_tokens_tag', String(Math.max(1, Math.floor(maxTokensTag))))
|
||||||
|
if (maxTokensDescribe !== undefined) setSetting('ai_max_tokens_describe', String(Math.max(1, Math.floor(maxTokensDescribe))))
|
||||||
|
if (maxTokensExtract !== undefined) setSetting('ai_max_tokens_extract', String(Math.max(1, Math.floor(maxTokensExtract))))
|
||||||
|
if (maxTokensTranslate !== undefined) setSetting('ai_max_tokens_translate', String(Math.max(1, Math.floor(maxTokensTranslate))))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreferredLanguage(): string {
|
export function getPreferredLanguage(): string {
|
||||||
@@ -127,6 +144,10 @@ export interface LibraryAiOverrides {
|
|||||||
promptTagger: string
|
promptTagger: string
|
||||||
promptExtract: string
|
promptExtract: string
|
||||||
promptTranslate: string
|
promptTranslate: string
|
||||||
|
maxTokensTag: number | null
|
||||||
|
maxTokensDescribe: number | null
|
||||||
|
maxTokensExtract: number | null
|
||||||
|
maxTokensTranslate: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LibraryAiSettingsRow {
|
interface LibraryAiSettingsRow {
|
||||||
@@ -138,6 +159,10 @@ interface LibraryAiSettingsRow {
|
|||||||
prompt_tagger: string | null
|
prompt_tagger: string | null
|
||||||
prompt_extract: string | null
|
prompt_extract: string | null
|
||||||
prompt_translate: string | null
|
prompt_translate: string | null
|
||||||
|
max_tokens_tag: number | null
|
||||||
|
max_tokens_describe: number | null
|
||||||
|
max_tokens_extract: number | null
|
||||||
|
max_tokens_translate: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLibraryAiOverrides(libraryId: string): LibraryAiOverrides {
|
export function getLibraryAiOverrides(libraryId: string): LibraryAiOverrides {
|
||||||
@@ -154,6 +179,10 @@ export function getLibraryAiOverrides(libraryId: string): LibraryAiOverrides {
|
|||||||
promptTagger: row?.prompt_tagger ?? '',
|
promptTagger: row?.prompt_tagger ?? '',
|
||||||
promptExtract: row?.prompt_extract ?? '',
|
promptExtract: row?.prompt_extract ?? '',
|
||||||
promptTranslate: row?.prompt_translate ?? '',
|
promptTranslate: row?.prompt_translate ?? '',
|
||||||
|
maxTokensTag: row?.max_tokens_tag ?? null,
|
||||||
|
maxTokensDescribe: row?.max_tokens_describe ?? null,
|
||||||
|
maxTokensExtract: row?.max_tokens_extract ?? null,
|
||||||
|
maxTokensTranslate: row?.max_tokens_translate ?? null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +193,7 @@ export function setLibraryAiOverrides(libraryId: string, overrides: Partial<Libr
|
|||||||
'INSERT OR IGNORE INTO library_ai_settings (library_id) VALUES (?)'
|
'INSERT OR IGNORE INTO library_ai_settings (library_id) VALUES (?)'
|
||||||
).run(libraryId)
|
).run(libraryId)
|
||||||
|
|
||||||
const fields: Record<string, string | undefined> = {
|
const stringFields: Record<string, string | undefined> = {
|
||||||
model_tagging: overrides.modelTagging,
|
model_tagging: overrides.modelTagging,
|
||||||
model_describe: overrides.modelDescribe,
|
model_describe: overrides.modelDescribe,
|
||||||
model_extract: overrides.modelExtract,
|
model_extract: overrides.modelExtract,
|
||||||
@@ -175,7 +204,7 @@ export function setLibraryAiOverrides(libraryId: string, overrides: Partial<Libr
|
|||||||
prompt_translate: overrides.promptTranslate,
|
prompt_translate: overrides.promptTranslate,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [col, val] of Object.entries(fields)) {
|
for (const [col, val] of Object.entries(stringFields)) {
|
||||||
if (val !== undefined) {
|
if (val !== undefined) {
|
||||||
db.prepare(`UPDATE library_ai_settings SET ${col} = ? WHERE library_id = ?`).run(
|
db.prepare(`UPDATE library_ai_settings SET ${col} = ? WHERE library_id = ?`).run(
|
||||||
val === '' ? null : val,
|
val === '' ? null : val,
|
||||||
@@ -183,6 +212,22 @@ export function setLibraryAiOverrides(libraryId: string, overrides: Partial<Libr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numberFields: Record<string, number | null | undefined> = {
|
||||||
|
max_tokens_tag: overrides.maxTokensTag,
|
||||||
|
max_tokens_describe: overrides.maxTokensDescribe,
|
||||||
|
max_tokens_extract: overrides.maxTokensExtract,
|
||||||
|
max_tokens_translate: overrides.maxTokensTranslate,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [col, val] of Object.entries(numberFields)) {
|
||||||
|
if (val !== undefined) {
|
||||||
|
db.prepare(`UPDATE library_ai_settings SET ${col} = ? WHERE library_id = ?`).run(
|
||||||
|
val === null ? null : Math.max(1, Math.floor(val)),
|
||||||
|
libraryId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEffectiveAiConfig(libraryId: string): AiConfig {
|
export function getEffectiveAiConfig(libraryId: string): AiConfig {
|
||||||
@@ -200,6 +245,10 @@ export function getEffectiveAiConfig(libraryId: string): AiConfig {
|
|||||||
promptTagger: overrides.promptTagger || global.promptTagger,
|
promptTagger: overrides.promptTagger || global.promptTagger,
|
||||||
promptExtract: overrides.promptExtract || global.promptExtract,
|
promptExtract: overrides.promptExtract || global.promptExtract,
|
||||||
promptTranslate: overrides.promptTranslate || global.promptTranslate,
|
promptTranslate: overrides.promptTranslate || global.promptTranslate,
|
||||||
|
maxTokensTag: overrides.maxTokensTag ?? global.maxTokensTag,
|
||||||
|
maxTokensDescribe: overrides.maxTokensDescribe ?? global.maxTokensDescribe,
|
||||||
|
maxTokensExtract: overrides.maxTokensExtract ?? global.maxTokensExtract,
|
||||||
|
maxTokensTranslate: overrides.maxTokensTranslate ?? global.maxTokensTranslate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ function seedAppSettings(db: Database.Database): void {
|
|||||||
ai_model: '',
|
ai_model: '',
|
||||||
preferred_language: 'English',
|
preferred_language: 'English',
|
||||||
ai_max_retries: '3',
|
ai_max_retries: '3',
|
||||||
|
ai_max_tokens_tag: '8192',
|
||||||
|
ai_max_tokens_describe: '8192',
|
||||||
|
ai_max_tokens_extract: '8192',
|
||||||
|
ai_max_tokens_translate: '8192',
|
||||||
}
|
}
|
||||||
const insert = db.prepare(
|
const insert = db.prepare(
|
||||||
'INSERT OR IGNORE INTO app_settings (key, value) VALUES (?, ?)'
|
'INSERT OR IGNORE INTO app_settings (key, value) VALUES (?, ?)'
|
||||||
@@ -276,6 +280,19 @@ function migrateLibraryAiSettings(db: Database.Database): void {
|
|||||||
prompt_translate TEXT
|
prompt_translate TEXT
|
||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// Add max_tokens columns if they don't exist yet
|
||||||
|
const row = db
|
||||||
|
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='library_ai_settings'")
|
||||||
|
.get() as { sql: string } | undefined
|
||||||
|
if (row && !row.sql.includes('max_tokens_tag')) {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE library_ai_settings ADD COLUMN max_tokens_tag INTEGER;
|
||||||
|
ALTER TABLE library_ai_settings ADD COLUMN max_tokens_describe INTEGER;
|
||||||
|
ALTER TABLE library_ai_settings ADD COLUMN max_tokens_extract INTEGER;
|
||||||
|
ALTER TABLE library_ai_settings ADD COLUMN max_tokens_translate INTEGER;
|
||||||
|
`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateLibrariesType(db: Database.Database): void {
|
function migrateLibrariesType(db: Database.Database): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user