Author: Guangyi Zhao
1. The problem
Emotion-Guard is my open-source demo of an idea that came out of real work: run emotion recognition in a kindergarten, watch for sustained negative mood, and nudge a teacher to check in on a child before distress turns into an incident — or before a stressed adult does something worse. The motivation is protective. The subjects, however, are three to six years old. They cannot consent; their parents sign for them; and the record being created is about their faces and their feelings, accumulated day after day.
This article is not about making the model more accurate. There is no real model yet — the only classifier that runs in this repository is a simulator. What I did instead was audit the code line by line and run it, end to end, on synthetic data, asking a more basic question: what does this system already owe, in deployment terms, before accuracy even enters the picture? Four small things turned out to carry the weight: how often the alert rules fire on perfectly ordinary days, how long history is kept by default, whether reports carry children's names, and whether downstream code can still tell simulated data from real data. The code currently handles none of the four well — and those are precisely the things regulators look at. Article 5(1)(f) of the EU AI Act has, since 2 February 2025, prohibited using AI to infer emotions in workplaces and education institutions, with only a narrow medical-or-safety exception. In the US there is no equivalent federal ban, but the 2025 COPPA amendments name biometric identifiers, facial templates included, as children's personal information requiring verifiable parental consent before collection plus a written retention policy and a ban on keeping data longer than reasonably necessary; Illinois' BIPA demands written notice and a written release, a public retention and destruction schedule, and gives individuals a private right of action. In much of the world, a system like this does not get to exist because it works. It has to prove it deserves to exist first.
One scientific caveat frames everything below. Barrett and colleagues' 2019 review in Psychological Science in the Public Interest covered more than a thousand studies and concluded that facial movements are neither reliable nor specific enough to confidently infer emotion — a smile is not proof of happiness, a scowl is not proof of anger. Facial signals are weak signals. Thresholds, human review, and retention all have to be designed for a system that is often wrong.
2. Method and architecture
The subject is the repository as shipped — 970 lines of Python, package plus examples and tests, unmodified. The pipeline has five stages. detector.py finds faces (a real OpenCV Haar detector, or a mock for the demo). classifier.py classifies emotion behind a swappable protocol, but only MockEmotionClassifier actually runs: it draws from preset distributions, while FERClassifier raises NotImplementedError and survives only as a docstring describing how fer or DeepFace would be wired in, with a seven-to-four class mapping (happy / calm / sad / angry). classroom.py stores every observation per person and computes mood shares. alerts.py runs three rules: sustained negative mood (at least 4 of the last 6 observations sad/angry for a warning, all 6 for critical), teacher spike (a single angry observation in the last 6 triggers a warning), and sudden swing (adjacent happy-then-angry within 20 minutes triggers a watch). dashboard.py renders a console view plus JSON and HTML reports that list, per person, name, ID, mood shares, and alerts.
First, a correction I owe the reader. I have called this system "multimodal." The audit does not support that. Searching the repository for audio, voice, and transcript returns zero hits. The only working input modality is the face. Everything below treats it as the single-modality system it is.
Eight experiment groups, all reproducible from data/2026-10-18-emotion-guard-bench.py and its JSON output:
- Baseline: the shipped test suite, plus one scripted school day — 8 children, 1 teacher, 24 twenty-minute slots, 216 observations, with three scripted beats (child C-04 slides into afternoon distress, C-02 swings once, teacher T-01 flashes anger once) and everything else filled by the mock distributions.
- Null-background false alarms: no scripted beats at all, only the shipped
typical_childandtypical_teacherdistributions (15% negative rate for children, 5% angry for the teacher), 30 simulated days, alerts evaluated slot by slot as the demo does. Seeds for this group are SHA-256-derived, for reasons section 3.3 will make clear. - Privacy surface audit: static checks of the data model (any retention, deletion, expiry, anonymisation, or consent machinery?) and a dynamic test of whether the report's
simulatedlabel survives contact with real-flagged data. - Retention volume: extrapolated from a 147-byte JSON row per observation.
- Truncation: cut each person's history to the last 6 observations and see what each rule loses.
- Confidence: the same all-sad sequence scored at confidence 0.99 versus 0.51.
- Swing edges: an intervening calm observation, and a 40-minute gap.
- Reproducibility: the same script in five separate processes, then three more with
PYTHONHASHSEED=0.
3. Measurements
3.1 Baseline: tests pass, and most swing alerts are not the story
All 14 shipped tests pass. Evaluated once at end of day, the scripted day produces between 3 and 8 alerts (why a range, see 3.3). One of them is constant: the sustained-negative critical for C-04. The scripted distress is caught — that part works. Swing watches, however, number 2 to 7, and only one is the scripted C-02 swing. The rest are ordinary children whose random adjacent draws happened to go happy, then angry. On the demo day itself, most of this rule's output is background noise wearing an alert's clothing.
The teacher spike shows the opposite failure. In end-of-day evaluation across five separate-process runs it appeared in only one: the scripted anger sits at slot 15 and has slid out of the last-6 window by day's end. examples/demo.py therefore evaluates slot by slot, and its comment says exactly why — end-of-day evaluation misses mid-day spikes. That design choice is correct, but it has a mirror image: any offline replay of stored logs will systematically miss teacher spikes and, as section 3.5 shows, systematically resurrect stale swings.
3.2 Null background: on calm days, the alerts are not calm
Thirty days, eight children a day, nobody distressed, nobody scripted — just the factory distributions rolling dice. Sustained-negative warnings fired at 33 slot evaluations; 15 of 240 child-days, 6.3%, saw at least one "sustained negative emotion, please check in" warning. Critical never fired: six consecutive negative draws at a 15% base rate is genuinely rare, and the rule deserves credit for that. The teacher rule does not. Teacher spike warnings fired on 23 of 30 days, 76.7%. The rule triggers on a single angry draw inside a six-observation window, the teacher's shipped angry rate is 5%, and slot-by-slot evaluation gives the dice dozens of chances a day. A warning most days is not a property of the classroom; it is a property of the arithmetic.
Translated: deploy these thresholds as shipped and, in a class where every child is fine every day, staff still get a sustained-distress warning roughly once per sixteen child-days, and a "your teacher showed anger, consider a rotation" warning two days out of three. Alert fatigue, in this design, is not a training problem. It is the rules' expected output.
3.3 Reproducibility: the demo's seed is not a seed
Why is the baseline an interval? Because examples/simulate.py derives per-slot seeds with Python's built-in hash(), and string hashing is randomised per process via PYTHONHASHSEED. Same script, same master_seed=7: five processes produced alert totals of 4, 5, 6, 8, and 8, and the first six observations' emotion sequences were different all five times. With PYTHONHASHSEED=0, three runs agreed exactly (8 alerts each). The module docstring promises "seeded, reproducible." Within one process, yes. Across runs, no. For a demo that is a shrug; for a system whose logs might later be replayed in a review or a dispute, it is disqualifying — the same "deterministic" day names different children. The fix costs one line: derive seeds with a stable hash. The false-alarm group in 3.2 already does this.
3.4 Privacy surface: the switches that should exist, don't
The static audit is short because there is so little to find. The Observation record holds the person (ID, name, role), a timestamp to the second, the emotion, a confidence, and a simulated flag. One genuine credit: it never stores the face image. Frames live only inside the detect-and-classify call; what persists is the verdict. That is the single best privacy decision in the codebase.
Everything downstream of that decision is missing. ClassroomSession has no purge, delete, expire, or anonymise method; searching the package for retention or delete returns zero hits. History grows without bound, and mood_summary computes fractions over all of it — which is to say, the system builds a longitudinal emotional profile per named child by default, and keeps it for exactly as long as nobody restarts the process. Person has no consent field, and ingest checks only that a person was registered, not that anyone agreed to anything. The pre-collection consent gate that COPPA and BIPA are built around does not exist in the data model. Reports are per-person by construction: ID, name, role, observation count, four mood shares, and every alert with its timestamp. One demo day serialises to 2,163 bytes in the run sampled here (it wobbles slightly with alert count), essentially all of it about identifiable individuals.
Then the dynamic test found something worse than an omission. I fed a fresh session a single observation explicitly flagged simulated=False — the situation the day a real model gets wired in. build_report still returned simulated: True, because that value is a hard-coded constant in dashboard.py, unrelated to the data. The console dashboard likewise prints "all data below is SIMULATED" unconditionally. In the demo that is harmless. The morning someone connects a real classifier and forgets those two lines, the system will be labelling real children's emotional records as simulated — and provenance labels are the first thing an audit reads.
3.5 Truncation: retention can be sized to the rules — with one exception
Cut every history to the last six observations and re-run. Sustained negative: identical output, critical included. Teacher spike: identical. Both rules only ever read the last six, so every observation beyond six is, for them, storage without function. The exception is the swing rule, which scans the entire history with zip(history, history[1:]). A happy-angry pair from early morning still produces a watch in an end-of-day evaluation over full history; truncated to the last six (by then all calm), it vanishes. So the rule with the worst noise profile (3.1, 3.2) is the only one demanding unbounded retention. That is the worst trade in the codebase. Mood shares are the honest counterpoint: they are defined over all history, so truncation changes their meaning. If you want longitudinal trends, say so, and run them under consent and a stated retention period. If you don't, report classroom aggregates only.
Volume, by contrast, is a non-issue. At 147 bytes a row, the demo cadence (216 observations a day) accumulates about 0.95 MB in 30 days and 5.7 MB in 180. Even at one observation per person per minute (nine people across eight hours, 4,320 a day), it is roughly 19 MB a month and 114 MB in half a year. Disks are cheap. What is not cheap is what each 147-byte row contains: a name, a minute, a feeling. Retention policy has to govern identifiability and time resolution, not megabytes.
3.6 Confidence is ignored, and swings only see neighbours
Two small experiments finish the rules' portrait. The same six sad observations at confidence 0.99 and at 0.51 produce byte-identical alerts — critical either way. AlertEngine never reads confidence when deciding; it only formats the number into the teacher-spike message text. Worse, the mock's confidence is a uniform random draw between 0.62 and 0.97, independent of the emotion drawn, so in the demo every confidence figure is decoration. Wire in a real model without adding a confidence gate and low-quality guesses will count toward "sustained distress" exactly as much as solid ones. And the swing rule only inspects adjacent pairs: happy-calm-angry does not fire, nor does happy-to-angry across a 40-minute gap. At a 20-minute sampling cadence, a real transition with any intermediate state is invisible to it — which is the same fact as 3.1's noise dominance, seen from the other side: the rule detects dice adjacency, not emotional change.
4. What I would change, in order of cost
- Stable seed derivation. One line. Without reproducibility, log-based review and appeals are theatre.
- Derive the
simulatedlabel from the data: summarise the flags at report level, carry provenance on each alert. The day simulated and real streams mix, downstream code must be able to tell them apart. - Size retention to the rules. Six observations per person fully serve two of three rules. Give the swing rule a bounded window (two hours, say) or retire it; do not feed the noisiest rule an infinite memory. Make per-person longitudinal shares an explicit opt-in feature that switches on consent and expiry machinery.
- Split reporting into two layers. The classroom layer is aggregates only: class-wide mood shares and per-rule alert counts. I prototyped it — same day, 199 bytes against the original 2,163, alert counts and classroom shares preserved exactly, at the cost of removing all nine names and every per-person curve. The individual layer keeps only what an alert needs, under rotating pseudonyms, with the name mapping held offline.
- Two gates on alerting: a confidence floor below which observations do not count toward sustained-negative totals, and a frequency budget (at most one warning per child per day; anything beyond that folds into a digest). Section 3.2 is the argument: without these, the rules manufacture warnings on 6.3% of child-days and 76.7% of teacher-days out of pure chance.
- Consent as a first-class field.
Personcarries consent status and time;ingestrefuses anyone without it. A gate at the ingestion point holds up better than a paragraph in a policy document. - Downgrade the language. Output from this system is "worth a look," never "this child is distressed." The current alert copy already says "recommend a caring check-in," and that restraint needs to survive into the product name and the sales pitch. Barrett et al. settle the matter: presenting these outputs as emotional truth is overclaiming.
On the law, an engineer's reading only, not legal advice: in the EU, education-sector emotion inference starts from prohibition, the safety exception has to be argued case by case with the burden on the deployer, and a child-protection framing like this project's may have an argument — it is not a free pass. In the US, the gates are COPPA's consent and retention rules, BIPA-style written releases and destruction schedules, and state student-privacy law. Both regimes point the same way: consent and retention design first, system second.
5. Limitations and open questions
The false-alarm figures are conditional on the mock's factory distributions — 15% negative for children, 5% angry for the teacher — which were chosen to make a demo legible, not measured from real preschoolers. Different base rates move 6.3% and 76.7%; they do not rescue the teacher rule's single-hit sensitivity, nor make critical common at low base rates. No real classifier was benchmarked: real model errors are not independent dice rolls but correlate with age, ethnicity, and lighting, so real-world false alarms will arrive in clusters, and alert fairness across groups needs its own measurement, which this article does not have. Nothing here measures what happens after an alert — teacher response rates, how fast trust in the system erodes, the cost of a missed true event. Those field numbers, not lab numbers, decide whether such a system should exist. The legal discussion is an engineering reading; the boundaries of the EU safety exception and COPPA's reach over purely on-premise, offline systems have not been reviewed by counsel. The reproducibility experiment is small (five processes, three controls), but the effect is all-or-nothing, which is enough for the qualitative claim.
6. References
- Emotion-Guard repository (Guangyi Zhao, MIT). Key files:
emotion_guard/classifier.py(mock distributions, FER stub),emotion_guard/alerts.py(three rules; defaults window=6, negative_threshold=4, critical_threshold=6, swing_window_minutes=20),emotion_guard/classroom.py(unbounded history, mood shares),emotion_guard/dashboard.py(hard-codedsimulated, per-person report),examples/simulate.py(_slot_seed),examples/demo.py(slot-by-slot evaluation comment). - Raw data for this article:
data/2026-10-18-emotion-guard-bench.json; script:data/2026-10-18-emotion-guard-bench.py(false-alarm group uses SHA-256-derived seeds, stable across processes). - Barrett, L. F., Adolphs, R., Marsella, S., Martinez, A. M., & Pollak, S. D. (2019). Emotional Expressions Reconsidered: Challenges to Inferring Emotion From Human Facial Movements. Psychological Science in the Public Interest, 20(1), 1–68. https://journals.sagepub.com/doi/10.1177/1529100619832930
- Regulation (EU) 2024/1689 (AI Act), Article 5(1)(f); European Commission, Guidelines on Prohibited Artificial Intelligence Practices (2025-02-04), Section 7.1.
- FTC, COPPA Rule amendments (final rule announced 2025-01-16): biometric identifiers as personal information; written retention policy and retention limits.
- Illinois Biometric Information Privacy Act, 740 ILCS 14/15 (written notice and release; public retention and destruction schedule) and 14/20 (private right of action, statutory damages).
Author: Guangyi Zhao. The system, code, and experiments described here are the author's own clean-room work. All experimental data is synthetic; no real children's images or personal information were used, and no client information is involved.
Top comments (0)