import 'dart:io'; import '../diagnostics/badnote_log.dart'; import '../editor/persistence/sidecar_repository.dart'; import '../models/note.dart'; import '../models/pen_tool.dart'; import 'database_service.dart'; import 'ocr_engine.dart'; import 'stroke_rasterizer.dart'; /// Runs OCR locally: typed text from strokes + handwriting via platform OCR. class OcrService { /// Extract searchable text from [note] and persist it for search. The text is /// written to the note's `*.badnote.json` sidecar `ocrText` field — the /// vault-scan source of truth the Phase 6 search index reads — and also /// appended to the (rebuildable) SQLite FTS cache so legacy callers keep /// working. [note.id] is the synthetic note path, which is exactly the /// `sourceFilePath` the editor opened its [SidecarRepository] with. Future processNote(Note note) async { BadNoteLog.instance.info(LogSubsystem.diag, 'ocr_start', fields: { 'note': note.id, 'strokes': note.strokes.length, }); final parts = []; for (final stroke in note.strokes) { if (stroke.tool == PenTool.text && stroke.textContent != null && stroke.textContent!.trim().isNotEmpty) { parts.add(stroke.textContent!.trim()); } } final handwritingStrokes = note.strokes .where( (s) => s.tool != PenTool.eraser && s.tool != PenTool.text && s.points.isNotEmpty, ) .toList(); if (handwritingStrokes.isNotEmpty) { final png = await StrokeRasterizer.render(handwritingStrokes); if (png != null) { final recognized = await OcrEngine.recognizeImage(png); if (recognized != null && recognized.isNotEmpty) { parts.add(recognized); BadNoteLog.instance.info(LogSubsystem.diag, 'ocr_handwriting', fields: { 'chars': recognized.length, }); } } } final combined = parts.join(' ').trim(); if (combined.isEmpty) { BadNoteLog.instance.debug(LogSubsystem.diag, 'ocr_empty'); return; } // Persist into the note's sidecar so the vault-scan search index finds it. // Prefer the editor's already-open repo (same in-memory sidecar — no race); // if the note is closed, open/flush/dispose a transient handle. final open = SidecarRepositoryRegistry.forPath(note.id); if (open != null) { open.scheduleOcrTextSave(combined); await open.flush(); } else if (await File('${note.id}$kSidecarSuffix').exists()) { final repo = await SidecarRepository.open(note.id, docType: 'notebook'); repo.scheduleOcrTextSave(combined); await repo.flush(); repo.dispose(); } // Also keep the legacy SQLite FTS cache warm (rebuildable; not the source // of truth). Harmless if the row is never read. final db = await DatabaseService.getInstance(); await db.appendOcrToFts(note.id, combined); } }