Spaces:
Sleeping
Sleeping
| """ | |
| 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"]) | |