read_the_room / app.py
Ilia-Iliev's picture
Upload folder using huggingface_hub
9ff1b35 verified
Raw
History Blame Contribute Delete
5.61 kB
"""Read the Room — navigate a complex social situation, directed by AI.
Each scenario is a tab. `gameview` builds every game from the Scenario data, so adding a
scenario in scenarios/ adds a tab here for free; `communityview` browses what other players
shared; `creatorview` is the author-your-own tab. Both hand a freshly parsed Scenario to
the dedicated Play tab.
"""
import threading
import urllib.request
import gradio as gr
import communityview
import creator
import creatorview
import gameview
from art import font_css
from engine import API_BASE
from scenarios import REGISTRY
PLAY_TAB_ID = "play_custom"
# The page chrome sits straight on the scene backdrop. The big title gets a white
# text-shadow edge (outline-by-shadow renders everywhere, unlike -webkit-text-stroke
# which eats into the glyphs); at tab-label sizes an outline just smudges, so the tab
# row gets a frosted translucent strip behind it instead and keeps its own typography.
def white_edge(r):
"""A text-shadow outline: 8 white shadows ringing the glyph at radius `r` px."""
return ",".join(
f"{dx}px {dy}px 0 #fff" for dx in (-r, 0, r) for dy in (-r, 0, r) if dx or dy
)
NAV_CSS = (
# Alkatra's glyph box runs a few px taller than the h1 line box; Gradio puts
# overflow:auto on the block as an INLINE style, so without !important the title
# grows a tiny scrollbar instead of just painting the overhang
f".rtr-title{{overflow:visible !important}}"
f".rtr-title h1{{color:#111;text-shadow:{white_edge(1)},{white_edge(2)}}}"
".tab-container{background:rgba(255,255,255,.72);backdrop-filter:blur(8px);"
"border-radius:10px;padding:0 10px}"
".tab-container button:not(.selected){color:#111}"
)
# Gradio's footer Settings panel doesn't work well here — hide it (and its divider dot),
# keeping "Use via API" and the Gradio credit.
FOOTER_CSS = (
"footer button.settings,footer .divider:not(.show-api-divider)"
"{display:none !important}"
)
def warm_model():
"""Ping the model endpoint so the Modal container boots while the user reads."""
def ping():
try:
# Held open on purpose: serve_modal's gate answers only once the model is
# loaded, and Modal cancels queued requests whose client disconnects.
urllib.request.urlopen(
API_BASE.removesuffix("/v1") + "/health", timeout=300
)
except OSError:
pass
threading.Thread(target=ping, daemon=True).start()
def do_load(scen):
"""Switch to the Play tab and fill it with the freshly parsed scenario."""
return (gr.update(selected=PLAY_TAB_ID), *gameview.load_into_play(scen))
def load_scenario(path):
"""Open a saved scenario straight in the Play tab — no detour through the editor."""
try:
scen = creator.spec_to_scenario(creator.load_spec(path))
except Exception as e: # not a spec / invalid spec — surface as a toast
raise gr.Error(f"{creator.LOAD_ERROR}: {e}")
return gameview.load_into_play(scen)
with gr.Blocks(
title="Read the Room",
# Soft's bundled "Montserrat" woff2 is really Montserrat Alternates — a display
# face that's rough on body text. Alkatra keeps the storybook feel but reads better;
# it ships in fonts/ (inlined by font_css) rather than as a GoogleFont so the app
# loads nothing from the network. Each name is wrapped in Font (a bare str would
# crash launch(): gradio compares themes font-by-font and Font.__eq__ can't take str).
# .set() strips Soft's per-field chrome — the indigo label pills and the white card
# behind every component turned the creator form into a wall of boxes. Labels become
# plain text on the page; only the inputs themselves keep an outline.
theme=gr.themes.Soft(
primary_hue="indigo",
font=(
gr.themes.Font("Alkatra"),
gr.themes.Font("ui-sans-serif"),
gr.themes.Font("system-ui"),
gr.themes.Font("sans-serif"),
),
).set(
block_background_fill="transparent",
block_shadow="none",
block_title_background_fill="none",
block_title_padding="0",
block_title_text_color="*neutral_500",
block_title_text_weight="500",
),
css=font_css()
+ gameview.HIDE_CSS
+ gameview.STORY_CSS
+ gameview.BG_CSS
+ creatorview.CREATOR_CSS
+ NAV_CSS
+ FOOTER_CSS,
) as demo:
gr.Markdown("# Read the Room", elem_classes="rtr-title")
with gr.Tabs() as tabs:
for scen in REGISTRY.values():
gameview.build_game(scen)
comm_play, comm_inputs, comm_share = communityview.build_community()
play_btn, form = creatorview.build_creator()
with gr.Tab("â–¶ Play your scenario", id=PLAY_TAB_ID):
with gr.Row():
load_play = creatorview.load_button()
gr.HTML("") # spacer keeps the button compact, left-aligned
_scen_state, play_outs = gameview.build_game_ui()
parse_state = gr.State()
play_btn.click(creatorview.parse_or_raise, form, parse_state).success(
do_load, parse_state, [tabs, *play_outs]
)
comm_play.click(communityview.play_or_raise, comm_inputs, parse_state).success(
do_load, parse_state, [tabs, *play_outs]
)
comm_share.click(lambda: gr.update(selected=creatorview.CREATOR_TAB_ID), None, tabs)
load_play.upload(load_scenario, load_play, play_outs)
demo.load(warm_model)
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
)