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
    )