Módulo de audio y avance imágenes (schemas y openAI) #1
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/v1/endpoints/audio/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/api/v1/endpoints/audio/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,8 @@ Homologación:
|
||||
Sin importar qué proveedor de IA se utilice, el resultado siempre se entrega en el mismo formato estándar para Qualidot.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
|
||||
from fastapi.security import APIKeyHeader
|
||||
@@ -19,7 +21,7 @@ from app.services.image.evaluations_adapters import evaluate_image_with_provider
|
||||
# Inicializar el router de FastAPI para este módulo
|
||||
image_router_analysis = APIRouter()
|
||||
|
||||
@image_router_analysis.post("/evaluations/", response_model=StandardImageAnalysisResult)
|
||||
@image_router_analysis.post("/evaluations", response_model=StandardImageAnalysisResult)
|
||||
async def evaluate_image(image_request: ImageRequestFile = Depends()) -> StandardImageAnalysisResult:
|
||||
"""
|
||||
Endpoint para analizar imágenes usando una rúbrica de evaluación infiriendo el proveedor de IA
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,7 @@ class Settings:
|
||||
# ---------------------------------------------------------------
|
||||
# Proveedores de Imagen
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
CLARIFAI_API_KEY = os.getenv("CLARIFAI_API_KEY", "")
|
||||
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
|
||||
settings = Settings()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,28 +14,22 @@ Pendiente (recomendación personal):
|
||||
- Delimitar los proveedores de IA soportados inicialmente
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import UploadFile
|
||||
from fastapi.params import File, Form
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
@dataclass
|
||||
class AudioRequestFile:
|
||||
"""Modelo de solicitud para transcripción de audio."""
|
||||
def __init__(
|
||||
self,
|
||||
file: UploadFile = File(..., description="Archivo de audio a procesar (ej. mp3, wav)"),
|
||||
provider: str = Form(..., description="Proveedor de IA a utilizar (ej. openai, assemblyai)"),
|
||||
model: str = Form(..., description="Modelo de IA a utilizar (ej. whisper-1)"),
|
||||
diarization: Optional[bool] = Form(False, description="Activa la separación e identificación de múltiples hablantes"),
|
||||
timestamps: Optional[bool] = Form(False, description="Activa las marcas de tiempo exactas por cada segmento hablado"),
|
||||
sentiment: Optional[bool] = Form(False, description="Activa el análisis de sentimiento (POSITIVO/NEGATIVO) por segmento")
|
||||
):
|
||||
self.file = file
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.diarization = diarization
|
||||
self.timestamps = timestamps
|
||||
self.sentiment = sentiment
|
||||
file: UploadFile = File(..., description="Archivo de audio a procesar (ej. mp3, wav)")
|
||||
provider: str = Form(..., description="Proveedor de IA a utilizar (ej. openai, assemblyai)")
|
||||
model: str = Form(..., description="Modelo de IA a utilizar (ej. whisper-1)")
|
||||
diarization: Optional[bool] = Form(False, description="Activa la separación e identificación de múltiples hablantes")
|
||||
timestamps: Optional[bool] = Form(False, description="Activa las marcas de tiempo exactas por cada segmento hablado")
|
||||
sentiment: Optional[bool] = Form(False, description="Activa el análisis de sentimiento (POSITIVO/NEGATIVO) por segmento")
|
||||
|
||||
class TranscriptionSegment(BaseModel):
|
||||
"""Modelo que representa un segmento de transcripción de audio detallado."""
|
||||
|
||||
@@ -9,36 +9,52 @@ Homologación:
|
||||
Garantiza que el resultado del análisis de imágenes siempre se entregue en el mismo
|
||||
formato estándar para Qualidot.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import UploadFile
|
||||
from fastapi.params import File, Form
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
class ImageEvaluationCriteria(BaseModel):
|
||||
"""Modelo que representa un criterio de evaluación específico dentro de una rúbrica de análisis de imágenes."""
|
||||
name: str = Field(..., description="Nombre del criterio de evaluación")
|
||||
description: str = Field(None, description="Descripción detallada del criterio")
|
||||
score: float = Field(None, description="Puntuación asignada al criterio después del análisis")
|
||||
subcriteria: Optional[List["ImageEvaluationCriteria"]] = Field(None, description="Lista de subcriterios de evaluación")
|
||||
|
||||
class ImageEvaluationRubric(BaseModel):
|
||||
"""Modelo que representa una rúbrica de evaluación personalizada para análisis de imágenes."""
|
||||
name: str = Field(..., description="Nombre de la rúbrica de evaluación")
|
||||
description: str = Field(None, description="Descripción detallada de la rúbrica")
|
||||
category: Optional[str] = Field(
|
||||
None,
|
||||
description="Contexto jerárquico de la categoría para especializar a la IA (ej. Audio > Education > English > B2)"
|
||||
)
|
||||
criteria: List[ImageEvaluationCriteria] = Field(None, description="Lista de criterios de evaluación específicos")
|
||||
|
||||
@dataclass
|
||||
class ImageRequestFile:
|
||||
"""Modelo de solicitud para análisis de imágenes."""
|
||||
def __init__(
|
||||
self,
|
||||
file: UploadFile = File(..., description="Archivo de imagen a procesar (ej. jpg, png)"),
|
||||
provider: str = Form(..., description="Proveedor de IA a utilizar (ej. openai, google)"),
|
||||
model: str = Form(..., description="Modelo de IA a utilizar (ej. vision-1)"),
|
||||
rubric: Optional[str] = Form(None, description="Rúbrica de evaluación personalizada para el análisis")
|
||||
):
|
||||
self.file = file
|
||||
self.provider = provider
|
||||
self.model = model
|
||||
self.rubric = rubric
|
||||
file: UploadFile = File(..., description="Archivo de imagen a procesar (ej. jpg, png)")
|
||||
provider: str = Form(..., description="Proveedor de IA a utilizar (ej. openai, google)")
|
||||
model: str = Form(..., description="Modelo de IA a utilizar (ej. vision-1)")
|
||||
rubric: UploadFile = File(..., description="Archivo JSON de evaluación")
|
||||
|
||||
class StandardImageAnalysisResult(BaseModel):
|
||||
"""Modelo que representa el resultado estándar de un análisis de imágenes para Qualidot."""
|
||||
status: str = Field(
|
||||
...,
|
||||
description="Estado del análisis (ej. 'success', 'error')"
|
||||
)
|
||||
analysis: Optional[dict] = Field(
|
||||
None,
|
||||
description="Resultados detallados del análisis de la imagen, estructurados según la rúbrica si se proporcionó"
|
||||
)
|
||||
error: Optional[str] = Field(
|
||||
None,
|
||||
description="Mensaje de error en caso de que el análisis haya fallado"
|
||||
)
|
||||
status: str = Field(..., description="Estado del análisis (ej. 'success', 'error')")
|
||||
original_filename: str = Field(..., description="Nombre original del archivo de imagen procesada")
|
||||
provider_used: str = Field(..., description="Proveedor de IA utilizado para el análisis (ej. 'OpenAI')")
|
||||
model_used: str = Field(..., description="Modelo específico utilizado para el análisis (ej. 'vision-1')")
|
||||
score: float = Field(None, description="Puntuación general asignada a la imagen después del análisis")
|
||||
feedback: str = Field(None, description="Comentarios o retroalimentación generada por el modelo de IA sobre la imagen")
|
||||
detailed_criteria: List[ImageEvaluationCriteria] = Field(None, description="Lista de criterios de evaluación detallados con sus respectivas puntuaciones y descripciones")
|
||||
|
||||
|
||||
class StandardImageAnalysis(BaseModel):
|
||||
"""Modelo que representa el resultado estándar de un análisis de imágenes para Qualidot."""
|
||||
score: float = Field(None, description="Puntuación general asignada a la imagen después del análisis")
|
||||
feedback: str = Field(None, description="Comentarios o retroalimentación generada por el modelo de IA sobre la imagen")
|
||||
detailed_criteria: List[ImageEvaluationCriteria] = Field(None, description="Lista de criterios de evaluación detallados con sus respectivas puntuaciones y descripciones")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -10,9 +10,14 @@ Propósito:
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import HTTPException
|
||||
from openai import OpenAI, AsyncOpenAI
|
||||
import assemblyai as aai
|
||||
from deepgram import (
|
||||
DeepgramClient,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.schemas.audio_standard import AudioRequestFile
|
||||
from app.schemas.audio_standard import StandardTranscriptionResult
|
||||
@@ -24,6 +29,7 @@ async def transcribe_audio_with_provider(audio_request: AudioRequestFile) -> Sta
|
||||
"""
|
||||
Función de adaptador para transcribir audio usando el proveedor de IA configurado.
|
||||
"""
|
||||
load_dotenv()
|
||||
provider = audio_request.provider.lower()
|
||||
|
||||
match provider:
|
||||
@@ -31,6 +37,8 @@ async def transcribe_audio_with_provider(audio_request: AudioRequestFile) -> Sta
|
||||
return await transcribe_with_openai(audio_request)
|
||||
case "assemblyai":
|
||||
return await transcribe_with_assemblyai(audio_request)
|
||||
case "deepgram":
|
||||
return await transcribe_with_deepgram(audio_request)
|
||||
case _:
|
||||
raise ValueError(f"Proveedor de IA no soportado: {audio_request.provider}")
|
||||
|
||||
@@ -39,6 +47,7 @@ async def transcribe_with_openai(audio_request: AudioRequestFile) -> StandardTra
|
||||
"""
|
||||
Función de adaptador para transcribir audio usando OpenAI.
|
||||
"""
|
||||
|
||||
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
||||
|
||||
audio_content = await audio_request.file.read()
|
||||
@@ -112,7 +121,8 @@ async def transcribe_with_assemblyai(audio_request: AudioRequestFile) -> Standar
|
||||
temp_audio_path = temp_audio.name
|
||||
|
||||
#Definimos el modelo a usar
|
||||
config = aai.TranscriptionConfig(language_code="es", speaker_labels=audio_request.diarization,
|
||||
config = aai.TranscriptionConfig(speech_models = [audio_request.model],
|
||||
language_code="es", speaker_labels=audio_request.diarization,
|
||||
sentiment_analysis=audio_request.sentiment)
|
||||
|
||||
transcription_obj = aai.Transcriber(config=config).transcribe(temp_audio_path)
|
||||
@@ -159,5 +169,71 @@ async def transcribe_with_assemblyai(audio_request: AudioRequestFile) -> Standar
|
||||
os.unlink(temp_audio_path)
|
||||
except Exception:
|
||||
pass
|
||||
# Aquí iría la implementación específica para AssemblyAI, similar a la de OpenAI pero usando su SDK y formato de respuesta
|
||||
pass
|
||||
|
||||
async def transcribe_with_deepgram(audio_request: AudioRequestFile):
|
||||
"""
|
||||
Función de adaptador para transcribir audio usando Deepgram.
|
||||
"""
|
||||
|
||||
#Inicializamos el cliente de Deepgram
|
||||
deepgram = DeepgramClient(api_key=settings.DEEPGRAM_API_KEY)
|
||||
audio_content = await audio_request.file.read()
|
||||
temp_audio_path = None
|
||||
|
||||
# Validar el audio antes de continuar
|
||||
validate_audio_request(audio_request, audio_content)
|
||||
|
||||
try:
|
||||
# Crear archivo temporal para el audio
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False,
|
||||
suffix=os.path.splitext(audio_request.file.filename)[1]
|
||||
) as temp_audio:
|
||||
temp_audio.write(audio_content)
|
||||
temp_audio_path = temp_audio.name
|
||||
with open(temp_audio_path, "rb") as audio_file:
|
||||
response = deepgram.listen.v1.media.transcribe_file(
|
||||
request=audio_file.read(),
|
||||
model=audio_request.model,
|
||||
sentiment=audio_request.sentiment,
|
||||
utterances=audio_request.diarization,
|
||||
diarize=audio_request.diarization,
|
||||
# Deepgram no tiene una opción específica de "timestamps", pero sí devuelve marcas de tiempo por segmento, así que no es necesario un parámetro adicional para eso
|
||||
smart_format=True,
|
||||
language='es',
|
||||
)
|
||||
response_json = json.loads(response.json())
|
||||
|
||||
result = StandardTranscriptionResult(
|
||||
status="success",
|
||||
original_filename=audio_request.file.filename,
|
||||
full_transcript=response_json.get("results", {}).get("channels", [{}])[0].get("alternatives", [{}])[0].get("transcript", ""),
|
||||
model_used=audio_request.model,
|
||||
provider_used="Deepgram",
|
||||
confidence_score=response_json.get("results", {}).get("channels", [{}])[0].get("alternatives", [{}])[0].get("confidence"),
|
||||
segments=[
|
||||
{
|
||||
"text": sentence.get("text", ""),
|
||||
"speaker": f"Speaker {paragraph.get('speaker')}" if audio_request.diarization and paragraph.get("speaker") is not None else None,
|
||||
"start_time": sentence.get("start") if audio_request.timestamps else None,
|
||||
"end_time": sentence.get("end") if audio_request.timestamps else None,
|
||||
"sentiment": sentence.get("sentiment") if audio_request.sentiment else None
|
||||
}
|
||||
for paragraph in response_json.get("results", {}).get("channels", [{}])[0].get("alternatives", [{}])[0].get("paragraphs", {}).get("paragraphs", [])
|
||||
for sentence in paragraph.get("sentences", [])
|
||||
] if (audio_request.diarization or audio_request.timestamps or audio_request.sentiment) else None
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
# Capturamos cualquier error de OpenAI o de lectura de archivos
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error transcribiendo el audio: {str(e)}"
|
||||
)
|
||||
|
||||
finally:
|
||||
if temp_audio_path and os.path.exists(temp_audio_path):
|
||||
try:
|
||||
os.unlink(temp_audio_path)
|
||||
except Exception:
|
||||
pass
|
||||
Binary file not shown.
BIN
app/services/image/__pycache__/prompt_builder.cpython-312.pyc
Normal file
BIN
app/services/image/__pycache__/prompt_builder.cpython-312.pyc
Normal file
Binary file not shown.
@@ -8,14 +8,22 @@ Propósito:
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
import mimetypes
|
||||
import tempfile
|
||||
import os
|
||||
from fastapi import HTTPException
|
||||
from matplotlib import image
|
||||
from openai import OpenAI, AsyncOpenAI
|
||||
import assemblyai as aai
|
||||
from pyparsing.common import Any
|
||||
from app.core.config import settings
|
||||
from app.schemas.image_standard import ImageRequestFile, StandardImageAnalysisResult
|
||||
from app.schemas.image_standard import ImageRequestFile, StandardImageAnalysisResult, ImageEvaluationRubric
|
||||
from app.core import config
|
||||
from app.utilities.image_utilities import json_to_rubric, encode_image_from_bytes
|
||||
from app.services.image.prompt_builder import build_image_evaluation_prompt
|
||||
from anthropic import Anthropic
|
||||
|
||||
# Función de adaptador principal que infiere el proveedor y llama al adaptador específico
|
||||
async def evaluate_image_with_provider(image_request: ImageRequestFile) -> StandardImageAnalysisResult:
|
||||
@@ -24,45 +32,174 @@ async def evaluate_image_with_provider(image_request: ImageRequestFile) -> Stand
|
||||
"""
|
||||
provider = image_request.provider.lower()
|
||||
|
||||
content = await image_request.rubric.read()
|
||||
rubric_dict = json.loads(content)
|
||||
rubric = json_to_rubric(rubric_dict)
|
||||
prompt = build_image_evaluation_prompt(rubric)
|
||||
|
||||
match provider:
|
||||
case "openai":
|
||||
return await evaluate_with_openai(image_request)
|
||||
case "inserte nombre de otra ia aqui":
|
||||
return await evaluate_with_ai_model2(image_request)
|
||||
|
||||
# AGREGAR OTROS CASOS PARA DIFERENTES PROVEEDORES DE IA AQUÍ
|
||||
|
||||
return await evaluate_with_openai(image_request, prompt)
|
||||
case "clarifai":
|
||||
return await evaluate_with_clarifai(image_request, prompt)
|
||||
case "claude":
|
||||
return await evaluate_with_claude(image_request, prompt)
|
||||
case _:
|
||||
raise ValueError(f"Proveedor de IA no soportado: {image_request.provider}")
|
||||
|
||||
# Función de adaptador para evaluar imágenes usando OpenAI
|
||||
async def evaluate_with_openai(image_request: ImageRequestFile) -> StandardImageAnalysisResult:
|
||||
async def evaluate_with_openai(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult:
|
||||
"""
|
||||
Función de adaptador para evaluar imágenes usando OpenAI.
|
||||
(Plantilla para futuras implementaciones)
|
||||
"""
|
||||
|
||||
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
||||
|
||||
# PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON OPENAI:
|
||||
# 1. Validar la imagen de entrada (tamaño, formato, etc.)
|
||||
# 2. Configurar el cliente de OpenAI con la clave API
|
||||
# 3. Llamar a la API de OpenAI para evaluar la imagen
|
||||
# 4. Convertir la respuesta de OpenAI al formato estándar de evaluación de imágenes de Qualidot
|
||||
# 5. Manejar errores y excepciones adecuadamente
|
||||
raise NotImplementedError("La función evaluate_with_openai aún no está implementada.")
|
||||
image_bytes = await image_request.file.read()
|
||||
base64_image = encode_image_from_bytes(image_bytes)
|
||||
|
||||
try:
|
||||
response = await client.chat.completions.create(
|
||||
model=image_request.model,
|
||||
messages=[
|
||||
{"role": "user", "content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{base64_image}"
|
||||
}
|
||||
}
|
||||
]}
|
||||
],
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
resultado = json.loads(response.choices[0].message.content)
|
||||
return StandardImageAnalysisResult(
|
||||
status="success",
|
||||
original_filename=image_request.file.filename,
|
||||
provider_used="OpenAI",
|
||||
model_used=image_request.model,
|
||||
**resultado
|
||||
)
|
||||
except Exception as e:
|
||||
# Capturamos cualquier error de OpenAI o de lectura de archivos
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error evaluando la imagen: {str(e)}"
|
||||
)
|
||||
|
||||
async def evaluate_with_ai_model2(image_request: ImageRequestFile) -> StandardImageAnalysisResult:
|
||||
async def evaluate_with_clarifai(image_request: ImageRequestFile, rubric: ImageEvaluationRubric, prompt: str) -> StandardImageAnalysisResult:
|
||||
"""
|
||||
Función de adaptador para transcribir video usando otra AI.
|
||||
(Plantilla para futuras implementaciones)
|
||||
Función de adaptador para evaluar imágenes usando Clarifai con un modelo Multimodal.
|
||||
"""
|
||||
try:
|
||||
# 1. Obtener el token de configuración (PAT)
|
||||
pat = settings.CLARIFAI_API_KEY
|
||||
if not pat:
|
||||
raise ValueError("La clave CLARIFAI_API_KEY no está configurada en el entorno.")
|
||||
|
||||
# PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE TRANSCRIPCIÓN CON IA (ELEGIR MODELO):
|
||||
# 1. Validar el video de entrada (tamaño, formato, etc.)
|
||||
# 2. Configurar el cliente de OpenAI con la clave API
|
||||
# 3. Llamar a la API de OpenAI para transcribir el video
|
||||
# 4. Convertir la respuesta de OpenAI al formato estándar de resumen de texto de Qualidot
|
||||
# 5. Manejar errores y excepciones adecuadamente
|
||||
raise NotImplementedError("La función transcribe_with_ai_model2 aún no está implementada.")
|
||||
# 2. Obtener la URL del modelo enviada en la petición
|
||||
model_url = image_request.model
|
||||
|
||||
# Inicializar el modelo de Clarifai
|
||||
model = Model(url=model_url, pat=pat)
|
||||
|
||||
# Otros modelos de IA
|
||||
# 3. Leer los bytes de la imagen subida
|
||||
image_bytes = await image_request.file.read()
|
||||
|
||||
if not image_bytes:
|
||||
raise ValueError("El archivo de imagen recibido está vacío.")
|
||||
|
||||
# 4. Preparar el input multimodal para Clarifai combinando la imagen y el prompt de evaluación
|
||||
multimodal_input = Inputs.get_multimodal_input(
|
||||
input_id="image_evaluation",
|
||||
image_bytes=image_bytes,
|
||||
raw_text=prompt
|
||||
)
|
||||
|
||||
# 5. Llamar a la API de Clarifai para evaluar la imagen
|
||||
predict_response = model.predict([multimodal_input])
|
||||
|
||||
# Extraer el texto crudo de la respuesta del modelo
|
||||
raw_output = predict_response.outputs[0].data.text.raw
|
||||
|
||||
# 6. Limpiar la respuesta y convertirla a JSON
|
||||
# Los LLMs suelen devolver el JSON envuelto en bloques de markdown (```json ... ```)
|
||||
clean_json = raw_output.replace("```json", "").replace("```", "").strip()
|
||||
|
||||
# Convertir el string limpio a un diccionario de Python
|
||||
parsed_data = json.loads(clean_json)
|
||||
|
||||
# 7. Retornar el resultado validado contra tu esquema estándar de Pydantic
|
||||
return StandardImageAnalysisResult(**parsed_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
# Error específico si el modelo alucinó texto extra y no devolvió un JSON válido
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"El modelo de Clarifai no devolvió un JSON válido. Error: {str(e)} | Respuesta cruda: {raw_output if 'raw_output' in locals() else 'N/A'}"
|
||||
)
|
||||
except Exception as e:
|
||||
# Captura cualquier otro error (problemas de red, token inválido, URL incorrecta, etc.)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error interno al evaluar con Clarifai: {str(e)}"
|
||||
)
|
||||
|
||||
async def evaluate_with_claude(image_request: ImageRequestFile, prompt: str) -> StandardImageAnalysisResult:
|
||||
"""
|
||||
Función de adaptador para evaluar imágenes usando Claude.
|
||||
"""
|
||||
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
|
||||
image_bytes = await image_request.file.read()
|
||||
base64_image = encode_image_from_bytes(image_bytes)
|
||||
|
||||
media_type = image_request.file.content_type
|
||||
|
||||
if media_type not in ["image/jpeg", "image/png", "image/gif", "image/webp"]:
|
||||
raise ValueError(f"Tipo de imagen no soportado por Anthropic: {media_type}")
|
||||
|
||||
try:
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": media_type,
|
||||
"data": base64_image
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": prompt}
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
response = client.messages.create(
|
||||
model=image_request.model,
|
||||
max_tokens=1024,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
json_string = response.content[0].text
|
||||
parsed_data = json.loads(json_string)
|
||||
|
||||
return StandardImageAnalysisResult(
|
||||
status="success",
|
||||
original_filename=image_request.file.filename,
|
||||
provider_used="Claude",
|
||||
model_used=image_request.model,
|
||||
**parsed_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error evaluando la imagen: {str(e)}"
|
||||
)
|
||||
57
app/services/image/prompt_builder.py
Normal file
57
app/services/image/prompt_builder.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Módulo encargado de construir el prompt estandarizado para la evaluación de imágenes,
|
||||
inyectando la rúbrica, el contexto de especialidad y el esquema JSON esperado.
|
||||
Este módulo es crucial para asegurar que los evaluadores expertos reciban instrucciones
|
||||
claras y consistentes, y que sus respuestas se ajusten al formato requerido para su posterior
|
||||
procesamiento y análisis.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
from app.schemas.image_standard import StandardImageAnalysis, ImageEvaluationRubric
|
||||
|
||||
def build_image_evaluation_prompt(rubric: ImageEvaluationRubric) -> str:
|
||||
"""
|
||||
Construye el prompt estandarizado inyectando la rúbrica,
|
||||
el contexto de especialidad y el esquema JSON esperado.
|
||||
"""
|
||||
|
||||
# 1. Extraemos el JSON de la rúbrica de forma limpia (ignorando nulos)
|
||||
rubric_json = rubric.model_dump_json(exclude_none=True, indent=2)
|
||||
|
||||
# 2. Extraemos el esquema dinámico de salida basado en Pydantic
|
||||
expected_output_schema = json.dumps(StandardImageAnalysis.model_json_schema(), indent=2)
|
||||
|
||||
# 3. Obtenemos el path de especialización
|
||||
# Si por alguna razón viene vacío, le damos un rol genérico por defecto
|
||||
specialization_path = rubric.category if rubric.category else "General Image Analysis"
|
||||
|
||||
# 4. Ensamblamos el prompt con f-strings
|
||||
prompt = f"""
|
||||
# ROLE
|
||||
You are a highly specialized Expert Evaluator with deep domain expertise in the following area: **{specialization_path}**. Your objective is to perform a highly accurate, objective, and domain-calibrated analysis of the provided input based STRICTLY on your specialization and the provided evaluation rubric.
|
||||
|
||||
# TASK
|
||||
I will provide you with an input and a JSON object representing an `EvaluationRubric`.
|
||||
Your task is to analyze the input and score it against every criterion and subcriterion defined in the rubric. You must justify your scores with objective feedback based on evidence.
|
||||
|
||||
# DOMAIN CONTEXT
|
||||
Specialization Path: {specialization_path}
|
||||
|
||||
# RUBRIC
|
||||
{rubric_json}
|
||||
|
||||
# EVALUATION RULES
|
||||
1. **Domain Calibration (CRITICAL):** Calibrate your expectations, strictness, and feedback entirely according to the **{specialization_path}** context. Do not evaluate using generalized standards; apply the specific standards expected at this exact level and category.
|
||||
2. **Strict Adherence:** Evaluate ONLY the criteria and subcriteria listed in the rubric. Do not invent new metrics.
|
||||
3. **Scoring:** Assign a numeric `score` to each criterion and subcriterion. The score must reflect how well the input meets the description of that specific metric.
|
||||
4. **Objective Feedback:** Generate constructive, evidence-grounded `feedback` for the overall evaluation. Mention specific elements or patterns observed in the input that justify the scores within the context of the specialization.
|
||||
5. **Subcriteria Handling:** If a criterion has `subcriteria`, evaluate each subcriterion individually. The parent criterion's score should be a logical aggregate (e.g., average) of its subcriteria scores.
|
||||
|
||||
# OUTPUT FORMAT
|
||||
You MUST return your response EXCLUSIVELY as a raw, valid JSON object that strictly adheres to the following JSON Schema definition. Do NOT include markdown blocks (```json), explanations, or any text outside the JSON object.
|
||||
|
||||
# EXPECTED JSON SCHEMA
|
||||
{expected_output_schema}
|
||||
"""
|
||||
return prompt
|
||||
Binary file not shown.
Binary file not shown.
@@ -27,7 +27,8 @@ async def evaluate_text_with_provider(text_request: TextRequestFile) -> Standard
|
||||
match provider:
|
||||
case "openai":
|
||||
return await evaluate_with_openai(text_request)
|
||||
# AGREGAR OTROS CASOS PARA DIFERENTES PROVEEDORES DE IA AQUÍ
|
||||
case "claude":
|
||||
return await evaluate_with_claude(text_request)
|
||||
case _:
|
||||
raise ValueError(f"Proveedor de IA no soportado: {text_request.provider}")
|
||||
|
||||
@@ -47,4 +48,18 @@ async def evaluate_with_openai(text_request: TextRequestFile) -> StandardTextAna
|
||||
# 5. Manejar errores y excepciones adecuadamente
|
||||
raise NotImplementedError("La función evaluate_with_openai aún no está implementada.")
|
||||
|
||||
# Otros modelos de IA
|
||||
# Función de adaptador para evaluar texto usando Claude
|
||||
async def evaluate_with_claude(text_request: TextRequestFile) -> StandardTextAnalysisResult:
|
||||
"""
|
||||
Función de adaptador para evaluar texto usando Claude.
|
||||
(Plantilla para futuras implementaciones)
|
||||
"""
|
||||
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
||||
|
||||
# PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE EVALUACIÓN CON CLAUDE:
|
||||
# 1. Validar el texto de entrada (tamaño, formato, etc.)
|
||||
# 2. Configurar el cliente de Claude con la clave API
|
||||
# 3. Llamar a la API de Claude para evaluar el texto
|
||||
# 4. Convertir la respuesta de Claude al formato estándar de evaluación de texto de Qualidot
|
||||
# 5. Manejar errores y excepciones adecuadamente
|
||||
raise NotImplementedError("La función evaluate_with_claude aún no está implementada.")
|
||||
Binary file not shown.
@@ -25,43 +25,40 @@ async def transcribe_video_with_provider(video_request: VideoRequestFile) -> Sta
|
||||
provider = video_request.provider.lower()
|
||||
|
||||
match provider:
|
||||
case "nombre de la ia 1 aqui":
|
||||
return await transcribe_with_ai_model1(video_request)
|
||||
case "nombre de la ia 2 aqui":
|
||||
return await transcribe_with_ai_model2(video_request)
|
||||
|
||||
# AGREGAR OTROS CASOS PARA DIFERENTES PROVEEDORES DE IA AQUÍ
|
||||
|
||||
case "gemini":
|
||||
return await transcribe_with_gemini(video_request)
|
||||
case "twelvelabs":
|
||||
return await transcribe_with_twelvelabs(video_request)
|
||||
case _:
|
||||
raise ValueError(f"Proveedor de IA no soportado: {video_request.provider}")
|
||||
|
||||
# Función de adaptador para transcribir video usando OpenAI
|
||||
async def transcribe_with_ai_model1(video_request: VideoRequestFile) -> StandardTranscriptionResult:
|
||||
# Función de adaptador para transcribir video usando Gemini
|
||||
async def transcribe_with_gemini(video_request: VideoRequestFile) -> StandardTranscriptionResult:
|
||||
"""
|
||||
Función de adaptador para transcribir video usando OpenAI.
|
||||
Función de adaptador para transcribir video usando Gemini.
|
||||
(Plantilla para futuras implementaciones)
|
||||
"""
|
||||
|
||||
# PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE TRANSCRIPCIÓN CON IA (ELEGIR MODELO):
|
||||
# 1. Validar el video de entrada (tamaño, formato, etc.)
|
||||
# 2. Configurar el cliente de OpenAI con la clave API
|
||||
# 3. Llamar a la API de OpenAI para transcribir el video
|
||||
# 4. Convertir la respuesta de OpenAI al formato estándar de resumen de texto de Qualidot
|
||||
# 2. Configurar el cliente de Gemini con la clave API
|
||||
# 3. Llamar a la API de Gemini para transcribir el video
|
||||
# 4. Convertir la respuesta de Gemini al formato estándar de resumen de texto de Qualidot
|
||||
# 5. Manejar errores y excepciones adecuadamente
|
||||
raise NotImplementedError("La función transcribe_with_ai_model1 aún no está implementada.")
|
||||
raise NotImplementedError("La función transcribe_with_gemini aún no está implementada.")
|
||||
|
||||
async def transcribe_with_ai_model2(video_request: VideoRequestFile) -> StandardTranscriptionResult:
|
||||
async def transcribe_with_twelvelabs(video_request: VideoRequestFile) -> StandardTranscriptionResult:
|
||||
"""
|
||||
Función de adaptador para transcribir video usando OpenAI.
|
||||
Función de adaptador para transcribir video usando TwelveLabs.
|
||||
(Plantilla para futuras implementaciones)
|
||||
"""
|
||||
|
||||
# PASOS A SEGUIR PARA IMPLEMENTAR LA LÓGICA DE TRANSCRIPCIÓN CON IA (ELEGIR MODELO):
|
||||
# 1. Validar el video de entrada (tamaño, formato, etc.)
|
||||
# 2. Configurar el cliente de OpenAI con la clave API
|
||||
# 3. Llamar a la API de OpenAI para transcribir el video
|
||||
# 4. Convertir la respuesta de OpenAI al formato estándar de resumen de texto de Qualidot
|
||||
# 2. Configurar el cliente de TwelveLabs con la clave API
|
||||
# 3. Llamar a la API de TwelveLabs para transcribir el video
|
||||
# 4. Convertir la respuesta de TwelveLabs al formato estándar de resumen de texto de Qualidot
|
||||
# 5. Manejar errores y excepciones adecuadamente
|
||||
raise NotImplementedError("La función transcribe_with_ai_model2 aún no está implementada.")
|
||||
raise NotImplementedError("La función transcribe_with_twelvelabs aún no está implementada.")
|
||||
|
||||
# Otros modelos de IA
|
||||
161
app/tests/id.json
Normal file
161
app/tests/id.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"rubric": {
|
||||
"id": "f5a9b2c1-8d3e-4a7f-b12c-9d8e7f6a5b4c",
|
||||
"name": "Official ID Verification Rubric",
|
||||
"description": "Rúbrica para evaluar la calidad, legibilidad y validez visual de fotografías de documentos de identificación oficial (ej. INE, Pasaporte o Licencia).",
|
||||
"user_id": "112b3fda-e380-4919-9f9d-ff941a3b1938",
|
||||
"status": true,
|
||||
"visibility": "private",
|
||||
"verified": true
|
||||
},
|
||||
"category": {
|
||||
"id": 10,
|
||||
"name": "National IDs",
|
||||
"status": true,
|
||||
"hierarchy": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Identity Verification",
|
||||
"level": 0,
|
||||
"parent_id": null
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Document Analysis",
|
||||
"level": 1,
|
||||
"parent_id": 1
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Official IDs",
|
||||
"level": 2,
|
||||
"parent_id": 4
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "National IDs",
|
||||
"level": 3,
|
||||
"parent_id": 7
|
||||
}
|
||||
]
|
||||
},
|
||||
"criteria": [
|
||||
{
|
||||
"id": 100,
|
||||
"name": "Image Quality",
|
||||
"description": "Evalúa la calidad general de la captura fotográfica del documento.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": null,
|
||||
"sub_criteria": [
|
||||
{
|
||||
"id": 101,
|
||||
"name": "Sharpness & Focus",
|
||||
"description": "La imagen está perfectamente enfocada, sin desenfoque de movimiento, permitiendo ver los microtextos.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 100
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "Lighting & Glare",
|
||||
"description": "Iluminación uniforme. No hay reflejos del flash (glare) ni sombras densas que oculten el rostro o información vital.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 100
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"name": "Framing & Completeness",
|
||||
"description": "Los cuatro bordes del documento son claramente visibles. La identificación no está cortada ni parcialmente fuera de la foto.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 200,
|
||||
"name": "Text Readability",
|
||||
"description": "Evalúa si los datos impresos en el documento son legibles para su extracción.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": null,
|
||||
"sub_criteria": [
|
||||
{
|
||||
"id": 201,
|
||||
"name": "Personal Data",
|
||||
"description": "El nombre completo, fecha de nacimiento y dirección (si aplica) son 100% legibles.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 200
|
||||
},
|
||||
{
|
||||
"id": 202,
|
||||
"name": "Document Identifiers",
|
||||
"description": "El número de documento, folio o código MRZ (zona de lectura mecánica) son claramente distinguibles.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 200
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 300,
|
||||
"name": "Security & Physical Integrity",
|
||||
"description": "Verificación visual de la integridad física del plástico o papel.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": null,
|
||||
"sub_criteria": [
|
||||
{
|
||||
"id": 301,
|
||||
"name": "Face Photograph Clarity",
|
||||
"description": "La fotografía del rostro impresa en el documento es clara, tiene buen contraste y no presenta alteraciones.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 300
|
||||
},
|
||||
{
|
||||
"id": 302,
|
||||
"name": "No Tampering Signs",
|
||||
"description": "El documento no presenta roturas, tachaduras, dobleces extremos o signos visuales de manipulación digital/física.",
|
||||
"type_criteria": "primary",
|
||||
"type_value": "numeric",
|
||||
"value": "10",
|
||||
"status": true,
|
||||
"criteria_id": 300
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "KYC Best Practices for ID Verification",
|
||||
"url": "https://example.com/kyc-standards",
|
||||
"reference_type": "internal_policy",
|
||||
"is_primary": true,
|
||||
"description": "Estándares internos de Qualidot para el proceso de Conoce a tu Cliente (KYC) y prevención de fraude.",
|
||||
"status": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
BIN
app/utilities/__pycache__/image_utilities.cpython-312.pyc
Normal file
BIN
app/utilities/__pycache__/image_utilities.cpython-312.pyc
Normal file
Binary file not shown.
52
app/utilities/image_utilities.py
Normal file
52
app/utilities/image_utilities.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Validadores comunes para los endpoints de imagen.
|
||||
"""
|
||||
import base64
|
||||
|
||||
from app.schemas.image_standard import ImageEvaluationRubric, ImageEvaluationCriteria
|
||||
|
||||
def json_to_rubric(json_data: dict) -> ImageEvaluationRubric:
|
||||
"""Convierte un diccionario JSON en un objeto ImageEvaluationRubric mapeando los campos correctos."""
|
||||
|
||||
def parse_criteria(criteria_list: list) -> list:
|
||||
parsed_criteria = []
|
||||
for item in criteria_list:
|
||||
# Convertir el valor a float si existe
|
||||
score_value = float(item["value"]) if item.get("value") is not None else None
|
||||
description = str(item["description"]) if item.get("description") is not None else None
|
||||
|
||||
# Procesar subcriterios recursivamente si existen
|
||||
sub_raw = item.get("sub_criteria")
|
||||
sub_parsed = parse_criteria(sub_raw) if sub_raw else None
|
||||
|
||||
parsed_criteria.append(ImageEvaluationCriteria(
|
||||
name=item["name"],
|
||||
description=description,
|
||||
score=score_value,
|
||||
subcriteria=sub_parsed
|
||||
))
|
||||
return parsed_criteria
|
||||
|
||||
# Jerarquía de la categoría
|
||||
category_info = json_data.get("category", {})
|
||||
hierarchy_list = category_info.get("hierarchy", [])
|
||||
|
||||
# Ordenamos por nivel para asegurar la lógica: Nivel 0 -> Nivel 1 -> Nivel 2
|
||||
sorted_hierarchy = sorted(hierarchy_list, key=lambda x: x.get("level", 0))
|
||||
|
||||
# Unimos los nombres con ' > '
|
||||
specialization_path = " > ".join([item["name"] for item in sorted_hierarchy]) if sorted_hierarchy else None
|
||||
|
||||
# Extraer la información principal de la rúbrica
|
||||
rubric_info = json_data.get("rubric", {})
|
||||
|
||||
return ImageEvaluationRubric(
|
||||
name=rubric_info.get("name", "Sin nombre"),
|
||||
description=str(rubric_info["description"]) if rubric_info.get("description") is not None else None,
|
||||
category=specialization_path,
|
||||
criteria=parse_criteria(json_data.get("criteria", []))
|
||||
)
|
||||
|
||||
def encode_image_from_bytes(image_bytes: bytes) -> str:
|
||||
"""Codifica imagen desde bytes a base64"""
|
||||
return base64.b64encode(image_bytes).decode('utf-8')
|
||||
@@ -25,4 +25,5 @@ numpy
|
||||
openai
|
||||
langchain
|
||||
langchain-openai
|
||||
assemblyai
|
||||
assemblyai
|
||||
anthropic
|
||||
Reference in New Issue
Block a user