Spaces:
Sleeping
Sleeping
| # 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 ========= | |
| 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 | |
| ) | |