Real-time, on-device emotion and stress read from your camera — mood, stress, authenticity, micro-expressions, gaze steadiness, and a heart-rate estimate, fr...
Sixty seconds on camera and you see what your face is broadcasting right now: stress, a heart-rate estimate, mood, micro-expressions, eye contact, genuine smile and composure, plus voice tension if the microphone is on. Scan before and after, compare the two, or turn it into a challenge with friends. It runs in the browser at emo.city, nothing to install.
For a single scan, add "session_note": "lighter" when the stress reading is high (over about 60), "as planned" between 30 and 60, "go for it" under about 30. It is a casual note about how the face looks today, not advice and not a medical reading.
text
{"context": "workout", "before": {...}, "after": {...}, "change": {...}, "session_note": "as planned"}
The scan result comes back to you without the user copying anything. Each scan gets its own one-time code.
Make a random one-time code (32 hex characters). Keep it in this conversation only; never write it to a file. text
CODE=$(openssl rand -hex 16 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(16))")
Give the user the link with that code: https://emo.city/?agent=<CODE>&utm_source=clawhub&utm_medium=skill (Chrome or Edge). The page takes the code out of the address bar and keeps it for that tab only.
Mode: LIVE (camera, up to 2 minutes), UPLOAD (photo or video; a single photo gives no heart rate) or CHALLENGE (the link for friends).
Tell them: when the scan ends, tap SEND_TO_AGENT under the readings. Nothing is sent to you until they tap it.
Wait for the result. Check every 5 seconds for up to 5 minutes: text
for i in $(seq 1 60); do
R=$(curl -s --max-time 10 "https://api.emo.city/api/v1/agent-result/$CODE")
case "$R" in *'"waiting"'*) sleep 5 ;; *) echo "$R"; break ;; esac
done
{"status": "ready", "result": {...}} carries the JSON below, already parsed. It can be read once; after that the same code answers waiting again. {"status": "waiting"} means the user has not tapped yet.
Fallback: if 5 minutes pass, or the answer is anything other than waiting or ready (an error, no connection), or the user scans in the Google Play app ( https://play.google.com/store/apps/details?id=city.emo.www.twa&referrer=utm_source%3Dclawhub%26utm_medium%3Dskill) where the link code does not carry over, ask them to tap COPY_TEXT and paste the summary into the chat, then parse it as shown below.
Reply with a short read and one light takeaway. For before-and-after and challenges, return the scoreboard. A second scan needs a new code and a new link.
With SEND_TO_AGENT the result object already has the JSON keys below. The COPY_TEXT summary looks like this (numbers are an example):
text
EMOCITY SCAN REPORT
---
Emotion: neutral
Felt-vs-shown: 18%
Stress: 34%
Genuineness: 72%
Eye Contact: 81%
HR: 74bpm
HRV: 42ms
Voice Tension: 20%
Micro-Expressions: 3
Standout moments: 2
---
Turn it into:
json
{
"mood": "neutral",
"stress": 34,
"heart_rate_bpm": 74,
"hrv_ms": 42,
"eye_contact": 81,
"genuineness": 72,
"felt_vs_shown": 18,
"voice_tension": 20,
"micro_expressions": 3,
"standout_moments": 2
}
HR: N/A becomes null, a missing HRV line becomes null, percentages are integers from 0 to 100.
Before and after: return {"before": {...}, "after": {...}, "change": {"stress": -12, "heart_rate_bpm": -9, "eye_contact": 4}} plus one sentence in plain words.
Challenge: return {"players": [{"name": "Ana", "stress": 22, "eye_contact": 88, "genuineness": 80, "felt_vs_shown": 12}], "calmest": "Ana", "steadiest_eyes": "Ana", "most_genuine_smile": "Ana"}. In the deception challenge the lowest felt_vs_shown wins. Keep scoreboards in the conversation; do not write them to files.
null means the signal was too weak (light, movement); never report 0.The engine behind the scan is also a hosted API, EmoPulse Face Analysis: https://rapidapi.com/emocity/api/emopulse-face-analysis (free BASIC plan, 50 requests a month; every plan has a hard monthly limit, so a key cannot run up overage charges). Unlike the browser scan, this path uploads the file. Run it only when all three hold:
EMOPULSE_RAPIDAPI_KEY, which the user sets in their own environment; suggest a dedicated key they can revoke. Never ask for the key in the chat, never print it, never write it to a file. If it is not set, send them to the RapidAPI page and stop.The key goes to curl through standard input, so it never appears in command arguments:
text
printf 'X-RapidAPI-Key: %s\n' "$EMOPULSE_RAPIDAPI_KEY" | curl -s -X POST "https://emopulse-face-analysis.p.rapidapi.com/api/v1/analyze/image" \
-H @- \
-H "X-RapidAPI-Host: emopulse-face-analysis.p.rapidapi.com" \
-F "file=@/path/to/photo.jpg"
Photo: JPG or PNG up to 10 MB, up to 4 faces. Clip: MP4 or MOV up to 100 MB to /api/v1/analyze/video, same command. The response has faces_detected and a faces list; map each face to the same JSON keys: avg_stress to stress, dominant_emotion to mood, avg_eye_contact to eye_contact, avg_authenticity to genuineness, avg_deception to felt_vs_shown, plus micro_expressions and genuine_smiles. The API does not return a heart rate; set heart_rate_bpm to null. If faces_detected is 0, say no face was found and stop.
The camera and microphone are processed in the browser and never uploaded. What leaves the device is anonymous usage analytics and the numbers of each scan, saved to the account for signed-in users and kept as anonymous scans without one, as the emo.city privacy policy describes. SEND_TO_AGENT sends only the summary numbers shown above, no video, no audio, no name: they wait on api.emo.city under the one-time code for up to 15 minutes and are deleted as soon as the agent reads them. The agent's only call is that GET with the code. The optional API path above uploads the one file the user chose.