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