Módulo de audio y avance imágenes (schemas y openAI) #1

Merged
ericorps merged 2 commits from Alan-Sanchez/ia-microservice:main into main 2026-03-31 13:33:36 -06:00
38 changed files with 601 additions and 92 deletions

Binary file not shown.

View File

@@ -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

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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")

View File

@@ -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

View File

@@ -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)}"
)

View 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

View File

@@ -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.")

View File

@@ -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
View 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
}
]
}

View 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')

View File

@@ -25,4 +25,5 @@ numpy
openai
langchain
langchain-openai
assemblyai
assemblyai
anthropic