feat: leer comprobantes via OCR externo + Gemini solo-texto

Antes, si Gemini Vision fallaba (cuota/API), el 100% de los comprobantes
del chat web quedaban sin poder leerse. Ahora el job intenta primero un
servicio OCR externo configurable (imagen -> texto) y le pasa el texto
a Gemini para que solo lo estructure en JSON, más barato y sin la cuota
de visión. Si el OCR no está habilitado o falla, cae al método anterior
(Gemini leyendo la imagen directamente) sin romper nada.

Agrega configuración en el panel (URL/token/habilitado + probar conexión)
y la spec del servicio OCR a desplegar en docs/ocr-service-spec.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-08-10 21:49:34 +00:00
co-authored by Claude Sonnet 5
parent 7f4053ea62
commit 7afd22d581
6 changed files with 435 additions and 13 deletions
@@ -150,6 +150,48 @@
<hr class="border-gray-100">
{{-- OCR de comprobantes --}}
<div>
<h3 class="text-sm font-bold text-gray-700 mb-1">OCR de comprobantes (antes de la IA)</h3>
<p class="text-xs text-gray-400 mb-4">Si está habilitado, la imagen del comprobante se manda primero a este servicio para extraer el texto, y luego ese texto (no la imagen) se le pasa a Gemini para estructurarlo. Si el servicio falla o está deshabilitado, se usa el método anterior (Gemini lee la imagen directamente).</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">URL del servicio OCR</label>
<input wire:model.defer="ocr_url" type="text" placeholder="https://ocr.tu-dominio.com"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none font-mono">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Token (Authorization: Bearer)</label>
<input wire:model.defer="ocr_token" type="password" placeholder="Token del servicio OCR"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-emerald-500 outline-none font-mono">
</div>
<div class="flex items-center gap-3">
<input wire:model.defer="ocr_habilitado" type="checkbox" value="1"
id="ocr_hab" class="w-4 h-4 accent-emerald-600">
<label for="ocr_hab" class="text-sm text-gray-700">
Habilitar OCR antes de enviar a la IA
</label>
</div>
<div>
<button wire:click="probarOcr"
class="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-semibold rounded-lg transition">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span wire:loading.remove wire:target="probarOcr">Probar conexión OCR</span>
<span wire:loading wire:target="probarOcr">Probando...</span>
</button>
@if($ocrResult)
<p class="mt-2 text-sm {{ str_contains($ocrResult, '✅') ? 'text-emerald-600' : 'text-red-600' }}">
{{ $ocrResult }}
</p>
@endif
</div>
</div>
</div>
<hr class="border-gray-100">
{{-- Validacion de pagos --}}
<div>
<h3 class="text-sm font-bold text-gray-700 mb-1">Validacion de pagos por foto + correo IMAP</h3>