""" 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 = """
Six-emotion replication challenge pose each emotion · classifier reads what it sees · download a one-page EmotionMap
Privacy

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 Wireframe (face-free) below if you would rather not submit identifiable photos; the export then ships only the anonymised landmark mesh.

""" FOOTER_HTML = """ """ 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( '
' '1. Take a photo with your webcam, ' 'or drop in an image' '2. Pick which of the six emotions ' 'you are posing' '3. Submit — the matching tile ' 'fills in' '
' ) 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"])