totes-emosh / app.py
drdeception
feat: six-emotion replication challenge — totes-emosh EmotionMap build
0d27c43
Raw
History Blame Contribute Delete
8.07 kB
"""
File: app.py
Author: Dr. Gordon Wright
Description: Six-emotion replication challenge. The user poses each of
the six basic emotions in turn; the classifier reads each
attempt; the tiles fill in to form a single-page A4
EmotionMap artefact at the end.
Compact layout: one-line header, collapsible privacy,
single input row, 2x3 tile grid, one download button.
Wireframe toggle anonymises every face on screen and in
the export.
License: MIT License
"""
import gradio as gr
from PIL import Image as PILImage
from app.app_utils import preprocess_image_and_predict
from app.session import (
BASIC_EMOTIONS,
Capture,
empty_session,
session_status,
)
from app.tiles import grid_tiles, export_single_page_pdf
HEADER_HTML = """
<div class="te-header">
<span class="te-title">Six-emotion replication challenge</span>
<span class="te-sub">pose each emotion · classifier reads what it sees · download a one-page EmotionMap</span>
</div>
<details class="te-privacy">
<summary>Privacy</summary>
<p>This app uploads nothing beyond the model inference call and stores
nothing on a server. Your captures live in this browser tab only — refresh
and they are gone. Turn on <strong>Wireframe (face-free)</strong> below if
you would rather not submit identifiable photos; the export then ships only
the anonymised landmark mesh.</p>
</details>
"""
FOOTER_HTML = """
<div class="te-footer">
Created by Dr. Gordon Wright — A LittleMonkeyLab caper.
Part of the Goldsmiths MSc in Psychology, Week 3 Part 4.
</div>
"""
def _tiles_and_status(state, wireframe):
image_size = None
for cap in state.values():
if cap is not None and cap.image_size is not None:
image_size = cap.image_size
break
return grid_tiles(state, wireframe=wireframe, image_size=image_size), session_status(state)
def submit_attempt(image, intended, wireframe, state):
if state is None:
state = empty_session()
if image is None:
tiles, status = _tiles_and_status(state, wireframe)
return state, *tiles, status + " (Upload or capture a face first.)"
if intended not in BASIC_EMOTIONS:
tiles, status = _tiles_and_status(state, wireframe)
return state, *tiles, status + " (Pick which emotion you are posing.)"
image_size = image.size # (W, H)
face, heatmap, confidences, blendshapes, landmarks, bbox = (
preprocess_image_and_predict(image)
)
if face is None:
tiles, status = _tiles_and_status(state, wireframe)
return state, *tiles, status + " (No face detected in that image.)"
state[intended] = Capture(
intended=intended,
face=face,
emotion_probs=confidences,
heatmap=heatmap,
blendshapes=blendshapes,
landmarks=landmarks,
bbox=bbox,
image_size=image_size,
)
tiles, status = _tiles_and_status(state, wireframe)
return state, *tiles, status
def retry_slot(slot_emotion, wireframe, state):
if state is None:
state = empty_session()
state[slot_emotion] = None
tiles, status = _tiles_and_status(state, wireframe)
return state, *tiles, status
def clear_all(_state, wireframe):
state = empty_session()
tiles, status = _tiles_and_status(state, wireframe)
return state, *tiles, status
def toggle_wireframe(wireframe, state):
tiles, status = _tiles_and_status(state or empty_session(), wireframe)
return tiles + [status]
def download_artefact(state, wireframe, student_name):
if state is None:
state = empty_session()
image_size = None
for cap in state.values():
if cap is not None and cap.image_size is not None:
image_size = cap.image_size
break
return export_single_page_pdf(
state,
student_name=student_name or "",
wireframe=wireframe,
image_size=image_size,
)
with gr.Blocks(title="totes-emosh") as demo:
gr.HTML(HEADER_HTML)
session_state = gr.State(empty_session())
gr.HTML(
'<div class="te-steps">'
'<span class="te-step"><b>1.</b> Take a photo with your webcam, '
'or drop in an image</span>'
'<span class="te-step"><b>2.</b> Pick which of the six emotions '
'you are posing</span>'
'<span class="te-step"><b>3.</b> Submit — the matching tile '
'fills in</span>'
'</div>'
)
with gr.Row(elem_classes="te-input-row"):
with gr.Column(scale=2):
input_image = gr.Image(
label="Your pose",
type="pil",
sources=["webcam", "upload"],
height=320,
elem_classes="te-input-image",
)
with gr.Column(scale=1):
intended_emotion = gr.Dropdown(
choices=BASIC_EMOTIONS,
value="happy",
label="Which emotion are you posing?",
)
wireframe_toggle = gr.Checkbox(
value=False,
label="Wireframe (face-free) mode",
)
submit_btn = gr.Button(
value="Submit attempt",
variant="primary",
size="lg",
elem_classes="te-submit",
)
clear_btn = gr.Button(value="Clear all six")
status_md = gr.Markdown(value=session_status(empty_session()),
elem_classes="te-status")
# 2x3 tile grid
initial_tiles = grid_tiles(empty_session())
tile_images = []
retry_buttons = []
for row in range(2):
with gr.Row(elem_classes="te-tile-row"):
for col in range(3):
idx = row * 3 + col
emo = BASIC_EMOTIONS[idx]
with gr.Column(elem_classes="te-tile-col"):
img = gr.Image(
value=initial_tiles[idx],
show_label=False,
interactive=False,
height=406,
elem_classes="te-tile-img",
)
retry = gr.Button(
value=f"Retry {emo}",
size="sm",
elem_classes="te-retry",
)
tile_images.append(img)
retry_buttons.append((emo, retry))
with gr.Row(elem_classes="te-export-row"):
student_name = gr.Textbox(
label="Your name (appears on the PDF)",
placeholder="optional",
scale=2,
)
download_btn = gr.Button(
value="Download single-page EmotionMap PDF",
variant="primary",
scale=1,
)
download_file = gr.File(label="EmotionMap PDF", interactive=False)
gr.HTML(FOOTER_HTML)
# ---- wiring ----
submit_outputs = [session_state, *tile_images, status_md]
submit_btn.click(
fn=submit_attempt,
inputs=[input_image, intended_emotion, wireframe_toggle, session_state],
outputs=submit_outputs,
queue=True,
)
clear_btn.click(
fn=clear_all,
inputs=[session_state, wireframe_toggle],
outputs=submit_outputs,
queue=False,
)
for slot_emo, button in retry_buttons:
button.click(
fn=retry_slot,
inputs=[gr.State(slot_emo), wireframe_toggle, session_state],
outputs=submit_outputs,
queue=False,
)
wireframe_toggle.change(
fn=toggle_wireframe,
inputs=[wireframe_toggle, session_state],
outputs=[*tile_images, status_md],
queue=False,
)
download_btn.click(
fn=download_artefact,
inputs=[session_state, wireframe_toggle, student_name],
outputs=[download_file],
queue=True,
)
if __name__ == "__main__":
demo.queue(api_open=False).launch(share=False, css_paths=["app.css"])