Add LLM OCR engine via OpenCode Go + mimo-v2-omni
Adds optional LLM-based OCR as an alternative to Tesseract for reading machine IDs from photos. Backend (server.py): - New run_ocr_llm() function calls OpenCode Go API (mimo-v2-omni model) - Auto-falls back to Tesseract if API key missing or call fails - Endpoints /api/analyze and /api/bulk-process accept ?ocr_engine=llm query param (default: tesseract) and ?ocr_model for model override - Configurable via env vars: OPENCODE_GO_API_KEY, LLM_OCR_MODEL - Requires User-Agent: Hermes-Agent/1.0 header for OpenCode Go API Frontend (static/index.html): - Toggle checkbox 'Use LLM OCR' in the UI - OCR engine badge shown in results (llm vs tesseract + model name) - getOcrParams() helper appends ?ocr_engine=llm to API calls Infrastructure: - .gitignore for uploads/ directory Closes: #2
This commit is contained in:
+44
-18
@@ -187,6 +187,20 @@
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* OCR engine toggle */
|
||||
.ocr-toggle {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
|
||||
font-size: 12px; color: var(--text2);
|
||||
}
|
||||
.ocr-toggle label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
|
||||
.ocr-toggle input[type="checkbox"] { accent-color: var(--accent); }
|
||||
.engine-badge {
|
||||
font-size: 10px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
|
||||
background: var(--card2); color: var(--text2); display: inline-block; margin-left: 4px;
|
||||
}
|
||||
.engine-badge.llm { background: var(--accent); color: #fff; }
|
||||
.engine-badge.tesseract { background: var(--card2); color: var(--text2); }
|
||||
|
||||
/* Bulk results */
|
||||
#mapContainer { height: 250px; border-radius: var(--radius-sm); margin-bottom: 10px; display: none; }
|
||||
.bulk-card {
|
||||
@@ -263,6 +277,14 @@
|
||||
<div id="serverResults"></div>
|
||||
</div>
|
||||
|
||||
<!-- OCR engine toggle -->
|
||||
<div class="ocr-toggle" style="margin-bottom:4px;">
|
||||
<label>
|
||||
<input type="checkbox" id="ocrLlmToggle" onchange="onOcrToggle()">
|
||||
🧠 Use LLM OCR (<code id="ocrModelLabel">mimo-v2-omni</code>)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Bulk process button -->
|
||||
<button class="btn btn-primary" id="bulkBtn" style="display:none;margin-top:12px;" onclick="startBulkProcess()">
|
||||
🔍 Bulk Process GPS Photos
|
||||
@@ -440,7 +462,7 @@ async function uploadSelected() {
|
||||
fd.append('file', file, file.name || 'photo.jpg');
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/analyze', { method: 'POST', body: fd });
|
||||
const resp = await fetch('/api/analyze' + getOcrParams(), { method: 'POST', body: fd });
|
||||
const data = await resp.json();
|
||||
|
||||
let html = '';
|
||||
@@ -465,21 +487,20 @@ async function uploadSelected() {
|
||||
|
||||
// OCR
|
||||
const ocr = data.ocr;
|
||||
if (ocr.available) {
|
||||
html += '<div style="margin-top:10px;font-weight:600;">🔤 OCR:</div>';
|
||||
if (ocr.raw_text) {
|
||||
html += '<div class="ocr-text">' + esc(ocr.raw_text) + '</div>';
|
||||
}
|
||||
if (ocr.match_5dash6) {
|
||||
const mid = ocr.match_5dash6.replace(/[^0-9]/g,'').slice(-5);
|
||||
html += '<div style="margin-top:4px;color:var(--green);">✅ Matched: <strong>' + esc(ocr.match_5dash6) + '</strong> → machine ID: ' + mid + '</div>';
|
||||
// Auto-lookup
|
||||
lookupAsset(mid);
|
||||
} else if (ocr.match_5plus) {
|
||||
html += '<div style="margin-top:4px;color:var(--amber);">⚠️ Digits: <strong>' + esc(ocr.match_5plus) + '</strong> (no 5-6 pattern)</div>';
|
||||
} else {
|
||||
html += '<div style="margin-top:4px;color:var(--text3);">No machine ID found in image</div>';
|
||||
}
|
||||
const engineCls = ocr.engine === 'llm' ? 'llm' : 'tesseract';
|
||||
html += '<div style="margin-top:10px;font-weight:600;">🔤 OCR <span class="engine-badge ' + engineCls + '">' + esc(ocr.engine || 'tesseract') + (ocr.llm_model ? ' ' + ocr.llm_model : '') + '</span></div>';
|
||||
if (ocr.raw_text) {
|
||||
html += '<div class="ocr-text">' + esc(ocr.raw_text) + '</div>';
|
||||
}
|
||||
if (ocr.match_5dash6) {
|
||||
const mid = ocr.match_5dash6.replace(/[^0-9]/g,'').slice(-5);
|
||||
html += '<div style="margin-top:4px;color:var(--green);">✅ Matched: <strong>' + esc(ocr.match_5dash6) + '</strong> → machine ID: ' + mid + '</div>';
|
||||
// Auto-lookup
|
||||
lookupAsset(mid);
|
||||
} else if (ocr.match_5plus) {
|
||||
html += '<div style="margin-top:4px;color:var(--amber);">⚠️ Digits: <strong>' + esc(ocr.match_5plus) + '</strong> (no 5-6 pattern)</div>';
|
||||
} else {
|
||||
html += '<div style="margin-top:4px;color:var(--text3);">No machine ID found in image</div>';
|
||||
}
|
||||
|
||||
div.innerHTML = html;
|
||||
@@ -540,7 +561,7 @@ async function startBulkProcess() {
|
||||
gpsPhotos.forEach(p => fd.append('files', p.file, p.file.name || 'photo.jpg'));
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/bulk-process', { method: 'POST', body: fd });
|
||||
const resp = await fetch('/api/bulk-process' + getOcrParams(), { method: 'POST', body: fd });
|
||||
const data = await resp.json();
|
||||
|
||||
// Merge thumbnails back into results
|
||||
@@ -753,8 +774,13 @@ function esc(s) {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function getOcrParams() {
|
||||
const useLlm = document.getElementById('ocrLlmToggle').checked;
|
||||
if (!useLlm) return '';
|
||||
return '?ocr_engine=llm';
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user