Spaces:
Sleeping
Sleeping
File size: 6,582 Bytes
a162c90 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | # src/expon/presentation/interfaces/rest/controllers/analysis_controller.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional, Dict, Any, Union
import os
from uuid import UUID
from sqlalchemy.orm import Session
from src.expon.shared.infrastructure.dependencies import get_db
from src.expon.iam.infrastructure.authorization.sfs.auth_bearer import get_current_user
from src.expon.presentation.infrastructure.persistence.jpa.repositories.presentation_repository import PresentationRepository
from src.expon.presentation.infrastructure.services.storage.local_storage_service import LocalStorageService
# Servicios de dominio
from src.expon.presentation.domain.services.transcription_service import TranscriptionService
from src.expon.presentation.domain.services.multimodal_service import MultimodalService
router = APIRouter()
asr_service = TranscriptionService()
multimodal = MultimodalService() # ← usamos el modelo MULTIMODAL
HF_MODEL_ID = os.getenv("HF_MODEL_ID", "alexander1010/expon-emotions")
HF_REVISION = os.getenv("HF_REVISION", "multimodal")
def _env_float(name: str, default: float) -> float:
try:
v = os.getenv(name, "")
return float(v) if v not in (None, "", "None") else default
except Exception:
return default
MM_ALPHA = _env_float("MM_ALPHA", 0.10)
MM_TEMP_TEXT = _env_float("MM_TEMP_TEXT", 1.8)
# ========= Schemas =========
class AnalyzeRequest(BaseModel):
presentation_id: str
text: Optional[str] = None # opcional; si viene vacío, usamos ASR
class AnalyzeResponse(BaseModel):
presentation_id: str
transcript: Optional[str]
confidence: float
dominant_emotion: str
emotion_probabilities: Dict[str, float]
alpha: float
temp_text: float
provider: str
feedback: Dict[str, Any] # feedback va por endpoints dedicados
# ========= Helpers =========
def _extract_transcript(asr_result: Union[str, Dict[str, Any], None]) -> Optional[str]:
if asr_result is None:
return None
if isinstance(asr_result, str):
return asr_result
if isinstance(asr_result, dict):
for k in ("text", "transcript", "combined_text", "combined_transcript"):
val = asr_result.get(k)
if isinstance(val, str) and val.strip():
return val
result = asr_result.get("result")
if isinstance(result, dict):
for k in ("text", "transcript"):
val = result.get(k)
if isinstance(val, str) and val.strip():
return val
return None
def _resolve_audio_path(db: Session, user_id: str, presentation_id: str) -> str:
"""
Resuelve la ruta física del audio usando el MISMO repo y storage de /upload.
- En BD guardas 'filename'
- En storage (LocalStorageService.base_path) está el archivo real
"""
# Asegura UUID como en tus otros endpoints
try:
pid = UUID(presentation_id)
except Exception:
raise HTTPException(status_code=400, detail="presentation_id no es un UUID válido")
repo = PresentationRepository(db)
pres = repo.get_by_id_and_user(pid, user_id)
if pres is None:
raise HTTPException(status_code=404, detail="Presentación no encontrada")
filename = getattr(pres, "filename", None)
if not filename:
raise HTTPException(status_code=500, detail="La presentación no tiene filename almacenado")
storage = LocalStorageService() # usa el mismo base_path por defecto que en /upload (/tmp/storage/audio)
audio_path = storage.get_path(filename)
if not os.path.exists(audio_path):
print(f"[analyze] No existe el archivo en storage: {audio_path}")
raise HTTPException(status_code=500, detail="No se encontró el archivo de audio en el almacenamiento")
return audio_path
# ========= Endpoint =========
@router.post("/analyze", response_model=AnalyzeResponse)
async def analyze_presentation(
payload: AnalyzeRequest,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
JSON:
{
"presentation_id": "<uuid>",
"text": "opcional (si viene, salta ASR)"
}
"""
presentation_id = payload.presentation_id
text = (payload.text or "").strip()
if not text or text.lower() == "string":
text = None
# 1) Ruta del audio desde storage/BD
audio_path = _resolve_audio_path(db, user.id, presentation_id)
# 2) Texto: si no llega, transcribimos el audio (ASR)
if text:
transcript = text
confidence_asr = 1.0
else:
asr_result = asr_service.transcribe(audio_path) # puede devolver str o dict
transcript = _extract_transcript(asr_result)
if not transcript:
raise HTTPException(status_code=500, detail="No se pudo obtener la transcripción del audio.")
confidence_asr = 1.0
if isinstance(asr_result, dict) and "confidence" in asr_result:
try:
confidence_asr = float(asr_result["confidence"])
except Exception:
pass
# 3) Emociones con modelo MULTIMODAL (usa audio + texto opcional)
# Firma asumida: predict(audio_path, text_or_none)
emo = multimodal.predict(audio_path, transcript if transcript else None)
# La salida esperada del servicio:
dominant = emo["dominant_emotion"]
probs = emo["emotion_probabilities"]
alpha = float(emo.get("alpha", MM_ALPHA))
temp_text = float(emo.get("temp_text", MM_TEMP_TEXT))
provider = emo.get("provider", f"hf:{HF_MODEL_ID}@{HF_REVISION}")
# Confianza devuelta: score de la emoción dominante
confidence_dom = float(probs.get(dominant, 0.0))
# (Opcional) Persistir análisis en BD si quieres consultarlo luego sin recalcular:
# try:
# repo = PresentationRepository(db)
# repo.update_analysis(
# presentation_id=UUID(presentation_id),
# transcript=transcript,
# dominant_emotion=dominant,
# emotion_probabilities=probs,
# confidence=confidence_dom,
# )
# except Exception:
# pass # no romper respuesta si falla la persistencia
return AnalyzeResponse(
presentation_id=presentation_id,
transcript=transcript,
confidence=confidence_dom,
dominant_emotion=dominant,
emotion_probabilities=probs,
alpha=alpha,
temp_text=temp_text,
provider=provider,
feedback={}, # el feedback se genera con /feedback
)
|