diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index ce49038fb5..f7ff29ae0c 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2093,7 +2093,9 @@ "process_now": "Process OCR", "processing": "Processing...", "processing_started": "OCR processing has been started. Please wait a moment and refresh.", + "processing_complete": "OCR processing complete.", "processing_failed": "Failed to start OCR processing", + "text_filtered_low_confidence": "OCR detected text with {{confidence}}% confidence, but it was discarded because your minimum threshold is {{threshold}}%.\n\nYou can adjust the threshold in Options → Media.", "view_extracted_text": "View extracted text (OCR)" }, "command_palette": { diff --git a/apps/client/src/widgets/type_widgets/ReadOnlyTextRepresentation.tsx b/apps/client/src/widgets/type_widgets/ReadOnlyTextRepresentation.tsx index 9ee376f977..c8f4340c32 100644 --- a/apps/client/src/widgets/type_widgets/ReadOnlyTextRepresentation.tsx +++ b/apps/client/src/widgets/type_widgets/ReadOnlyTextRepresentation.tsx @@ -1,6 +1,6 @@ import "./ReadOnlyTextRepresentation.css"; -import type { TextRepresentationResponse } from "@triliumnext/commons"; +import type { OCRProcessResponse, TextRepresentationResponse } from "@triliumnext/commons"; import { useEffect, useState } from "preact/hooks"; import { t } from "../../services/i18n"; @@ -62,10 +62,27 @@ export function TextRepresentation({ textUrl, processUrl }: TextRepresentationPr async function processOCR() { setProcessing(true); try { - const response = await server.post<{ success: boolean; message?: string }>(processUrl, { forceReprocess: true }); + const response = await server.post(processUrl, { forceReprocess: true }); if (response.success) { - toast.showMessage(t("ocr.processing_started")); - setTimeout(fetchText, 2000); + const result = response.result; + const minConfidence = response.minConfidence ?? 0; + + // Check if text was filtered due to low confidence + if (result && !result.text && result.confidence > 0 && minConfidence > 0) { + const confidencePercent = Math.round(result.confidence * 100); + const thresholdPercent = Math.round(minConfidence * 100); + toast.showMessage( + t("ocr.text_filtered_low_confidence", { + confidence: confidencePercent, + threshold: thresholdPercent + }), + 10000, // Show for 10 seconds since this is important info + "bx bx-info-circle" + ); + } else { + toast.showMessage(t("ocr.processing_complete")); + } + setTimeout(fetchText, 500); } else { toast.showError(response.message || t("ocr.processing_failed")); } diff --git a/apps/server/src/routes/api/ocr.ts b/apps/server/src/routes/api/ocr.ts index 65618b7db8..1177634d00 100644 --- a/apps/server/src/routes/api/ocr.ts +++ b/apps/server/src/routes/api/ocr.ts @@ -1,10 +1,16 @@ -import { TextRepresentationResponse } from "@triliumnext/commons"; +import type { OCRProcessResponse, TextRepresentationResponse } from "@triliumnext/commons"; import type { Request } from "express"; import becca from "../../becca/becca.js"; import ocrService from "../../services/ocr/ocr_service.js"; +import options from "../../services/options.js"; import sql from "../../services/sql.js"; +function getMinConfidenceThreshold(): number { + const minConfidence = options.getOption('ocrMinConfidence') ?? 0; + return parseFloat(minConfidence); +} + /** * @swagger * /api/ocr/process-note/{noteId}: @@ -48,7 +54,7 @@ import sql from "../../services/sql.js"; * - session: [] * tags: ["ocr"] */ -async function processNoteOCR(req: Request<{ noteId: string }>) { +async function processNoteOCR(req: Request<{ noteId: string }>): Promise { const { noteId } = req.params; const { language, forceReprocess = false } = req.body || {}; @@ -62,7 +68,11 @@ async function processNoteOCR(req: Request<{ noteId: string }>) { return [400, { success: false, message: 'Note is not an image or has unsupported format' }]; } - return { success: true, result }; + return { + success: true, + result, + minConfidence: getMinConfidenceThreshold() + }; } /** @@ -108,7 +118,7 @@ async function processNoteOCR(req: Request<{ noteId: string }>) { * - session: [] * tags: ["ocr"] */ -async function processAttachmentOCR(req: Request<{ attachmentId: string }>) { +async function processAttachmentOCR(req: Request<{ attachmentId: string }>): Promise { const { attachmentId } = req.params; const { language, forceReprocess = false } = req.body || {}; @@ -122,7 +132,11 @@ async function processAttachmentOCR(req: Request<{ attachmentId: string }>) { return [400, { success: false, message: 'Attachment is not an image or has unsupported format' }]; } - return { success: true, result }; + return { + success: true, + result, + minConfidence: getMinConfidenceThreshold() + }; } /** diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index 48abb76404..84b16482bb 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -295,6 +295,20 @@ export interface TextRepresentationResponse { message?: string; } +export interface OCRProcessResponse { + success: boolean; + message?: string; + result?: { + text: string; + confidence: number; + extractedAt: string; + language?: string; + pageCount?: number; + }; + /** The minimum confidence threshold that was applied (0-1 scale). */ + minConfidence?: number; +} + export interface IconRegistry { sources: { prefix: string;