File size: 20,435 Bytes
0d27c43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# DEVLOG — totes-emosh

## 2026-06-07 chore: rename Fingerprint → EmotionMap; attribution footer; verdict bands

**Capability:** facial-expression-toy-app (static-only)
**Uses:****Approach:** Rename across current-facing materials at Gordon's
call — "Fingerprint" doesn't make sense in context, defined term is
now **EmotionMap**. Bulk replace in `app.py`, `app/tiles.py` (PDF
title + temp-file prefix), `README.md`, `.codewright.yaml`, plus all
lesson docs (`formative-activity.html`, `expert-insight.{html,md}`,
`key-takeaways.{html,md}`, `W3L4-deliverable.md`,
`teaching/.../README.md`). Word template script renamed
`scratch/build_emotionmap_template.py`; Word file regenerated as
`Lesson4-EmotionMap-template.docx`; old `.docx` deleted.

Also (earlier same day): added the standard attribution footer to
the app (`gr.HTML` block under the download button) and to the
single-page PDF (line below the privacy reminder) — *Created by Dr.
Gordon Wright — A LittleMonkeyLab caper. Part of the Goldsmiths MSc
in Psychology, Week 3 Part 4.* Verdict bands made far more visible:
full-width coloured strips (green AGREES / orange DISAGREES), white
bold 15pt title + 11pt detail, taller tile (TILE_H 396, VERDICT_H
44). Empty tiles get a matching grey "awaiting your attempt" band so
the grid stays aligned. Deleted dead `app/description.py` (no
longer imported anywhere).

Smoke tests: `app.py` imports clean, both PDF modes render against
the six bundled sample faces (`lesson4-emotionmap*-{face,wireframe}.pdf`),
the attribution line and EmotionMap title both stamp correctly.
Historical DEVLOG / `toy-app-plan.md` entries deliberately left
intact for the historical record.

---

## 2026-06-05 feat: six-emotion replication grid + wireframe (face-free) mode + single-page A4 PDF artefact

**Capability:** facial-expression-toy-app (static-only)
**Uses:** mediapipe (478-point face mesh), Pillow (compositing), matplotlib (PDF)
**Approach:** Reshaped the toy app around the *replication challenge*
framing. New `app/tiles.py` owns:

- A six-tile grid (2 rows × 3 cols) keyed by `BASIC_EMOTIONS = [happy,
  sad, fear, disgust, anger, surprise]`. Neutral dropped — it isn't
  part of the replication ask.
- A composite per-tile renderer (`render_tile`, `render_filled_tile`,
  `render_empty_tile`) that produces a single 280×370 PIL image
  containing a header strip (`1. HAPPY`), the face (or wireframe)
  thumbnail, a 7-emotion classifier bar strip, and a coloured verdict
  line (`agrees: Happiness (0.99)` / `sees: Fear (0.51) — not Anger`).
- `render_wireframe()` — anonymised landmark mesh, drawn from
  hand-coded subsets of MediaPipe FACEMESH connections (face oval,
  lips outer + inner, both eyes, both brows, nose bridge) plus dots
  for every one of the 478 landmarks. The result is recognisably
  face-shaped — you can read brows pulled down on the angry attempt,
  a smile on the happy attempt — without any identifying photographic
  content.
- `export_single_page_pdf()` — one A4 portrait page with a title row,
  name strip, 2×3 tile grid, status footer, and privacy reminder.
  Replaces the two-page radar / two-mode export from earlier today.

State model: `app/session.py` now exposes
`empty_session() -> {emotion: Capture|None}` over the six emotions and
a `Capture` dataclass with `intended`, `landmarks`, `bbox`, and
`image_size` (the last three are needed so the wireframe can be
re-drawn at the exact crop of the face). `session_status()` reports
"N / 6 captured · classifier agreed on M / 6".

UI rewrite in `app.py`: gone are the logo row, multi-line header,
two-paragraph privacy banner, separate Static / Dynamic / Fingerprint
tabs, gr.Gallery, and the radar plot. Replaced by:

- A one-line `<h>` header with a small `<details>` privacy disclosure.
- A single compact input row (image upload/webcam + intended-emotion
  dropdown + wireframe toggle + submit + clear-all).
- A six-tile grid where every tile re-renders on every action.
- Per-tile **Retry** buttons that clear just that slot.
- A name field + single **Download single-page Fingerprint PDF**
  button at the bottom.

Toggling Wireframe re-renders every visible tile in real time, so
students can preview either format before they download.

`app/fingerprint.py` and `scratch/smoke_fingerprint.py` deleted —
radar and two-PDF export are gone. `app/app_utils.preprocess_image_and_predict()`
now returns the bbox and landmarks alongside the face/heatmap/probs/
blendshapes so the tiles module can reconstruct the wireframe from a
stored Capture.

Word doc Fingerprint template regenerated as a paper fallback — a
2×3 replication grid table mirroring the in-app layout, plus the
Privacy and Reflection sections. Lesson copy updated:
`formative-activity.html` now frames the activity as a
six-emotion replication challenge with the wireframe-vs-face format
choice; `expert-insight.md` and `key-takeaways.md` rewritten around
the per-slot agree/disagree pattern.

**Related:** online-teaching-for-bence

Reframing note (same day, post-shipping): Gordon shared the original
lecture brief, which puts the activity at slides 23-24 as the
experiential capstone after a deception-focused arc (Spy the Lie /
Duping Delight → Ekman micro-expressions → TSA SPOT boondoggle →
Porter & ten Brinke 2008 on posed emotion → FACS survived, the
deception theory didn't). The six-tile replication grid lands as a
single-person Porter & ten Brinke replication: pose each emotion in
turn; happy will be easiest to fake; the classifier disagreement
pattern *is* the data. No app changes needed for that reframing —
the existing per-tile agree/disagree verdict already exposes the
Porter & ten Brinke effect. Lesson copy in
`online-teaching-for-bence` updated accordingly:
`expert-insight.md` rewritten in Gordon's voice around the deception
arc, `formative-activity.html` now leads with the two stated LOs,
`key-takeaways.md` rewritten to match.

Smoke tests: `scratch/smoke_tiles.py` builds synthetic state, renders
both face and wireframe tile grids, and exports both PDF modes.
`scratch/smoke_real_face.py` runs the six bundled sample images
through the real classifier + landmarker; all six produced a face,
landmarks (478 each), bbox, and a top-class read — five correct
(Happy 0.99, Sad 0.99, Fear 0.99, Disgust 0.99, Surprise 0.76),
Anger misread as Fear 0.51 (which is itself the variability story
the lesson is now built around). Sample tile renders at
`scratch/tile-samples/` confirm the face mode and wireframe mode
both communicate the pose cleanly.

`PYTHONUNBUFFERED=1 uv run python app.py` reaches Gradio's
"Running on local URL" within a few seconds, no warnings, no
deprecations.

---

## 2026-06-05 chore: stripped to static-only; basic-emotions scope; video moves to a separate app

**Capability:** facial-expression-toy-app (static-only)
**Uses:** gradio, torch, mediapipe (tasks API), grad-cam
**Approach:** Activity scope re-cut at Gordon's call: too many trials,
basic emotions in the static path are enough on their own. Dynamic
Faces tab removed from `app.py`. `submit_video_and_add`,
`clear_dynamic_info`, and all video-tab widgets gone. `LABEL_PRESETS`
trimmed from 17 entries (warm-up × 7 + AU × 6 + condition × 3 + custom)
to 8 (basic emotions × 7 + custom). Static tab renamed to "Faces" and
the preset dropdown relabelled "Emotion you posed".

Dead code purged: deleted `app/face_utils.py` (`display_info` overlay
was video-only), `app/plot.py` (`statistics_plot` line graph was
video-only), `scratch/smoke_video_peaks.py`. `app/app_utils.py` now
exports only `preprocess_image_and_predict`. `app/model.py` no longer
loads the LSTM weights or class; `LSTMPyTorch` removed from
`app/model_architectures.py`; `config.toml` `[model_dynamic]` section
and stale `FRAME_DOWNSAMPLING` removed. Deleted the bundled LSTM
checkpoint (`FER_dinamic_LSTM_IEMOCAP.pt`, ~11 MB) and the leftover
demo render artefacts (`result_face.mp4`, `result_hm.mp4`).

Word-doc Fingerprint template regenerated against the slimmed scope:
A1/A2 (basic-emotion stills + radar) and B1/B2 (basic-emotion bar
charts + radar) — AU-build and crash-emotion sections gone. Privacy
checklist and reflection prompts retained, prompts retargeted to the
basic-emotions experience.

Bumped `config.toml` `APP_VERSION` to 0.3.0.

**Related:** online-teaching-for-bence

Smoke tests: `import` of `app.py` via the spec loader builds the
Blocks cleanly. `PYTHONUNBUFFERED=1 uv run python app.py` reaches
"Running on local URL: http://127.0.0.1:7861" with no warnings, no
deprecation notices. The Fingerprint PDF export still works against
the new label scheme — radar uses session-level means so it doesn't
care about label structure.

The video element will live in a separate app (TBC). The HF Space
push should now be of this slimmer canonical version.

---

## 2026-06-05 feat: R3 + R4 + R5 landed; Word doc Fingerprint template generated

**Capability:** facial-expression-toy-app
**Uses:** matplotlib (polar + PdfPages), python-docx
**Approach:** New `app/fingerprint.py` owns the session-level analytics
and export. R3: `session_radar(captures)` builds a polar plot of mean
classifier confidence per emotion across the whole session (Static
captures + video peaks combined), with one coloured marker per emotion
and a filled polygon. Empty sessions render a blank grid so the panel
doesn't pop in/out. R4: `export_fingerprint_pdf(captures, mode)` uses
`matplotlib.backends.backend_pdf.PdfPages` to assemble the Fingerprint
in either Format A (cover with radar, summary bar, then a 4-up face
grid per page) or Format B (cover with radar, summary bar, a labelled
table of top emotions, then per-capture probability bars 4-up). Returns
a NamedTemporaryFile path so `gr.File` can serve it as a download. R5:
`PRIVACY_BANNER` `gr.Markdown` block sits above the tab strip with a
new `.privacy-banner` CSS rule (warm cream block, amber left border).
New "Fingerprint" tab shows the live radar and a format-toggle radio +
"Download Fingerprint PDF" button; the radar updates from every event
that mutates the session (`submit`, `submit_dynamic`, remove-last,
clear-session). `pyproject.toml` and `requirements.txt` gain
`matplotlib>=3.8.0` and `python-docx>=1.1.0`.

Also generated the Word-doc Fingerprint template at
`Lesson4-Fingerprint-template.docx` (~39 KB) via
`scratch/build_fingerprint_template.py`. Single document carries both
Format A (warm-up stills, AU-build attempts, crash-emotion captures,
radar) and Format B (warm-up bars, AU table, per-condition time-series
tables, radar) panels with delete-the-other-format instructions; a
shared Privacy Confirm checklist and Reflection section close the
document. Mirrors §6 of `formative-activity.html` verbatim.

**Related:** online-teaching-for-bence

Smoke tests: `scratch/smoke_fingerprint.py` builds 7 synthetic
captures, renders the radar (`Figure`, 1 axis), exports Format A
(~75 KB PDF) and Format B (~79 KB PDF), and verifies the empty-session
paths produce valid PDFs too (~43 KB / 44 KB). Full app launch:
`PYTHONUNBUFFERED=1 uv run python app.py` reaches "Running on local
URL: http://127.0.0.1:7861" cleanly, no warnings, no deprecations.
UI flow (radar update on submit, format toggle, PDF download) needs a
browser eyeball — can't verify click wiring from CLI.

R-list now complete: R0 ✅ R1 ✅ R2 ✅ R3 ✅ R4 ✅ R5 ✅. Next step is
pushing back to the HF Space as the new canonical version once Gordon
has eyeballed the UI flow in a browser.

---

## 2026-06-03 feat: R2 — video sweep + per-emotion peak frames feed the shared session

**Capability:** facial-expression-toy-app
**Uses:** mediapipe, torch (LSTM), cv2, gradio
**Approach:** Extended `preprocess_video_and_predict` to keep a
`frame_records` buffer of `(frame_idx, face_crop, prob_vector,
blendshapes)` for each analysed frame (one every `FRAME_DOWNSAMPLING`
face-detected frames). Post-loop, `_extract_peak_captures` derives 7
`Capture` instances by per-emotion argmax across the LSTM probability
vectors. Returns the existing 4-tuple (videos + plot) extended to a
5-tuple with the peak captures list. `app.py` lifted `session_captures`
from the Static tab scope to the demo level so both tabs share one
session; new `submit_video_and_add` handler runs the video pipeline,
merges the 7 peaks into the same `gr.State` that R1's Static submit
writes to, and surfaces a status markdown on the Dynamic tab telling
the student what was added. `Capture.heatmap` made `Optional` —
peak captures don't carry a heatmap (would require 7 extra Grad-CAM
passes; pedagogically not required).
**Related:** online-teaching-for-bence

Smoke test on the bundled `videos/BillClinton.mp4`: 7 peak captures
extracted. Pedagogically useful artefact: `peak-from-video:happiness`
lands on a frame where the actual top class is *Anger* 0.48 — the
clip never registers strongly happy, so the "happiest" frame is
still angry. That mismatch between the label (what we extracted
for) and the top emotion (what the classifier called it) is the
variability story made directly visible in the gallery caption.

`scratch/smoke_video_peaks.py` runs the pipeline end-to-end on the
bundled clip; Gradio app boots in ~18s with no warnings.

---

## 2026-06-03 feat: R1 — multi-capture session memory landed

**Capability:** facial-expression-toy-app
**Uses:** gradio (gr.State, gr.Gallery), mediapipe, torch
**Approach:** New `app/session.py` carrying a `Capture` dataclass
(label, face crop, heatmap, emotion probabilities, blendshapes) plus
`LABEL_PRESETS` (17 entries: 7 warm-up emotions, 6 hard AUs from the
FACS Build-an-AU section, 3 crash-emotion conditions, and a custom
free-text option). `app_utils.preprocess_image_and_predict` extended
to a 4-tuple return that surfaces the blendshape dict R0 plumbed.
`app.py` Static tab rewritten: per-tab `gr.State` accumulates
`Capture` instances across submissions; a label-preset dropdown +
visibility-toggled custom textbox set the label before each submission;
a `gr.Gallery` renders the session as a grid of cropped-face thumbnails
captioned with the resolved label and the top emotion; Remove-last and
Clear-session buttons act on the state. Latest-capture readout (face +
heatmap + 7-emotion label) preserved alongside the gallery so each
submission gives immediate feedback. Also moved `gr.Blocks(css=)` to
`launch(css_paths=[...])` (Gradio 6.0 deprecation).
**Related:** online-teaching-for-bence

Smoke tests in `scratch/`: session ops correct, Capture renders the
right caption (`warm-up:happy · Happiness (0.99)`), blendshapes line
up with AU 12 (`mouthSmileLeft/Right` ~0.92 on Happy.png). Gradio app
boots in ~18s with no deprecation warnings, server reaches "Running
on" state. UI flow (gallery rendering, click handlers, visibility
toggles) needs manual browser eyeball — verifying click wiring from
the CLI isn't feasible.

Env hygiene: `numexpr` (transitive of the now-removed py-feat) was
hard-failing pandas import on numpy 2.x. Uninstalled — pandas degrades
gracefully without it. Worth a clean venv rebuild from `requirements.txt`
at some point but not blocking.

---

## 2026-06-03 feat: R0 — face detection switched to MediaPipe FaceLandmarker tasks API

**Capability:** facial-expression-toy-app
**Uses:** mediapipe (tasks API), torch, pytorch_grad_cam
**Approach:** Implemented R0 (Option δ). New `app/face_landmarker.py`
module wraps MediaPipe FaceLandmarker as a lazy singleton, exposing
`detect(pil_image) -> (landmarks, blendshapes)` and
`bbox_from_landmarks(...)`. Bundle downloads on first call via the
existing `load_model()` helper, driven by a new `[model_landmarker]`
section in `config.toml`. `app/app_utils.py` rewritten to call the new
module instead of `mp.solutions.face_mesh` (broken on Apple Silicon);
return signatures preserved so `app.py` is unchanged. Dead code
(`get_box`, `norm_coordinates`) removed from `app/face_utils.py`;
`display_info` retained for the dynamic-video overlay. Smoke test
post-R0 confirms end-to-end pipeline runs cleanly and classifier
output matches pre-R0 results within rounding (Disgust 0.987,
Fear 0.992, Happiness 0.986, Sadness 0.990, Surprise 0.763).
Blendshapes are extracted on every detection but not yet surfaced to
the UI — they land with R1's session memory rewrite.
**Related:** online-teaching-for-bence

Also cleaned: broken py-feat install removed from the venv. uv.lock
committed for reproducible builds.

---

## 2026-06-03 audit: pipeline walked, smoke-tested upstream, evaluated AU detection options

**Capability:** facial-expression-toy-app
**Uses:** mediapipe (tasks API), torch, pytorch_grad_cam (rejected: py-feat)
**Approach:** Walked the existing pipeline (`app.py` + `app/` package, six
modules) and ran three smoke tests under `scratch/`. Confirmed the
ResNet50 + Grad-CAM static path works on bundled samples — top class
hits the labelled emotion on every well-defined sample (Disgust 0.92,
Fear 0.97, Happiness 0.995, Sadness 0.99, Surprise 0.84; Anger
top-2 with Fear; "contempt" maps to Anger because the model has no
contempt class). MediaPipe legacy solutions API
(`mp.solutions.face_mesh` in `app_utils.py`) is broken on Apple Silicon
mediapipe>=0.10 — only the tasks API ships; blocks local dev as-is.
Py-feat 0.6.2 rejected as an AU-detection route: imports fail against
torchvision 0.27 (uses removed `torchvision.io.read_video`), confirmed
in both the main venv and an isolated venv. Pivoted to MediaPipe Face
Landmarker (tasks API), which downloads its own bundle and runs on
Apple Silicon: bilateral ARKit blendshape output maps cleanly onto the
lesson's target AUs (`browInnerUp` = AU 1, `cheekSquintLeft/Right` =
AU 6 Duchenne, `mouthDimpleLeft/Right` = AU 14 dimpler, etc.). Sample
sanity-check: Fear shows asymmetric `browOuterUp` 0.49/0.29; contempt
shows unilateral `mouthDimpleLeft` 0.10 alongside asymmetric smile.
**Related:** online-teaching-for-bence

**Recommendation pending user approval: Option δ.** Replace
`mp.solutions.face_mesh` with the tasks API Face Landmarker; reuse the
same model for both face cropping (478 landmarks → bbox, replacing
`get_box`) and blendshape output (AU-equivalent feedback). Keep
upstream ResNet50/LSTM for emotion classification. Three wins: fixes
local dev, gives the FACS Build-an-AU section the per-AU feedback it
needs, no new heavy dependency. Adds R0 ahead of R1 in the R-plan at
`~/Code/teaching/online-teaching-for-bence/toy-app-plan.md`:
"Replace legacy `mp.solutions.face_mesh` with the tasks API Face
Landmarker — required to run on Apple Silicon, unlocks blendshape
output." Awaiting user approval before implementation.

Smoke tests live under `scratch/` (gitignored content, scripts kept
local; not committed — they're throwaway evidence for this decision).

---

## 2026-06-03 chore: rename repo from all-a-bit-emotional to totes-emosh

**Capability:** facial-expression-toy-app
**Uses:****Approach:** Renamed the local codewright fork from `all-a-bit-emotional`
to `totes-emosh` at Gordon's request. Updated `.codewright.yaml`,
`pyproject.toml`, README, DEVLOG, registry, and capability index.
Upstream HuggingFace Space slug at LittleMonkeyLab/All_a_bit_emotional
is unchanged.
**Related:** online-teaching-for-bence

---

## 2026-06-03 init: standalone fork of LittleMonkeyLab/All_a_bit_emotional HF Space

**Capability:** new: facial-expression-toy-app
**Uses:** gradio, torch, mediapipe, grad-cam, hf-hub (upstream Space)
**Approach:** Downloaded the live HuggingFace Space via `hf download` and
re-anchored it as a codewright-managed repo at
`~/Code/research/totes-emosh/` (originally named `all-a-bit-emotional`,
renamed same day). No code changes — clean fork of
the deployed Space as the baseline for the toy-app rework described in
`~/Code/teaching/online-teaching-for-bence/toy-app-plan.md` (R1–R5).
Added `pyproject.toml` as the codewright-canonical dependency manifest;
preserved upstream `requirements.txt` for HuggingFace Space deployment.
**Related:** online-teaching-for-bence

Standalone exists so the rework can be code-reviewed and version-controlled
before pushing back upstream as the new canonical Space. Original Space
remains live and unchanged at
https://huggingface.co/spaces/LittleMonkeyLab/All_a_bit_emotional.
Next session: walk the existing `app/` package and plan the R1 (multi-capture
session memory) integration point.

---