"""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'' f'' ) # eyes eye_w = round(eye_r * 0.9) face_svg += ( f'' f'' f'' f'' ) # mouth as quadratic bezier face_svg += ( f'' ) # 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""" {face_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'
{svg}' f'
{emo}
' f'
{mw}
' f'
' ) def _vadugwi_html(mood: list[int], deltas: list[int] | None, raw: list[int] | None) -> str: parts = ['
'] # "this message read" line if raw: parts.append( '
' 'this message read (raw): ' + ' '.join(f'{DIMS[i]}={raw[i]}' for i in range(7)) + '
' ) 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'' f'{arrow}{abs(delta)}' ) else: delta_html = '' parts.append( f'
' f'
' f'{DIMS[i]} {DIM_LABELS[i]}' f'{val}{delta_html}' f'
' f'
' f'
' f'
' f'
' ) parts.append('
') return ''.join(parts) def _trace_html(trace: dict) -> str: parts = ['
'] parts.append('
words detected:
') # word chips parts.append('
') for w in trace.get("words", []): role = w.get("role", "GAS") bg = ROLE_COLORS.get(role, "#ddd") parts.append( f'{w["word"]}' ) parts.append('
') # structures structs = trace.get("structures", []) if structs: parts.append('
structures:
') parts.append('
') for s in structs: parts.append( f'{s}' ) parts.append('
') # contributors contribs = trace.get("contributors", []) if contribs: parts.append('
top contributors:
') parts.append('
') 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'{dim}{sign}{val}' if not deltas_str: deltas_str = '' parts.append( f'
' f'{word}{deltas_str}
' ) parts.append('
') # unknown unk = trace.get("unknown_tokens", []) if unk: parts.append( f'
didn\'t know: {", ".join(unk)}
' ) parts.append('
') return ''.join(parts) def _why_html(deltas: list[int], acceptance: dict) -> str: parts = ['
'] 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'
' f'biggest move: ' f'{DIMS[max_idx]} ({DIM_LABELS[max_idx]}) {direction} by {abs(max_delta)}' f'
' ) 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 = (' BREACH') if breached else '' parts.append( f'
' f'raw hit {wr} · ' f'armor {armor} · ' f'absorbed {pct}% → ' f'effective {we}{breach_badge}' f'
' ) parts.append('
') 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='
send a message to see the trace
', elem_id="trace-panel", ) gr.Markdown("### why") why_out = gr.HTML( value='
', 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)