totes-emosh / app /session.py
drdeception
feat: six-emotion replication challenge — totes-emosh EmotionMap build
0d27c43
Raw
History Blame Contribute Delete
2.71 kB
"""
File: session.py
Author: Dr. Gordon Wright
Description: In-memory session model for the six-emotion replication
challenge. The session is a fixed-shape dict keyed by the
six basic emotions (no neutral). Each slot is either None
(not yet attempted) or a Capture. Sessions live in a
gr.State and reset on page refresh.
License: MIT License
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, Dict, List
import numpy as np
from PIL import Image
# Six basic emotions for the replication challenge. Order is the
# canonical Ekman order; the UI grid will render in this order. Neutral
# is excluded — it isn't part of the replication ask.
BASIC_EMOTIONS: List[str] = [
"happy",
"sad",
"fear",
"disgust",
"anger",
"surprise",
]
# Maps the lowercase activity label onto the classifier's DICT_EMO key.
EMOTION_TO_CLASSIFIER: Dict[str, str] = {
"happy": "Happiness",
"sad": "Sadness",
"fear": "Fear",
"disgust": "Disgust",
"anger": "Anger",
"surprise": "Surprise",
}
@dataclass
class Capture:
"""One image-and-prediction event in a session.
`intended` records which of the six emotions the student was
*trying* to produce. The classifier's own answer lives in
`emotion_probs`; the two often disagree and that disagreement is
the lesson.
"""
intended: str
face: np.ndarray
emotion_probs: Dict[str, float]
heatmap: Optional[np.ndarray] = None
blendshapes: Optional[Dict[str, float]] = None
landmarks: Optional[list] = None
bbox: Optional[tuple] = None
image_size: Optional[tuple] = None
def top_emotion(self) -> tuple:
items = sorted(self.emotion_probs.items(), key=lambda x: -x[1])
return items[0]
def thumbnail(self) -> Image.Image:
return Image.fromarray(self.face)
def classifier_agrees(self) -> bool:
top, _ = self.top_emotion()
return top == EMOTION_TO_CLASSIFIER.get(self.intended)
def empty_session() -> Dict[str, Optional[Capture]]:
return {emo: None for emo in BASIC_EMOTIONS}
def session_status(state: Dict[str, Optional[Capture]]) -> str:
"""One-line summary of progress across the six slots."""
filled = sum(1 for c in state.values() if c is not None)
if filled == 0:
return f"0 / 6 emotions captured. Pick one from the list and submit your first attempt."
if filled < 6:
return f"{filled} / 6 emotions captured. Keep going."
agree = sum(1 for c in state.values() if c is not None and c.classifier_agrees())
return f"All 6 captured — classifier agreed on {agree} / 6."