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