Files
motovaultpro/ocr/app/routers/decode.py
Eric Gullickson a75f7b5583 feat: add VIN decode endpoint to OCR Python service (refs #224)
Add POST /decode/vin endpoint using Gemini 2.5 Flash for VIN string
decoding. Returns structured vehicle data (year, make, model, trim,
body/drive/fuel type, engine, transmission) with confidence score.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:40:10 -06:00

68 lines
2.0 KiB
Python

"""VIN decode router - Gemini-powered VIN string decoding."""
import logging
import re
import time
from fastapi import APIRouter, HTTPException
from app.engines.gemini_engine import (
GeminiEngine,
GeminiProcessingError,
GeminiUnavailableError,
)
from app.models import VinDecodeRequest, VinDecodeResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/decode", tags=["decode"])
_VIN_REGEX = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
# Shared engine instance (lazy init on first request)
_gemini_engine = GeminiEngine()
@router.post("/vin", response_model=VinDecodeResponse)
async def decode_vin(request: VinDecodeRequest) -> VinDecodeResponse:
"""Decode a VIN string into structured vehicle data using Gemini.
Accepts a 17-character VIN and returns year, make, model, trim, etc.
"""
vin = request.vin.upper().strip()
if not _VIN_REGEX.match(vin):
raise HTTPException(
status_code=400,
detail=f"Invalid VIN format: must be 17 alphanumeric characters (excluding I, O, Q). Got: {vin}",
)
start_ms = time.monotonic_ns() // 1_000_000
try:
result = _gemini_engine.decode_vin(vin)
except GeminiUnavailableError as exc:
logger.error("Gemini unavailable for VIN decode: %s", exc)
raise HTTPException(status_code=503, detail=str(exc)) from exc
except GeminiProcessingError as exc:
logger.error("Gemini processing error for VIN %s: %s", vin, exc)
raise HTTPException(status_code=422, detail=str(exc)) from exc
elapsed_ms = (time.monotonic_ns() // 1_000_000) - start_ms
return VinDecodeResponse(
success=True,
vin=vin,
year=result.year,
make=result.make,
model=result.model,
trimLevel=result.trim_level,
bodyType=result.body_type,
driveType=result.drive_type,
fuelType=result.fuel_type,
engine=result.engine,
transmission=result.transmission,
confidence=result.confidence,
processingTimeMs=elapsed_ms,
error=None,
)