"""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'
'
)
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)