feat: per-extraction OCR language override
Allow users to specify a Tesseract language string (e.g. jpn+jpn_vert)
on a per-extraction basis, overriding the global OCR language setting.
- Add payload column to ai_jobs table (migration) to carry per-call data
- Thread ocrLanguages payload through enqueueJob → processNextJob → extractItemText
- New GET /api/ai-settings/ocr endpoint (requireAuth) returns { ocrMode, ocrLanguages }
- ImageLightbox fetches OCR settings and shows a language input next to the
Extract Text button when mode is hybrid or tesseract (hidden for llm-only)
- MixedView fetches OCR settings and passes them down to EntryTile; kebab
Extract Text on images shows an inline language prompt before dispatching the job
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ interface AiJobRow {
|
||||
started_at: number | null
|
||||
completed_at: number | null
|
||||
item_title: string | null
|
||||
payload: string | null
|
||||
}
|
||||
|
||||
function rowToJob(row: AiJobRow): AiJob {
|
||||
@@ -75,6 +76,7 @@ export function enqueueJob(
|
||||
jobType: AiJobType,
|
||||
libraryId: string,
|
||||
sourceLanguage?: string,
|
||||
payload?: Record<string, string>,
|
||||
): string {
|
||||
const db = getDb()
|
||||
|
||||
@@ -96,9 +98,9 @@ export function enqueueJob(
|
||||
const metadata = jobType === 'translate' && sourceLanguage ? sourceLanguage : null
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO ai_jobs (id, item_key, library_id, job_type, status, error, attempt, max_retries, created_at, item_title)
|
||||
VALUES (?, ?, ?, ?, 'queued', ?, 0, ?, ?, ?)`
|
||||
).run(id, itemKey, libraryId, jobType, metadata, maxRetries, Date.now(), title)
|
||||
`INSERT INTO ai_jobs (id, item_key, library_id, job_type, status, error, attempt, max_retries, created_at, item_title, payload)
|
||||
VALUES (?, ?, ?, ?, 'queued', ?, 0, ?, ?, ?, ?)`
|
||||
).run(id, itemKey, libraryId, jobType, metadata, maxRetries, Date.now(), title, payload ? JSON.stringify(payload) : null)
|
||||
|
||||
// Wake the processor
|
||||
wakeProcessor()
|
||||
@@ -251,6 +253,8 @@ async function processNextJob(): Promise<boolean> {
|
||||
|
||||
// Extract sourceLanguage for translate jobs (stored in error field at enqueue)
|
||||
const sourceLanguage = row.job_type === 'translate' ? row.error : null
|
||||
// Parse job payload (carries per-call overrides, e.g. ocrLanguages for extract jobs)
|
||||
const jobPayload = row.payload ? (JSON.parse(row.payload) as Record<string, string>) : null
|
||||
|
||||
db.prepare(
|
||||
"UPDATE ai_jobs SET status = 'running', started_at = ?, error = NULL WHERE id = ?"
|
||||
@@ -265,7 +269,7 @@ async function processNextJob(): Promise<boolean> {
|
||||
await generateItemDescription(row.item_key)
|
||||
break
|
||||
case 'extract':
|
||||
await extractItemText(row.item_key)
|
||||
await extractItemText(row.item_key, jobPayload?.ocrLanguages)
|
||||
break
|
||||
case 'translate':
|
||||
await translateItemText(row.item_key, sourceLanguage || undefined)
|
||||
|
||||
@@ -538,7 +538,7 @@ async function extractWithTesseract(
|
||||
* Translation is not performed automatically — call translateItemText() separately.
|
||||
* Returns { extractedText, translatedText } where translatedText is always null.
|
||||
*/
|
||||
export async function extractItemText(itemKey: string): Promise<{ extractedText: string; translatedText: string | null }> {
|
||||
export async function extractItemText(itemKey: string, ocrLanguagesOverride?: string): Promise<{ extractedText: string; translatedText: string | null }> {
|
||||
const libraryId = itemKey.split(':')[0]
|
||||
const config = getEffectiveAiConfig(libraryId)
|
||||
|
||||
@@ -567,7 +567,8 @@ export async function extractItemText(itemKey: string): Promise<{ extractedText:
|
||||
throw Object.assign(new Error('Text extraction is only available for images'), { code: 'NO_IMAGE' })
|
||||
}
|
||||
|
||||
const { ocrMode, ocrLanguages, ocrConfidenceThreshold } = config
|
||||
const { ocrMode, ocrLanguages: configOcrLanguages, ocrConfidenceThreshold } = config
|
||||
const ocrLanguages = ocrLanguagesOverride?.trim() || configOcrLanguages
|
||||
|
||||
// ── Tesseract path ────────────────────────────────────────────────────────
|
||||
if (ocrMode === 'tesseract' || ocrMode === 'hybrid') {
|
||||
|
||||
@@ -338,4 +338,12 @@ function migrateAiJobs(db: Database.Database): void {
|
||||
CREATE INDEX IF NOT EXISTS ai_jobs_status ON ai_jobs(status);
|
||||
CREATE INDEX IF NOT EXISTS ai_jobs_created_at ON ai_jobs(created_at);
|
||||
`)
|
||||
|
||||
// Add payload column if not present
|
||||
const aiJobsRow = db
|
||||
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='ai_jobs'")
|
||||
.get() as { sql: string } | undefined
|
||||
if (aiJobsRow && !aiJobsRow.sql.includes('payload')) {
|
||||
db.exec('ALTER TABLE ai_jobs ADD COLUMN payload TEXT')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user