Files
BadNote/lib/providers/ocr_provider.dart
Akiba So e939759458
All checks were successful
CI / Windows build (push) Successful in 15m50s
feat(search): index PDF text, OCR scanned PDFs on import
Search now covers handwriting, the PDF text layer, AND scanned
(rasterized) PDFs.

- PdfTextIndexer runs at import: sums the embedded text layer across
  pages; if present it stores that as the document body, otherwise the
  PDF is rasterized and its rendered pages are OCR'd in the background.
  The result lands in the sidecar `pageText` field (distinct from
  `ocrText`, the handwriting OCR). Idempotent (skips a sidecar that
  already has pageText); degrades gracefully with no OCR engine.
- pdfrx_page_text_source abstracts text/render so it's testable.
- VaultSearchIndex now harvests title + typed text + handwriting OCR +
  PDF pageText, so search finds notes, typed PDFs and scanned PDFs.

analyze clean, 409 tests green.
2026-06-25 00:23:19 +08:00

57 lines
2.3 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/ocr_service.dart';
import '../services/pdf_text_indexer.dart';
import '../services/pdfrx_page_text_source.dart';
enum OcrStatus { none, processing, done, failed }
final ocrServiceProvider = Provider<OcrService>((ref) => OcrService());
/// The import-time PDF document-body indexer, wired to the pdfrx-backed embedded
/// text + page-render OCR sources (see [PdfrxPageTextSource]). The import flow
/// fires [PdfTextIndexer.indexPdf] (fire-and-forget) so a scanned PDF's text
/// becomes searchable in the background without blocking the editor opening.
final pdfTextIndexerProvider = Provider<PdfTextIndexer>(
(ref) => PdfTextIndexer(
loadEmbeddedText: PdfrxPageTextSource.loadEmbeddedText,
ocrPages: PdfrxPageTextSource.ocrPages,
),
);
/// Tracks local OCR processing status per note ID.
///
/// This map only ever holds an entry per note that has had OCR triggered in
/// the current session. To keep it from growing without bound over a long
/// session, prune terminal/stale entries via [OcrStatusX] (e.g. remove an
/// entry once its result has been surfaced, or call [OcrStatusX.pruneOcr]
/// after a sweep). Kept as a [StateProvider] so existing call sites that
/// assign `ocrStatusProvider.notifier.state` continue to work.
final ocrStatusProvider = StateProvider<Map<String, OcrStatus>>((ref) => {});
/// Pruning helpers for [ocrStatusProvider] that keep its backing map bounded.
extension OcrStatusX on Ref {
/// Removes the tracked status for [noteId] (e.g. when its note is deleted
/// or its result has been consumed by the UI).
void clearOcr(String noteId) {
final current = read(ocrStatusProvider);
if (!current.containsKey(noteId)) return;
read(ocrStatusProvider.notifier).state = Map<String, OcrStatus>.from(
current,
)..remove(noteId);
}
/// Drops all completed/failed entries, keeping only in-flight work so the
/// map stays bounded.
void pruneOcr() {
final current = read(ocrStatusProvider);
final next = <String, OcrStatus>{
for (final entry in current.entries)
if (entry.value == OcrStatus.processing) entry.key: entry.value,
};
if (next.length != current.length) {
read(ocrStatusProvider.notifier).state = next;
}
}
}