Files
sirpremiumv2/app/Services/OcrExtractionService.php
T
LizandroandClaude Sonnet 5 5e0c0367d9 feat: costo OCR en dashboard, logs correctos y fix de thinking tokens
- OcrExtractionService: registra cada llamada en ai_usage_logs (servicio=ocr,
  costo=5 COP/foto) y ahora tambien loggea cuando SI funciona, no solo
  cuando falla (antes era imposible confirmar por log que el OCR corrio).
- GeminiVisionService: el log decia siempre "[Gemini Vision]" sin importar
  si la llamada fue de imagen o de texto post-OCR, haciendo imposible
  distinguir cual camino se uso realmente. Ahora usa el servicio real.
- GeminiVisionService: agrega thinkingConfig.thinkingBudget=0 (igual que
  GeminiIntentService) para que el modelo no gaste tokens narrando su
  razonamiento en texto plano antes del JSON final -- se estaba colando
  ese razonamiento en la respuesta y comiendose el presupuesto de tokens.
- GeminiIntentService: resultado ya no queda forzado a 'ok'; ahora filtra
  bloques 'thought' igual que vision, y guarda tokens/raw en el detalle
  para poder diagnosticar sin acceso al servidor.
- ShowAiLogs: card y filtro de costo OCR en el dashboard de consumo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 01:52:59 +00:00

89 lines
3.0 KiB
PHP

<?php
namespace App\Services;
use App\Models\AiUsageLog;
use App\Models\ChatConfig;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class OcrExtractionService
{
private const COSTO_POR_FOTO = 5; // pesos COP, tarifa plana del servicio OCR
private string $url;
private string $token;
public function __construct()
{
$this->url = self::normalizarUrl(ChatConfig::get('ocr_url', ''));
$this->token = ChatConfig::get('ocr_token', '');
}
/** Acepta tanto "https://host" como "https://host/extract" (error comun de configuracion). */
public static function normalizarUrl(string $url): string
{
$url = rtrim(trim($url), '/');
return preg_replace('#/extract$#', '', $url);
}
public function habilitado(): bool
{
return ChatConfig::get('ocr_habilitado', '0') === '1' && $this->url !== '';
}
/** Envia la imagen al servicio OCR externo y devuelve el texto crudo, o null si falla. */
public function extraerTexto(string $base64, string $mimeType, ?int $usuarioId = null, string $canal = 'web'): ?string
{
if (! $this->url) {
return null;
}
$inicio = microtime(true);
try {
$response = Http::withToken($this->token)
->timeout(20)
->post("{$this->url}/extract", [
'image_base64' => $base64,
'mime_type' => $mimeType,
]);
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
$data = $response->json();
if (! $response->successful() || ! ($data['success'] ?? false) || empty($data['text'])) {
$error = $data['error'] ?? ('HTTP ' . $response->status());
Log::warning('[OCR] ' . $error . ': ' . substr($response->body(), 0, 300));
$this->registrarUso($usuarioId, $canal, $tiempoMs, 'error', ['error' => $error]);
return null;
}
Log::info("[OCR] ok ({$tiempoMs}ms): " . substr($data['text'], 0, 200));
$this->registrarUso($usuarioId, $canal, $tiempoMs, 'ok', ['caracteres' => mb_strlen($data['text'])]);
return $data['text'];
} catch (\Throwable $e) {
$tiempoMs = (int) ((microtime(true) - $inicio) * 1000);
Log::warning('[OCR] excepcion: ' . $e->getMessage());
$this->registrarUso($usuarioId, $canal, $tiempoMs, 'error', ['error' => $e->getMessage()]);
return null;
}
}
/** Cada foto enviada al OCR cuesta lo mismo, se lea bien o no. */
private function registrarUso(?int $usuarioId, string $canal, int $tiempoMs, string $resultado, array $detalle): void
{
AiUsageLog::registrar([
'servicio' => 'ocr',
'canal' => $canal,
'usuario_id' => $usuarioId,
'tiempo_ms' => $tiempoMs,
'costo' => self::COSTO_POR_FOTO,
'resultado' => $resultado,
'detalle' => $detalle,
]);
}
}