diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc index f3f153b..62f1043 100644 Binary files a/app/__pycache__/__init__.cpython-312.pyc and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index 37ab906..40141ef 100644 Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/api/v1/__pycache__/__init__.cpython-312.pyc b/app/api/v1/__pycache__/__init__.cpython-312.pyc index a9b3aad..ea854c9 100644 Binary files a/app/api/v1/__pycache__/__init__.cpython-312.pyc and b/app/api/v1/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ee3e21f Binary files /dev/null and b/app/api/v1/endpoints/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc b/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc index f270342..ad1858e 100644 Binary files a/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc and b/app/api/v1/endpoints/__pycache__/router.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/audio/__pycache__/__init__.cpython-312.pyc b/app/api/v1/endpoints/audio/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b289065 Binary files /dev/null and b/app/api/v1/endpoints/audio/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc b/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc index 695a164..8b36043 100644 Binary files a/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc and b/app/api/v1/endpoints/audio/__pycache__/transcription.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/image/__pycache__/rubricated_analysis.cpython-312.pyc b/app/api/v1/endpoints/image/__pycache__/rubricated_analysis.cpython-312.pyc index db6c217..67e4025 100644 Binary files a/app/api/v1/endpoints/image/__pycache__/rubricated_analysis.cpython-312.pyc and b/app/api/v1/endpoints/image/__pycache__/rubricated_analysis.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/image/rubricated_analysis.py b/app/api/v1/endpoints/image/rubricated_analysis.py index cafc0ab..8fe1d8c 100644 --- a/app/api/v1/endpoints/image/rubricated_analysis.py +++ b/app/api/v1/endpoints/image/rubricated_analysis.py @@ -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 diff --git a/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc b/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc index 2329166..fa6687b 100644 Binary files a/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc and b/app/api/v1/endpoints/texto/__pycache__/resume.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc b/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc index 8564fc5..f40e7e3 100644 Binary files a/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc and b/app/api/v1/endpoints/texto/__pycache__/rubricated_analysis.cpython-312.pyc differ diff --git a/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc b/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc index 1c008f0..129eba5 100644 Binary files a/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc and b/app/api/v1/endpoints/video/__pycache__/transcription.cpython-312.pyc differ diff --git a/app/core/__pycache__/config.cpython-312.pyc b/app/core/__pycache__/config.cpython-312.pyc index 19234a7..4a5b6fe 100644 Binary files a/app/core/__pycache__/config.cpython-312.pyc and b/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/app/core/config.py b/app/core/config.py index 1f7d4c1..b484c5c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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() \ No newline at end of file diff --git a/app/schemas/__pycache__/audio_standar.cpython-312.pyc b/app/schemas/__pycache__/audio_standar.cpython-312.pyc deleted file mode 100644 index 81a07ee..0000000 Binary files a/app/schemas/__pycache__/audio_standar.cpython-312.pyc and /dev/null differ diff --git a/app/schemas/__pycache__/audio_standard.cpython-312.pyc b/app/schemas/__pycache__/audio_standard.cpython-312.pyc index b95adda..f4b703f 100644 Binary files a/app/schemas/__pycache__/audio_standard.cpython-312.pyc and b/app/schemas/__pycache__/audio_standard.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/image_standard.cpython-312.pyc b/app/schemas/__pycache__/image_standard.cpython-312.pyc index ffcf4c3..d19fde4 100644 Binary files a/app/schemas/__pycache__/image_standard.cpython-312.pyc and b/app/schemas/__pycache__/image_standard.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/text_standard.cpython-312.pyc b/app/schemas/__pycache__/text_standard.cpython-312.pyc index f38f664..2a75a68 100644 Binary files a/app/schemas/__pycache__/text_standard.cpython-312.pyc and b/app/schemas/__pycache__/text_standard.cpython-312.pyc differ diff --git a/app/schemas/__pycache__/video_standard.cpython-312.pyc b/app/schemas/__pycache__/video_standard.cpython-312.pyc index b814004..47c3f1b 100644 Binary files a/app/schemas/__pycache__/video_standard.cpython-312.pyc and b/app/schemas/__pycache__/video_standard.cpython-312.pyc differ diff --git a/app/schemas/audio_standard.py b/app/schemas/audio_standard.py index 9e45a62..26c8299 100644 --- a/app/schemas/audio_standard.py +++ b/app/schemas/audio_standard.py @@ -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.""" diff --git a/app/schemas/image_standard.py b/app/schemas/image_standard.py index bfc529d..2a89045 100644 --- a/app/schemas/image_standard.py +++ b/app/schemas/image_standard.py @@ -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" - ) \ No newline at end of file + 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") + \ No newline at end of file diff --git a/app/services/__pycache__/transcription_adapters.cpython-312.pyc b/app/services/__pycache__/transcription_adapters.cpython-312.pyc deleted file mode 100644 index 85ad205..0000000 Binary files a/app/services/__pycache__/transcription_adapters.cpython-312.pyc and /dev/null differ diff --git a/app/services/audio/__pycache__/transcription_adapters.cpython-312.pyc b/app/services/audio/__pycache__/transcription_adapters.cpython-312.pyc index 999534b..9422417 100644 Binary files a/app/services/audio/__pycache__/transcription_adapters.cpython-312.pyc and b/app/services/audio/__pycache__/transcription_adapters.cpython-312.pyc differ diff --git a/app/services/audio/transcription_adapters.py b/app/services/audio/transcription_adapters.py index 1455a5d..873103b 100644 --- a/app/services/audio/transcription_adapters.py +++ b/app/services/audio/transcription_adapters.py @@ -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 \ No newline at end of file + +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 \ No newline at end of file diff --git a/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc b/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc index 2971a13..1572b1c 100644 Binary files a/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc and b/app/services/image/__pycache__/evaluations_adapters.cpython-312.pyc differ diff --git a/app/services/image/__pycache__/prompt_builder.cpython-312.pyc b/app/services/image/__pycache__/prompt_builder.cpython-312.pyc new file mode 100644 index 0000000..e2f87e0 Binary files /dev/null and b/app/services/image/__pycache__/prompt_builder.cpython-312.pyc differ diff --git a/app/services/image/evaluations_adapters.py b/app/services/image/evaluations_adapters.py index f9f286e..8abdcbf 100644 --- a/app/services/image/evaluations_adapters.py +++ b/app/services/image/evaluations_adapters.py @@ -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 \ No newline at end of file + # 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)}" + ) \ No newline at end of file diff --git a/app/services/image/prompt_builder.py b/app/services/image/prompt_builder.py new file mode 100644 index 0000000..cddac68 --- /dev/null +++ b/app/services/image/prompt_builder.py @@ -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 \ No newline at end of file diff --git a/app/services/text/__pycache__/evaluations_adapters.cpython-312.pyc b/app/services/text/__pycache__/evaluations_adapters.cpython-312.pyc index c4b6d91..f2550cf 100644 Binary files a/app/services/text/__pycache__/evaluations_adapters.cpython-312.pyc and b/app/services/text/__pycache__/evaluations_adapters.cpython-312.pyc differ diff --git a/app/services/text/__pycache__/resume_adapters.cpython-312.pyc b/app/services/text/__pycache__/resume_adapters.cpython-312.pyc index cbed22d..7e63f0a 100644 Binary files a/app/services/text/__pycache__/resume_adapters.cpython-312.pyc and b/app/services/text/__pycache__/resume_adapters.cpython-312.pyc differ diff --git a/app/services/text/evaluations_adapters.py b/app/services/text/evaluations_adapters.py index b136d8c..322e06e 100644 --- a/app/services/text/evaluations_adapters.py +++ b/app/services/text/evaluations_adapters.py @@ -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 \ No newline at end of file +# 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.") \ No newline at end of file diff --git a/app/services/video/__pycache__/transcription_adapters.cpython-312.pyc b/app/services/video/__pycache__/transcription_adapters.cpython-312.pyc index ca3e8be..5426b45 100644 Binary files a/app/services/video/__pycache__/transcription_adapters.cpython-312.pyc and b/app/services/video/__pycache__/transcription_adapters.cpython-312.pyc differ diff --git a/app/services/video/transcription_adapters.py b/app/services/video/transcription_adapters.py index aa8fb41..e68625d 100644 --- a/app/services/video/transcription_adapters.py +++ b/app/services/video/transcription_adapters.py @@ -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 \ No newline at end of file diff --git a/app/tests/id.json b/app/tests/id.json new file mode 100644 index 0000000..ef892ac --- /dev/null +++ b/app/tests/id.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/app/utilities/__pycache__/audio_utilities.cpython-312.pyc b/app/utilities/__pycache__/audio_utilities.cpython-312.pyc index fa521a5..557b859 100644 Binary files a/app/utilities/__pycache__/audio_utilities.cpython-312.pyc and b/app/utilities/__pycache__/audio_utilities.cpython-312.pyc differ diff --git a/app/utilities/__pycache__/image_utilities.cpython-312.pyc b/app/utilities/__pycache__/image_utilities.cpython-312.pyc new file mode 100644 index 0000000..1f994f8 Binary files /dev/null and b/app/utilities/__pycache__/image_utilities.cpython-312.pyc differ diff --git a/app/utilities/image_utilities.py b/app/utilities/image_utilities.py new file mode 100644 index 0000000..4a83aa2 --- /dev/null +++ b/app/utilities/image_utilities.py @@ -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') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ecc246d..e220608 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ numpy openai langchain langchain-openai -assemblyai \ No newline at end of file +assemblyai +anthropic \ No newline at end of file