clanker-hackathon / gradio_app.py
deucebucket's picture
clanker hackathon gradio entry (verification copy)
ecca2a3 verified
Raw
History Blame Contribute Delete
18 kB
"""Clanker Gradio hackathon app — deterministic emotional creature."""
from __future__ import annotations
import sys
import os
import tempfile
sys.path.insert(0, os.path.dirname(__file__))
import gradio as gr
from app.scorer import score_with_trace
from app.soul_bridge import SoulBridge
from app.appearance import mood_to_appearance
from app.voice import mood_word, emoticon
from app.trace_view import shape_trace
# ── DIMS ─────────────────────────────────────────────────────────────────────
DIMS = ["V", "A", "D", "U", "G", "W", "I"]
DIM_LABELS = ["valence", "arousal", "dominance", "urgency", "gravity", "self-worth", "intent"]
DIM_COLORS = ["#ff9f5a", "#ffd44a", "#8ec5ff", "#c79bff", "#9fd0c9", "#ff7d9c", "#86e0a0"]
ROLE_COLORS = {
"EMOTIONAL": "#ffe08a",
"AMPLIFIER": "#ffc06a",
"NEGATOR": "#ff8080",
"SELF_REF": "#b0e0ff",
"CONNECTOR": "#d0d0d0",
"CHOPPER": "#ffa0c0",
"SOLVENT": "#c0ffc0",
"GAS": "#e0e0e0",
}
# ── SVG CREATURE ─────────────────────────────────────────────────────────────
def creature_svg(appearance: dict) -> str:
h = appearance.get("hue", 28)
s = appearance.get("saturation", 80)
l = appearance.get("lightness", 58)
body_fill = f"hsl({h},{s}%,{l}%)"
dark_l = max(l - 14, 10)
arm_color = f"hsl({h},{s}%,{dark_l}%)"
hand_color = "#d4a574" # beige — always, per spec
aura_opacity = min(1.0, (appearance.get("aura", 0.25)) * 2.4)
face = appearance.get("face", {})
mouth_val = face.get("mouth", 0.0) # -1..+1
eye_val = face.get("eye", 1.0) # openness
brow_val = face.get("brow", 0.0) # -1..+1
sc = appearance.get("scale", 1.0)
lean = appearance.get("lean", 0.0) * 10
dy = appearance.get("droop", 0.0) * 35
blob_transform = f"translate(0 {dy:.1f}) rotate({lean:.1f} 100 116) translate(100 116) scale({sc:.3f}) translate(-100 -116)"
# ── Face elements ─────────────────────────────────────────────────────────
face_color = f"hsl({h},{max(s-30,10)}%,{max(l-28,10)}%)"
eye_r = max(4, min(11, round(7 * eye_val)))
eye_y = 108
eye_lx, eye_rx = 78, 122
brow_tilt_l = -brow_val * 8
brow_tilt_r = brow_val * 8
brow_y = 88
brow_len = 18
mouth_y1 = 158
mouth_cy = mouth_y1 + mouth_val * 18
mouth_x1, mouth_x2 = 76, 124
mouth_cx = 100
# brows
face_svg = (
f'<line x1="{eye_lx - brow_len/2:.1f}" y1="{brow_y + brow_tilt_l:.1f}" '
f'x2="{eye_lx + brow_len/2:.1f}" y2="{brow_y - brow_tilt_l:.1f}" '
f'stroke="{face_color}" stroke-width="3.5" stroke-linecap="round"/>'
f'<line x1="{eye_rx - brow_len/2:.1f}" y1="{brow_y - brow_tilt_r:.1f}" '
f'x2="{eye_rx + brow_len/2:.1f}" y2="{brow_y + brow_tilt_r:.1f}" '
f'stroke="{face_color}" stroke-width="3.5" stroke-linecap="round"/>'
)
# eyes
eye_w = round(eye_r * 0.9)
face_svg += (
f'<ellipse cx="{eye_lx}" cy="{eye_y}" rx="{eye_w}" ry="{eye_r}" fill="{face_color}"/>'
f'<ellipse cx="{eye_rx}" cy="{eye_y}" rx="{eye_w}" ry="{eye_r}" fill="{face_color}"/>'
f'<circle cx="{eye_lx - 2}" cy="{eye_y - 3}" r="2.5" fill="white" opacity="0.7"/>'
f'<circle cx="{eye_rx - 2}" cy="{eye_y - 3}" r="2.5" fill="white" opacity="0.7"/>'
)
# mouth as quadratic bezier
face_svg += (
f'<path d="M{mouth_x1} {mouth_y1:.1f} Q{mouth_cx} {mouth_cy:.1f} {mouth_x2} {mouth_y1:.1f}" '
f'stroke="{face_color}" stroke-width="4" fill="none" stroke-linecap="round"/>'
)
# arms pose
if mouth_val > 0.4:
arm_l = 'M50 148 Q20 110 10 86'
hand_l = 'cx="10" cy="82"'
arm_r = 'M150 148 Q180 110 190 86'
hand_r = 'cx="190" cy="82"'
elif mouth_val < -0.2:
arm_l = 'M50 148 Q34 136 26 122'
hand_l = 'cx="26" cy="118"'
arm_r = 'M150 148 Q166 136 174 122'
hand_r = 'cx="174" cy="118"'
else:
arm_l = 'M50 148 Q28 130 18 112'
hand_l = 'cx="18" cy="108"'
arm_r = 'M150 148 Q172 130 182 112'
hand_r = 'cx="182" cy="108"'
svg = f"""<svg viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet" width="320" height="240"
style="display:block;margin:auto;">
<defs>
<linearGradient id="wall-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffe8b0"/>
<stop offset="60%" stop-color="#f5d08a"/>
<stop offset="100%" stop-color="#ddb060"/>
</linearGradient>
<radialGradient id="light-grad" cx="50%" cy="20%" r="60%">
<stop offset="0%" stop-color="rgba(255,240,180,0.28)"/>
<stop offset="100%" stop-color="rgba(255,240,180,0)"/>
</radialGradient>
<radialGradient id="vignette-grad" cx="50%" cy="50%" r="70%">
<stop offset="50%" stop-color="rgba(60,30,0,0)"/>
<stop offset="100%" stop-color="rgba(60,30,0,0.28)"/>
</radialGradient>
<radialGradient id="aura-grad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="hsl({h},{s}%,{l}%)" stop-opacity="0.6"/>
<stop offset="100%" stop-color="hsl({h},{s}%,{l}%)" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- wall -->
<rect x="0" y="0" width="400" height="192" fill="url(#wall-grad)"/>
<!-- floor -->
<rect x="0" y="192" width="400" height="108" fill="#c9a06a"/>
<!-- baseboard -->
<rect x="0" y="190" width="400" height="4" fill="rgba(100,60,20,0.35)"/>
<!-- aura glow -->
<circle cx="200" cy="160" r="76" fill="url(#aura-grad)" opacity="{aura_opacity:.2f}"
style="mix-blend-mode:screen;"/>
<!-- contact shadow -->
<ellipse cx="200" cy="192" rx="40" ry="6" fill="#00000030"/>
<!-- creature -->
<g transform="translate(160,108) scale(0.40)">
<g transform="{blob_transform}" style="filter:drop-shadow(0 25px 50px rgba(0,0,0,0.28))">
<!-- body -->
<circle cx="100" cy="116" r="74" fill="{body_fill}"/>
<!-- highlight -->
<ellipse cx="82" cy="86" rx="16" ry="10" fill="white" opacity="0.28" transform="rotate(-15,82,86)"/>
<!-- cheeks -->
<ellipse cx="68" cy="136" rx="13" ry="9" fill="#ff9090" opacity="0.38"/>
<ellipse cx="132" cy="136" rx="13" ry="9" fill="#ff9090" opacity="0.38"/>
<!-- left arm -->
<path d="{arm_l}" stroke="{arm_color}" stroke-width="14" stroke-linecap="round" fill="none"/>
<ellipse {hand_l} rx="20" ry="17" fill="{hand_color}"/>
<!-- right arm -->
<path d="{arm_r}" stroke="{arm_color}" stroke-width="14" stroke-linecap="round" fill="none"/>
<ellipse {hand_r} rx="20" ry="17" fill="{hand_color}"/>
<!-- face -->
<g>{face_svg}</g>
</g>
</g>
<!-- warm light overlay -->
<rect x="0" y="0" width="400" height="300" fill="url(#light-grad)" pointer-events="none"/>
<!-- vignette -->
<rect x="0" y="0" width="400" height="300" fill="url(#vignette-grad)" pointer-events="none"/>
</svg>"""
return svg
# ── HTML BUILDERS ─────────────────────────────────────────────────────────────
def _creature_html(appearance: dict, mood_list: list[int]) -> str:
svg = creature_svg(appearance)
emo = emoticon(mood_list)
mw = mood_word(mood_list)
return (
f'<div style="text-align:center;">{svg}'
f'<div style="margin-top:8px;font-size:1.5rem;">{emo}</div>'
f'<div style="color:#8a6f44;font-weight:600;font-size:0.9rem;letter-spacing:0.05em;">{mw}</div>'
f'</div>'
)
def _vadugwi_html(mood: list[int], deltas: list[int] | None, raw: list[int] | None) -> str:
parts = ['<div style="font-family:monospace;font-size:0.82rem;">']
# "this message read" line
if raw:
parts.append(
'<div style="color:#aaa;margin-bottom:6px;">'
'this message read (raw): '
+ ' '.join(f'{DIMS[i]}={raw[i]}' for i in range(7))
+ '</div>'
)
for i in range(7):
val = mood[i] if mood else 128
delta = deltas[i] if deltas else 0
pct = round((val / 255) * 100)
color = DIM_COLORS[i]
if delta and delta != 0:
arrow = '▲' if delta > 0 else '▼'
delta_color = '#4caf50' if delta > 0 else '#f44336'
delta_html = (
f'<span style="color:{delta_color};margin-left:4px;">'
f'{arrow}{abs(delta)}</span>'
)
else:
delta_html = ''
parts.append(
f'<div style="margin-bottom:6px;">'
f'<div style="display:flex;justify-content:space-between;margin-bottom:2px;">'
f'<span style="color:#ccc;">{DIMS[i]} <small style="color:#888;">{DIM_LABELS[i]}</small></span>'
f'<span style="color:#eee;font-weight:600;">{val}{delta_html}</span>'
f'</div>'
f'<div style="background:#2a2a2a;border-radius:4px;height:8px;">'
f'<div style="width:{pct}%;background:{color};height:100%;border-radius:4px;"></div>'
f'</div>'
f'</div>'
)
parts.append('</div>')
return ''.join(parts)
def _trace_html(trace: dict) -> str:
parts = ['<div style="font-family:monospace;font-size:0.80rem;line-height:1.6;">']
parts.append('<div style="color:#aaa;margin-bottom:4px;">words detected:</div>')
# word chips
parts.append('<div style="margin-bottom:10px;display:flex;flex-wrap:wrap;gap:4px;">')
for w in trace.get("words", []):
role = w.get("role", "GAS")
bg = ROLE_COLORS.get(role, "#ddd")
parts.append(
f'<span style="background:{bg};color:#222;border-radius:4px;padding:1px 6px;'
f'font-size:0.77rem;" title="{role}">{w["word"]}</span>'
)
parts.append('</div>')
# structures
structs = trace.get("structures", [])
if structs:
parts.append('<div style="color:#aaa;margin-bottom:2px;">structures:</div>')
parts.append('<div style="margin-bottom:8px;display:flex;flex-wrap:wrap;gap:4px;">')
for s in structs:
parts.append(
f'<span style="background:#3a3060;color:#c8b4ff;border-radius:4px;'
f'padding:1px 7px;font-size:0.77rem;">{s}</span>'
)
parts.append('</div>')
# contributors
contribs = trace.get("contributors", [])
if contribs:
parts.append('<div style="color:#aaa;margin-bottom:2px;">top contributors:</div>')
parts.append('<div style="margin-bottom:8px;">')
for c in contribs:
word = c.get("word", "?")
deltas_str = ''
for key, dim in [('dv','V'),('da','A'),('dd','D'),('du','U'),('dg','G')]:
val = c.get(key, 0)
if val and val != 0:
col = '#4caf50' if val > 0 else '#f44336'
sign = '+' if val > 0 else ''
deltas_str += f'<span style="color:{col};margin-right:3px;">{dim}{sign}{val}</span>'
if not deltas_str:
deltas_str = '<span style="color:#555;">—</span>'
parts.append(
f'<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px;">'
f'<span style="color:#ddd;min-width:90px;">{word}</span>{deltas_str}</div>'
)
parts.append('</div>')
# unknown
unk = trace.get("unknown_tokens", [])
if unk:
parts.append(
f'<div style="color:#888;font-size:0.75rem;">didn\'t know: {", ".join(unk)}</div>'
)
parts.append('</div>')
return ''.join(parts)
def _why_html(deltas: list[int], acceptance: dict) -> str:
parts = ['<div style="font-size:0.85rem;">']
if deltas:
max_idx = max(range(7), key=lambda i: abs(deltas[i]))
max_delta = deltas[max_idx]
if max_delta != 0:
direction = 'up' if max_delta > 0 else 'down'
col = '#4caf50' if max_delta > 0 else '#f44336'
parts.append(
f'<div style="margin-bottom:8px;">'
f'biggest move: <b style="color:{col};">'
f'{DIMS[max_idx]} ({DIM_LABELS[max_idx]}) {direction} by {abs(max_delta)}'
f'</b></div>'
)
if acceptance:
wr = acceptance.get("weight_raw", 0)
we = acceptance.get("weight_effective", 0)
armor = acceptance.get("armor", 0)
pct = acceptance.get("absorbed_pct", 0)
breached = acceptance.get("breached", False)
breach_badge = (' <span style="background:#f44336;color:white;border-radius:4px;'
'padding:0 5px;font-size:0.75rem;">BREACH</span>') if breached else ''
parts.append(
f'<div style="color:#aaa;font-size:0.82rem;">'
f'raw hit <b style="color:#eee;">{wr}</b> · '
f'armor <b style="color:#eee;">{armor}</b> · '
f'absorbed <b style="color:#eee;">{pct}%</b> → '
f'effective <b style="color:#eee;">{we}</b>{breach_badge}'
f'</div>'
)
parts.append('</div>')
return ''.join(parts)
# ── HANDLER ────────────────────────────────────────────────────────────────────
def handler(text: str, state):
# Lazily create bridge on first message
if state is None:
db_dir = tempfile.mkdtemp()
db_path = os.path.join(db_dir, "soul.db")
state = SoulBridge(db_path=db_path)
bridge: SoulBridge = state
score, raw, trace_raw = score_with_trace(text)
bridge.ingest(score)
mood = bridge.mood()
deltas = bridge.deltas()
appearance = mood_to_appearance(mood)
acceptance = bridge.last_acceptance()
shaped_trace = shape_trace(trace_raw)
creature_html = _creature_html(appearance, mood)
vadugwi_html = _vadugwi_html(mood, deltas, raw)
trace_html = _trace_html(shaped_trace)
why_html = _why_html(deltas, acceptance)
return creature_html, vadugwi_html, trace_html, why_html, state
# ── CSS ───────────────────────────────────────────────────────────────────────
CSS = """
body, .gradio-container { font-family: 'Segoe UI', sans-serif; }
.panel-dark {
background: #1a1a2e !important;
border-radius: 12px !important;
padding: 16px !important;
border: 1px solid #2a2a4a !important;
}
.creature-panel {
text-align: center;
padding: 16px;
}
#vadugwi-panel {
background: #181828;
border-radius: 10px;
padding: 14px;
border: 1px solid #2a2a4a;
}
#trace-panel, #why-panel {
background: #181828;
border-radius: 10px;
padding: 14px;
border: 1px solid #2a2a4a;
margin-top: 8px;
}
"""
# ── APP ───────────────────────────────────────────────────────────────────────
_NEUTRAL_MOOD = [128, 128, 0, 128, 128, 128, 128]
_NEUTRAL_APPEARANCE = mood_to_appearance(_NEUTRAL_MOOD)
with gr.Blocks() as demo:
gr.Markdown(
"# 🤗 Clanker — a creature with feelings you can read\n"
"A deterministic, explainable emotional engine. **No model — just readable physics.** "
"Talk to it and watch all 7 emotional dimensions (VADUGWI) move, with the receipts for *why*."
)
bridge_state = gr.State(None)
with gr.Row():
# Left column: creature
with gr.Column(scale=1, min_width=340):
creature_out = gr.HTML(
value=_creature_html(_NEUTRAL_APPEARANCE, _NEUTRAL_MOOD),
label="creature",
elem_classes=["creature-panel"],
)
# Right column: VADUGWI + trace + why
with gr.Column(scale=1, min_width=340):
gr.Markdown("### VADUGWI · live")
vadugwi_out = gr.HTML(
value=_vadugwi_html(_NEUTRAL_MOOD, None, None),
elem_id="vadugwi-panel",
)
gr.Markdown("### what it detected")
trace_out = gr.HTML(
value='<div style="color:#888;font-size:0.85rem;">send a message to see the trace</div>',
elem_id="trace-panel",
)
gr.Markdown("### why")
why_out = gr.HTML(
value='<div style="color:#888;font-size:0.85rem;">—</div>',
elem_id="why-panel",
)
with gr.Row():
with gr.Column():
txt_in = gr.Textbox(
placeholder="say something to it…",
label="",
show_label=False,
scale=4,
)
send_btn = gr.Button("Send", variant="primary", scale=1)
gr.Examples(
examples=[
"you are wonderful, i'm so proud of you",
"shut up, nobody likes you",
"ngl this is kinda mid lowkey",
"i'm terrified and everything is falling apart",
],
inputs=txt_in,
)
outputs = [creature_out, vadugwi_out, trace_out, why_out, bridge_state]
send_btn.click(
fn=handler,
inputs=[txt_in, bridge_state],
outputs=outputs,
)
txt_in.submit(
fn=handler,
inputs=[txt_in, bridge_state],
outputs=outputs,
)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Soft(primary_hue="orange"), css=CSS)