parth036 commited on
Commit
7fc7afa
·
verified ·
1 Parent(s): f84f536

Upload folder using huggingface_hub

Browse files
.dockerignore CHANGED
@@ -13,5 +13,9 @@ fer2013.csv
13
  output10.wav
14
  output.txt
15
  install_log.txt
 
 
 
 
16
  *.whl
17
  .pytest_cache/
 
13
  output10.wav
14
  output.txt
15
  install_log.txt
16
+ torch_install.log
17
+ *.log
18
+ hf_cache/
19
+ .cache/
20
  *.whl
21
  .pytest_cache/
.gitignore CHANGED
@@ -17,6 +17,10 @@ dist/
17
  output.txt
18
  /output10.wav
19
  install_log.txt
 
 
 
 
20
 
21
  # Training artifacts / datasets (not committed)
22
  results/
 
17
  output.txt
18
  /output10.wav
19
  install_log.txt
20
+ torch_install.log
21
+ *.log
22
+ hf_cache/
23
+ .cache/
24
 
25
  # Training artifacts / datasets (not committed)
26
  results/
Dockerfile CHANGED
@@ -8,29 +8,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
  ENV PYTHONUNBUFFERED=1 \
11
- TF_CPP_MIN_LOG_LEVEL=3 \
12
- TF_USE_LEGACY_KERAS=1 \
13
- TF_ENABLE_ONEDNN_OPTS=0 \
14
- HF_HOME=/tmp/hf \
15
  PORT=7860
16
 
17
  WORKDIR /app
18
 
19
- # Install Python deps first for better layer caching.
 
20
  COPY requirements-modern.txt .
21
  RUN pip install --no-cache-dir -r requirements-modern.txt
22
 
23
- # App code + models.
24
- COPY . .
 
 
25
 
26
- # On Hugging Face Spaces, *.h5 are stored via Git-LFS and the build context may
27
- # contain LFS *pointers* (~130 bytes) instead of the real weights. If so, fetch
28
- # the real files from the public resolve URL. Local builds (real files present)
29
- # skip this automatically.
30
- ARG MODEL_BASE_URL=https://huggingface.co/spaces/parth036/affect-screening/resolve/main
31
- RUN python -c "import os,urllib.request; base='${MODEL_BASE_URL}'; [None if (os.path.exists(r) and os.path.getsize(r)>100000) else (print('fetching',r) or urllib.request.urlretrieve(base+'/'+r,r)) for r in ['models/fer.h5','models/voice_model.h5']]"
32
 
33
  EXPOSE 7860
34
 
35
- # Single worker (TF model is loaded lazily per process); threads handle concurrency.
36
- CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT:-7860} --workers 1 --threads 4 --timeout 180 app:app"]
 
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
  ENV PYTHONUNBUFFERED=1 \
11
+ HF_HOME=/app/hf_cache \
12
+ HF_HUB_DISABLE_TELEMETRY=1 \
13
+ OMP_NUM_THREADS=2 \
 
14
  PORT=7860
15
 
16
  WORKDIR /app
17
 
18
+ # CPU-only PyTorch first (small wheel), then the rest of the deps.
19
+ RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
20
  COPY requirements-modern.txt .
21
  RUN pip install --no-cache-dir -r requirements-modern.txt
22
 
23
+ # Pre-download the Transformer models into the image so first request is fast.
24
+ RUN python -c "from transformers import pipeline; \
25
+ pipeline('image-classification', model='trpakov/vit-face-expression'); \
26
+ pipeline('audio-classification', model='superb/wav2vec2-base-superb-er')"
27
 
28
+ # App code (models/ holds only the Haar cascade now).
29
+ COPY . .
 
 
 
 
30
 
31
  EXPOSE 7860
32
 
33
+ # One worker (models load lazily per process); threads handle light concurrency.
34
+ CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT:-7860} --workers 1 --threads 2 --timeout 300 app:app"]
anxiety_app/config.py CHANGED
@@ -1,7 +1,8 @@
1
- """Central configuration: filesystem paths and runtime settings.
2
 
3
- Paths are resolved relative to the project root so the app works regardless of
4
- the current working directory.
 
5
  """
6
  from pathlib import Path
7
 
@@ -9,29 +10,28 @@ from pathlib import Path
9
  BASE_DIR = Path(__file__).resolve().parent.parent
10
  MODELS_DIR = BASE_DIR / "models"
11
 
12
- # Model artifacts
13
- FACE_MODEL_JSON = MODELS_DIR / "fer.json"
14
- FACE_MODEL_WEIGHTS = MODELS_DIR / "fer.h5"
15
- HAAR_CASCADE = MODELS_DIR / "haarcascade_frontalface_default.xml"
16
- VOICE_MODEL_JSON = MODELS_DIR / "voice_model.json"
17
- VOICE_MODEL_WEIGHTS = MODELS_DIR / "voice_model.h5"
18
 
19
- # Where microphone recordings are written
20
- RECORDING_PATH = BASE_DIR / "output10.wav"
 
 
 
 
21
 
22
- # Audio recording parameters
23
- SAMPLE_RATE = 44100
24
- CHANNELS = 2
25
- CHUNK = 1024
26
- RECORD_SECONDS = 4
27
 
28
- # FER2013 emotion classes in the model's native (Kaggle) order. The app maps
29
- # "fear" -> Anxiety and "sad" -> Depressed as supplementary affective signals
30
- # (NOT a clinical diagnosis).
31
- FACE_EMOTIONS = ("angry", "disgust", "fear", "happy", "sad", "surprise", "neutral")
32
 
33
- # Voice model output classes (gender + emotion).
34
- VOICE_CLASSES = (
35
- "Male Angry", "Male Calm", "Male Anxious", "Male Happy", "Male Depressed",
36
- "Female Angry", "Female Calm", "Female Anxious", "Female Happy", "Female Depressed",
37
- )
 
 
1
+ """Central configuration: model identifiers, paths, and runtime settings.
2
 
3
+ Inference uses pretrained Transformer models from the Hugging Face Hub:
4
+ - Face : a ViT fine-tuned for facial-expression recognition (FER2013 classes)
5
+ - Voice: a wav2vec2 model fine-tuned for speech-emotion recognition (RAVDESS)
6
  """
7
  from pathlib import Path
8
 
 
10
  BASE_DIR = Path(__file__).resolve().parent.parent
11
  MODELS_DIR = BASE_DIR / "models"
12
 
13
+ # Pretrained Hugging Face models
14
+ FACE_MODEL_ID = "trpakov/vit-face-expression"
15
+ # Official SUPERB emotion-recognition head (loads cleanly under current
16
+ # Transformers; community RAVDESS checkpoints ship a mismatched head).
17
+ VOICE_MODEL_ID = "superb/wav2vec2-base-superb-er"
 
18
 
19
+ # Pretty names for abbreviated emotion labels (SUPERB / IEMOCAP).
20
+ LABEL_PRETTY = {
21
+ "neu": "neutral", "hap": "happy", "ang": "angry", "sad": "sad",
22
+ "exc": "excited", "fru": "frustrated", "fea": "fear", "sur": "surprise",
23
+ "dis": "disgust",
24
+ }
25
 
26
+ # OpenCV face detector (used to crop faces before the ViT classifies them)
27
+ HAAR_CASCADE = MODELS_DIR / "haarcascade_frontalface_default.xml"
 
 
 
28
 
29
+ # Where uploaded/recorded audio is written
30
+ RECORDING_PATH = BASE_DIR / "output10.wav"
 
 
31
 
32
+ # Map raw emotion labels (from either model) to the app's affective signal.
33
+ # NOTE: a supplementary cue, NOT a clinical diagnosis.
34
+ SIGNAL_MAP = {
35
+ "fear": "Anxious", "fearful": "Anxious",
36
+ "sad": "Depressed", "sadness": "Depressed",
37
+ }
anxiety_app/routes.py CHANGED
@@ -63,7 +63,7 @@ def api_voice():
63
  if file is None:
64
  return jsonify(error="no audio uploaded"), 400
65
  try:
66
- return jsonify(label=voice_emotion.analyze_upload(file))
67
  except Exception as exc: # noqa: BLE001
68
  logger.exception("Voice analysis failed")
69
  return jsonify(error=str(exc)), 500
 
63
  if file is None:
64
  return jsonify(error="no audio uploaded"), 400
65
  try:
66
+ return jsonify(voice_emotion.analyze_upload(file)) # {emotion, signal}
67
  except Exception as exc: # noqa: BLE001
68
  logger.exception("Voice analysis failed")
69
  return jsonify(error=str(exc)), 500
anxiety_app/services/face_emotion.py CHANGED
@@ -1,14 +1,11 @@
1
- """Facial emotion recognition.
2
 
3
- Loads the FER2013 CNN once (lazily) and classifies each detected face. Emotions
4
- are tallied across a session; the dominant affective signal is returned as a
5
- supplementary cue (Anxious / Depressed / Normal) -- NOT a clinical diagnosis.
6
 
7
- Detection robustness: faces are found on a CLAHE contrast-equalized image (helps
8
- low light) using two Haar cascades -- the bundled frontal-face plus OpenCV's
9
- ``alt2`` cascade (better with glasses / slight angles). Boxes from both are
10
- merged. Classification still uses the raw crop with per-image standardization to
11
- match how the model was trained (fixes the original /255 train/serve skew).
12
  """
13
  import logging
14
 
@@ -18,34 +15,30 @@ from .. import config
18
 
19
  logger = logging.getLogger(__name__)
20
 
21
- # Heavy deps (tf_keras, cv2) are imported lazily so the package can be imported
22
- # (e.g. for fast route tests) without TensorFlow/OpenCV present.
23
- _model = None
24
  _cascades = None
25
  _clahe = None
26
 
27
- # Detection tuning
28
- _SCALE_FACTOR = 1.1 # finer pyramid -> catches more faces (glasses/angles)
29
  _MIN_NEIGHBORS = 5
30
  _MIN_SIZE = (60, 60)
31
 
32
 
33
  def _load():
34
- global _model, _cascades, _clahe
35
- if _model is None:
36
  import cv2
37
- from tf_keras.models import model_from_json
38
 
39
- logger.info("Loading face model from %s", config.FACE_MODEL_JSON)
40
- _model = model_from_json(config.FACE_MODEL_JSON.read_text())
41
- _model.load_weights(str(config.FACE_MODEL_WEIGHTS))
42
-
43
- # Our shipped cascade + OpenCV's bundled alt2 (more robust to glasses).
44
  paths = [str(config.HAAR_CASCADE),
45
  cv2.data.haarcascades + "haarcascade_frontalface_alt2.xml"]
46
  _cascades = [c for c in (cv2.CascadeClassifier(p) for p in paths) if not c.empty()]
47
  _clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
48
- return _model
49
 
50
 
51
  def _iou(a, b):
@@ -55,20 +48,17 @@ def _iou(a, b):
55
  iw = max(0, min(ax + aw, bx + bw) - ix)
56
  ih = max(0, min(ay + ah, by + bh) - iy)
57
  inter = iw * ih
58
- if inter == 0:
59
- return 0.0
60
- return inter / float(aw * ah + bw * bh - inter)
61
 
62
 
63
  def _detect(gray):
64
- """Return de-duplicated face boxes from CLAHE-equalized grayscale."""
65
  eq = _clahe.apply(gray)
66
  raw = []
67
  for cascade in _cascades:
68
  for box in cascade.detectMultiScale(eq, scaleFactor=_SCALE_FACTOR,
69
  minNeighbors=_MIN_NEIGHBORS, minSize=_MIN_SIZE):
70
  raw.append(tuple(int(v) for v in box))
71
- # Keep the largest boxes first; drop ones that overlap an already-kept box.
72
  raw.sort(key=lambda b: b[2] * b[3], reverse=True)
73
  kept = []
74
  for box in raw:
@@ -77,32 +67,21 @@ def _detect(gray):
77
  return kept
78
 
79
 
80
- def preprocess(face_gray):
81
- """48x48 grayscale -> standardized (1, 48, 48, 1) batch."""
82
- import cv2
83
-
84
- face = cv2.resize(face_gray, (48, 48)).astype("float32")
85
- face = (face - face.mean()) / (face.std() + 1e-7) # per-image standardization
86
- return face.reshape(1, 48, 48, 1)
87
-
88
-
89
- def classify_face(face_gray):
90
- """Return the predicted FER2013 emotion label for one grayscale face crop."""
91
- model = _load()
92
- preds = model.predict(preprocess(face_gray), verbose=0)
93
- return config.FACE_EMOTIONS[int(np.argmax(preds[0]))]
94
-
95
 
96
- # Map raw emotions to the app's affective signal (NOT a diagnosis).
97
- _SIGNAL = {"fear": "Anxious", "sad": "Depressed"}
98
 
 
 
 
99
 
100
- def emotion_to_signal(emotion):
101
- return _SIGNAL.get(emotion, "Normal")
 
102
 
103
 
104
  def classify_frame(image_bytes):
105
- """Detect faces in an encoded image (e.g. a browser JPEG) and classify each.
106
 
107
  Returns a list of {"box": [x, y, w, h], "emotion": str, "signal": str}.
108
  """
@@ -114,9 +93,10 @@ def classify_frame(image_bytes):
114
  if img is None:
115
  return []
116
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
 
117
  results = []
118
  for (x, y, w, h) in _detect(gray):
119
- emotion = classify_face(gray[y:y + h, x:x + w])
120
  results.append({
121
  "box": [x, y, w, h],
122
  "emotion": emotion,
@@ -127,47 +107,7 @@ def classify_frame(image_bytes):
127
 
128
  def _verdict(counts):
129
  """Map tallied emotions to a dominant affective signal."""
130
- signal = {
131
- "Anxious": counts.get("fear", 0),
132
- "Depressed": counts.get("sad", 0),
133
- "Normal": sum(v for k, v in counts.items() if k not in ("fear", "sad")),
134
- }
135
  return max(signal, key=signal.get)
136
-
137
-
138
- def detect_emotion(camera_index=0):
139
- """Open the webcam, classify faces until 'q' is pressed, return the verdict.
140
-
141
- NOTE: opens a native OpenCV window; requires a local display + webcam.
142
- """
143
- import cv2
144
-
145
- _load()
146
- cap = cv2.VideoCapture(camera_index)
147
- if not cap.isOpened():
148
- raise RuntimeError("Could not open the webcam (camera index %s)." % camera_index)
149
-
150
- counts = {}
151
- try:
152
- while True:
153
- ok, frame = cap.read()
154
- if not ok:
155
- continue
156
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
157
- for (x, y, w, h) in _detect(gray):
158
- cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 0, 0), 3)
159
- emotion = classify_face(gray[y:y + h, x:x + w])
160
- counts[emotion] = counts.get(emotion, 0) + 1
161
- cv2.putText(frame, emotion, (x, y), cv2.FONT_HERSHEY_SIMPLEX,
162
- 1, (0, 0, 255), 2)
163
- cv2.imshow("Facial emotion analysis", cv2.resize(frame, (1000, 700)))
164
- cv2.setWindowProperty("Facial emotion analysis", cv2.WND_PROP_TOPMOST, 1)
165
- if cv2.waitKey(10) == ord("q"):
166
- break
167
- finally:
168
- cap.release()
169
- cv2.destroyAllWindows()
170
-
171
- verdict = _verdict(counts)
172
- logger.info("Face session counts=%s -> %s", counts, verdict)
173
- return verdict
 
1
+ """Facial emotion recognition with a pretrained Vision Transformer.
2
 
3
+ Pipeline: detect faces with OpenCV (CLAHE-equalized for low light, two Haar
4
+ cascades for glasses/angles), crop each face, and classify the expression with a
5
+ ViT fine-tuned on FER2013 (``trpakov/vit-face-expression``).
6
 
7
+ The dominant affective signal across a session (Anxious / Depressed / Normal) is
8
+ a supplementary cue -- NOT a clinical diagnosis.
 
 
 
9
  """
10
  import logging
11
 
 
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
+ # Heavy deps (torch/transformers/cv2) are imported lazily so the package can be
19
+ # imported (e.g. for fast route tests) without them present.
20
+ _pipe = None
21
  _cascades = None
22
  _clahe = None
23
 
24
+ _SCALE_FACTOR = 1.1
 
25
  _MIN_NEIGHBORS = 5
26
  _MIN_SIZE = (60, 60)
27
 
28
 
29
  def _load():
30
+ global _pipe, _cascades, _clahe
31
+ if _pipe is None:
32
  import cv2
33
+ from transformers import pipeline
34
 
35
+ logger.info("Loading face model %s", config.FACE_MODEL_ID)
36
+ _pipe = pipeline("image-classification", model=config.FACE_MODEL_ID, top_k=1)
 
 
 
37
  paths = [str(config.HAAR_CASCADE),
38
  cv2.data.haarcascades + "haarcascade_frontalface_alt2.xml"]
39
  _cascades = [c for c in (cv2.CascadeClassifier(p) for p in paths) if not c.empty()]
40
  _clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
41
+ return _pipe
42
 
43
 
44
  def _iou(a, b):
 
48
  iw = max(0, min(ax + aw, bx + bw) - ix)
49
  ih = max(0, min(ay + ah, by + bh) - iy)
50
  inter = iw * ih
51
+ return inter / float(aw * ah + bw * bh - inter) if inter else 0.0
 
 
52
 
53
 
54
  def _detect(gray):
55
+ """Return de-duplicated face boxes from a CLAHE-equalized grayscale image."""
56
  eq = _clahe.apply(gray)
57
  raw = []
58
  for cascade in _cascades:
59
  for box in cascade.detectMultiScale(eq, scaleFactor=_SCALE_FACTOR,
60
  minNeighbors=_MIN_NEIGHBORS, minSize=_MIN_SIZE):
61
  raw.append(tuple(int(v) for v in box))
 
62
  raw.sort(key=lambda b: b[2] * b[3], reverse=True)
63
  kept = []
64
  for box in raw:
 
67
  return kept
68
 
69
 
70
+ def emotion_to_signal(emotion):
71
+ return config.SIGNAL_MAP.get(emotion.lower(), "Normal")
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
 
 
73
 
74
+ def classify_face(face_rgb):
75
+ """Classify one RGB face crop (numpy array). Returns the emotion label."""
76
+ from PIL import Image
77
 
78
+ pipe = _load()
79
+ pred = pipe(Image.fromarray(face_rgb))[0]
80
+ return pred["label"].lower()
81
 
82
 
83
  def classify_frame(image_bytes):
84
+ """Detect faces in an encoded image and classify each.
85
 
86
  Returns a list of {"box": [x, y, w, h], "emotion": str, "signal": str}.
87
  """
 
93
  if img is None:
94
  return []
95
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
96
+ rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
97
  results = []
98
  for (x, y, w, h) in _detect(gray):
99
+ emotion = classify_face(rgb[y:y + h, x:x + w])
100
  results.append({
101
  "box": [x, y, w, h],
102
  "emotion": emotion,
 
107
 
108
  def _verdict(counts):
109
  """Map tallied emotions to a dominant affective signal."""
110
+ signal = {"Anxious": 0, "Depressed": 0, "Normal": 0}
111
+ for emotion, n in counts.items():
112
+ signal[emotion_to_signal(emotion)] += n
 
 
113
  return max(signal, key=signal.get)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
anxiety_app/services/voice_emotion.py CHANGED
@@ -1,57 +1,48 @@
1
- """Speech emotion recognition from a recorded WAV file.
2
 
3
- Loads the RAVDESS-trained 1-D CNN once (lazily) and predicts one of 10
4
- gender x emotion classes from MFCC features. Gender is part of this single
5
- model -- there is no separate gender network.
6
  """
7
  import logging
8
 
9
- import numpy as np
10
-
11
  from .. import config
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
- _model = None
 
16
 
17
 
18
  def _load():
19
- global _model
20
- if _model is None:
21
- from tf_keras.models import model_from_json
22
 
23
- logger.info("Loading voice model from %s", config.VOICE_MODEL_JSON)
24
- _model = model_from_json(config.VOICE_MODEL_JSON.read_text())
25
- _model.load_weights(str(config.VOICE_MODEL_WEIGHTS))
26
- return _model
27
 
28
 
29
- def extract_features(wav_path):
30
- """Return the mean MFCC feature vector the model expects."""
31
  import librosa
32
 
33
- X, sample_rate = librosa.load(str(wav_path), res_type="kaiser_fast",
34
- duration=2.5, sr=22050 * 2, offset=0.5)
35
- mfccs = np.mean(librosa.feature.mfcc(y=X, sr=sample_rate, n_mfcc=13), axis=0)
36
- return np.expand_dims(np.expand_dims(mfccs, axis=0), axis=2)
37
-
38
-
39
- def analyze(wav_path=None):
40
- """Classify a recording and return its gender+emotion label."""
41
- wav_path = wav_path or config.RECORDING_PATH
42
- model = _load()
43
- preds = model.predict(extract_features(wav_path), batch_size=32, verbose=0)
44
- label = config.VOICE_CLASSES[int(preds.argmax(axis=1)[0])]
45
- logger.info("Voice prediction for %s -> %s", wav_path, label)
46
- return label
47
 
48
 
49
  def analyze_upload(file_storage):
50
- """Classify an uploaded audio blob (e.g. browser MediaRecorder webm/wav).
51
-
52
- librosa decodes via soundfile (wav) or audioread/ffmpeg (webm/ogg), so the
53
- container ships ffmpeg. Returns the gender+emotion label.
54
- """
55
  import os
56
  import tempfile
57
 
 
1
+ """Speech emotion recognition with a pretrained wav2vec2 model.
2
 
3
+ Uses ``ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition`` (fine-tuned on
4
+ RAVDESS) to predict the emotion of a short clip. Returns the top emotion plus the
5
+ app's affective signal (a supplementary cue, NOT a diagnosis).
6
  """
7
  import logging
8
 
 
 
9
  from .. import config
10
 
11
  logger = logging.getLogger(__name__)
12
 
13
+ _pipe = None
14
+ TARGET_SR = 16000
15
 
16
 
17
  def _load():
18
+ global _pipe
19
+ if _pipe is None:
20
+ from transformers import pipeline
21
 
22
+ logger.info("Loading voice model %s", config.VOICE_MODEL_ID)
23
+ _pipe = pipeline("audio-classification", model=config.VOICE_MODEL_ID, top_k=1)
24
+ return _pipe
 
25
 
26
 
27
+ def analyze(wav_path=None):
28
+ """Classify an audio file and return {'emotion', 'signal'}."""
29
  import librosa
30
 
31
+ wav_path = str(wav_path or config.RECORDING_PATH)
32
+ pipe = _load()
33
+ # Decode + resample to 16 kHz mono ourselves so we don't depend on the
34
+ # pipeline's audio backend (works for wav locally and webm in the container).
35
+ audio, _ = librosa.load(wav_path, sr=TARGET_SR, mono=True)
36
+ pred = pipe({"raw": audio, "sampling_rate": TARGET_SR})[0]
37
+ raw = pred["label"].lower()
38
+ emotion = config.LABEL_PRETTY.get(raw, raw)
39
+ result = {"emotion": emotion, "signal": config.SIGNAL_MAP.get(emotion, "Normal")}
40
+ logger.info("Voice prediction for %s -> %s", wav_path, result)
41
+ return result
 
 
 
42
 
43
 
44
  def analyze_upload(file_storage):
45
+ """Classify an uploaded audio blob (browser webm/wav). Returns {'emotion','signal'}."""
 
 
 
 
46
  import os
47
  import tempfile
48
 
anxiety_app/static/voice.js CHANGED
@@ -32,8 +32,12 @@
32
  try {
33
  const res = await fetch("/api/voice", { method: "POST", body: fd });
34
  const data = await res.json();
35
- resultEl.textContent = data.label ? "Result: " + data.label
36
- : "Error: " + (data.error || "unknown");
 
 
 
 
37
  statusEl.textContent = "Done.";
38
  } catch (e) {
39
  statusEl.textContent = "Request failed: " + e.message;
 
32
  try {
33
  const res = await fetch("/api/voice", { method: "POST", body: fd });
34
  const data = await res.json();
35
+ if (data.emotion) {
36
+ const emo = data.emotion.charAt(0).toUpperCase() + data.emotion.slice(1);
37
+ resultEl.textContent = "Result: " + emo + " (" + data.signal + ")";
38
+ } else {
39
+ resultEl.textContent = "Error: " + (data.error || "unknown");
40
+ }
41
  statusEl.textContent = "Done.";
42
  } catch (e) {
43
  statusEl.textContent = "Request failed: " + e.message;
requirements-modern.txt CHANGED
@@ -1,22 +1,25 @@
1
- # Modernized dependency set (Option C) — Python 3.13
2
- # Uses current TensorFlow + the legacy `tf-keras` package so the original
3
- # Keras 2.0.6 model files (fer.json/h5, model.json + .h5) load unchanged.
 
4
  #
5
- # Setup:
6
- # py -3.13 -m venv venv
7
- # venv\Scripts\activate
 
 
8
  # pip install -r requirements-modern.txt
9
- # python app.py # or: flask run
10
  #
11
  # Capture happens in the browser, so no PyAudio/system audio libs are required.
12
 
13
- tensorflow==2.21.0
14
- tf-keras==2.21.0
15
- opencv-python
16
  librosa
17
  resampy
18
- pandas
19
- scikit-learn
20
  soundfile
 
 
 
21
  flask
22
  gunicorn
 
1
+ # Runtime dependencies (Python 3.11/3.13).
2
+ # Inference uses pretrained Transformer models (PyTorch + Transformers):
3
+ # face : trpakov/vit-face-expression
4
+ # voice: ehcalabres/wav2vec2-lg-xlsr-en-speech-emotion-recognition
5
  #
6
+ # In Docker, CPU-only torch is installed first from the PyTorch CPU index
7
+ # (see Dockerfile) to keep the image small. On Windows the PyPI wheel is CPU.
8
+ #
9
+ # Setup (local):
10
+ # py -3.13 -m venv venv ; venv\Scripts\activate
11
  # pip install -r requirements-modern.txt
12
+ # python app.py
13
  #
14
  # Capture happens in the browser, so no PyAudio/system audio libs are required.
15
 
16
+ torch
17
+ transformers
 
18
  librosa
19
  resampy
 
 
20
  soundfile
21
+ opencv-python
22
+ pillow
23
+ numpy
24
  flask
25
  gunicorn