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
@@ -18,6 +18,7 @@ class ShowConfiguracionChat extends Component
public string $webhookResult = '';
public string $whisperResult = '';
public string $geminiResult = '';
public string $ocrResult = '';
// ── Gemini IA ─────────────────────────────────────────────
public string $gemini_api_key = '';
@@ -35,6 +36,11 @@ class ShowConfiguracionChat extends Component
public string $whisper_token = '';
public string $whisper_habilitado = '0';
// ── OCR de comprobantes (previo a Gemini) ──────────────────
public string $ocr_url = '';
public string $ocr_token = '';
public string $ocr_habilitado = '0';
// ── Bre-B ─────────────────────────────────────────────────
public string $recarga_banco_nombre = '';
public string $recarga_banco_llave = '';
@@ -47,6 +53,7 @@ class ShowConfiguracionChat extends Component
'gemini_api_key', 'gemini_model', 'gemini_habilitado', 'gemini_prompt_extra',
'validacion_foto_habilitada', 'validacion_accion_auto', 'validacion_monto_tolerancia',
'whisper_url', 'whisper_token', 'whisper_habilitado',
'ocr_url', 'ocr_token', 'ocr_habilitado',
'recarga_banco_nombre', 'recarga_banco_llave', 'recarga_banco_titular',
];
@@ -79,6 +86,7 @@ class ShowConfiguracionChat extends Component
'gemini_api_key', 'gemini_model', 'gemini_habilitado', 'gemini_prompt_extra',
'validacion_foto_habilitada', 'validacion_accion_auto', 'validacion_monto_tolerancia',
'whisper_url', 'whisper_token', 'whisper_habilitado',
'ocr_url', 'ocr_token', 'ocr_habilitado',
'recarga_banco_nombre', 'recarga_banco_llave', 'recarga_banco_titular',
];
@@ -172,6 +180,42 @@ class ShowConfiguracionChat extends Component
}
}
public function probarOcr(): void
{
$url = rtrim(trim($this->ocr_url ?: ChatConfig::get('ocr_url')), '/');
$token = trim($this->ocr_token ?: ChatConfig::get('ocr_token'));
if (! $url) {
$this->ocrResult = '⚠️ La URL del servicio OCR está vacía.';
return;
}
// 1x1 png transparente, solo para verificar que el servicio responde.
$pixel = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=');
try {
$inicio = microtime(true);
$response = Http::withToken($token)
->timeout(15)
->post("{$url}/extract", [
'image_base64' => base64_encode($pixel),
'mime_type' => 'image/png',
]);
$ms = (int) ((microtime(true) - $inicio) * 1000);
if ($response->successful()) {
$this->ocrResult = "✅ Servicio OCR responde OK ({$ms}ms). " .
"Respuesta: " . substr($response->body(), 0, 200);
} else {
$this->ocrResult = "❌ El servicio respondió con error " . $response->status() . ": " .
substr($response->body(), 0, 200);
}
} catch (\Throwable $e) {
$this->ocrResult = '❌ No se pudo conectar con el servicio OCR: ' . $e->getMessage();
}
}
public function registrarWebhook(): void
{
$token = trim($this->telegram_token);