Spaces:
Sleeping
Sleeping
| from unittest import result | |
| from fastapi import FastAPI, File, UploadFile, HTTPException | |
| from google import genai | |
| from httpcore import request | |
| from google.genai import types | |
| from dotenv import load_dotenv | |
| from pydantic import BaseModel | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM | |
| import cv2 | |
| from paddleocr import PaddleOCR | |
| import numpy as np | |
| import time | |
| import os | |
| os.environ["OMP_NUM_THREADS"] = "1" | |
| os.environ["FLAGS_allocator_strategy"] = "naive_best_fit" | |
| MODEL_ID = "models/gemini-2.5-flash" | |
| MODEL_FALLBACK = "models/gemini-3.1-flash-lite-preview" | |
| tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-small") | |
| model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-small") | |
| modelo_local = pipeline("text2text-generation", model=model, tokenizer=tokenizer) | |
| load_dotenv() | |
| api_key = os.environ.get("GEMINI_API_KEY") | |
| client = genai.Client(api_key=api_key) | |
| config = types.GenerateContentConfig( | |
| temperature=0, | |
| ) | |
| class ChatRequest(BaseModel): | |
| pregunta: str | |
| contexto: str = "" | |
| historial: list[str] = [] | |
| class ChatResponse(BaseModel): | |
| respuesta: str | |
| historial: list[str] = [] | |
| class OCRResponse(BaseModel): | |
| texto: str | |
| app = FastAPI(title="OCR API", version="1.0.0") | |
| ocr = PaddleOCR(use_angle_cls=False, lang="es", det_limit_side_len=480) | |
| try: | |
| local_model = pipeline("text2text-generation", model="google/flan-t5-small") | |
| except Exception as e: | |
| print(f"Aviso: No se pudo cargar el modelo local: {e}") | |
| local_model = None | |
| app.mount("/static", StaticFiles(directory="static", html=True), name="static") | |
| def home(): | |
| return FileResponse("static/index.html") | |
| def prompt(request: ChatRequest): | |
| max_reintentos = 2 | |
| segundos_espera = 1.5 | |
| e = "Error desconocido" | |
| try: | |
| pregunta = request.pregunta | |
| historial = request.historial | |
| texto_ocr = request.contexto # <--- Tomamos el texto enviado por el frontend | |
| texto_prompt = f""" | |
| Eres un asistente que responde preguntas usando SOLO información del documento. | |
| REGLAS IMPORTANTES: | |
| - No copies el documento completo. | |
| - No repitas texto largo del documento. | |
| - Extrae SOLO la información necesaria. | |
| - Si la respuesta no está en el documento, di: "No aparece en el documento". | |
| - Responde de forma breve y directa. | |
| DOCUMENTO: | |
| \"\"\"{texto_ocr}\"\"\" <--- Cambiado: ahora usa el OCR | |
| HISTORIAL: | |
| {historial} | |
| PREGUNTA: | |
| {pregunta} | |
| RESPUESTA: | |
| """ | |
| for intento in range(max_reintentos): | |
| try: | |
| response = client.models.generate_content( | |
| model=MODEL_ID, | |
| contents=texto_prompt, | |
| config=config | |
| ) | |
| return ChatResponse(respuesta=response.text) | |
| except Exception as ex: | |
| e = ex | |
| print(f"Intento {intento+1} fallido: {e}") | |
| time.sleep(segundos_espera) | |
| try: | |
| response = client.models.generate_content( | |
| model=MODEL_FALLBACK, | |
| contents=texto_prompt, | |
| config=config | |
| ) | |
| return ChatResponse(respuesta=response.text) | |
| except Exception as e_fallback: | |
| print(f"Fallido: {e_fallback}") | |
| try: | |
| # Simplificamos el prompt para el modelo local pequeño | |
| res_local = modelo_local(f"question: {pregunta} context: {texto_ocr}", max_new_tokens=50) | |
| return ChatResponse(respuesta=res_local[0]['generated_text']) | |
| except Exception as e_final: | |
| raise HTTPException(status_code=500, detail="Error en todos los modelos (incluyendo local)") | |
| except Exception as e: | |
| if isinstance(e, HTTPException): raise e | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| except Exception as e: | |
| if isinstance(e, HTTPException): raise e | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def extraer_texto(resultado_ocr): | |
| textos = [] | |
| # PaddleOCR devuelve una lista de páginas. | |
| # Validamos que el resultado no sea None y que la primera página tenga contenido. | |
| if resultado_ocr and resultado_ocr[0] is not None: | |
| for linea in resultado_ocr[0]: | |
| # linea[1][0] es donde reside el texto detectado | |
| textos.append(linea[1][0]) | |
| return " ".join(textos) | |
| def preprocesar_imagen(image_bytes): | |
| # Convertir bytes a imagen | |
| nparr = np.frombuffer(image_bytes, np.uint8) | |
| img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) | |
| if img is None: | |
| raise ValueError("No se pudo decodificar la imagen") | |
| # PaddleOCR maneja internamente el binarizado, | |
| # es mejor enviarle la imagen limpia o solo en gris. | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| return gray | |
| async def ocr_image(file: UploadFile = File(...)): | |
| if file.content_type not in {"image/png", "image/jpeg", "image/jpg", "image/webp"}: | |
| raise HTTPException(status_code=400, detail="Formato no soportado") | |
| try: | |
| # Leemos los bytes del archivo | |
| contenido = await file.read() | |
| # Preprocesamos y ejecutamos OCR | |
| img = preprocesar_imagen(contenido) | |
| # cls=True activa la clasificación de ángulo si se configuró en la instancia | |
| result = ocr.ocr(img) | |
| texto_extraido = extraer_texto(result) | |
| if not texto_extraido.strip(): | |
| return OCRResponse(texto="No se detectó texto en la imagen.") | |
| return OCRResponse(texto=texto_extraido) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Error OCR: {str(e)}") | |