pipV1 / pip_character.py
Itsjustamit's picture
files for v1
cd35cc5 verified
"""
Pip Character - Cute animated blob with emotional states.
Kawaii-style SVG character with expressive animations.
"""
from typing import Literal
PipState = Literal[
"neutral", "happy", "sad", "thinking", "concerned",
"excited", "sleepy", "listening", "attentive", "speaking"
]
# Cute pastel color palettes for different emotional states
COLORS = {
"neutral": {
"body": "#A8D8EA",
"body_dark": "#7EC8E3",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"happy": {
"body": "#B5EAD7",
"body_dark": "#8FD8B8",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"sad": {
"body": "#C7CEEA",
"body_dark": "#A8B2D8",
"cheek": "#DDA0DD",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"thinking": {
"body": "#E2D1F9",
"body_dark": "#C9B1E8",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"concerned": {
"body": "#FFDAC1",
"body_dark": "#FFB89A",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"excited": {
"body": "#FFEAA7",
"body_dark": "#FFD93D",
"cheek": "#FF9999",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"sleepy": {
"body": "#DCD6F7",
"body_dark": "#C4BBF0",
"cheek": "#E8C5D6",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"listening": {
"body": "#A8E6CF",
"body_dark": "#88D8B0",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"attentive": {
"body": "#95E1D3",
"body_dark": "#75D1C3",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
"speaking": {
"body": "#B5EAD7",
"body_dark": "#8FD8B8",
"cheek": "#FFB5C5",
"highlight": "#FFFFFF",
"eye": "#2C3E50"
},
}
def get_pip_svg(state: PipState = "neutral", size: int = 200) -> str:
"""
Generate cute SVG for Pip in the specified emotional state.
"""
colors = COLORS.get(state, COLORS["neutral"])
# Get components
eyes = _get_cute_eyes(state, colors)
mouth = _get_cute_mouth(state, colors)
extras = _get_cute_extras(state, colors)
animation_class = _get_animation_class(state)
svg = f'''
<div class="pip-container" style="display: flex; justify-content: center; align-items: center; padding: 20px;">
<style>
{_get_css_animations()}
</style>
<svg width="{size}" height="{size}" viewBox="0 0 200 200" class="pip-svg">
<defs>
<!-- Cute gradient for body -->
<radialGradient id="bodyGrad-{state}" cx="35%" cy="25%" r="65%">
<stop offset="0%" style="stop-color:{colors['highlight']};stop-opacity:0.4" />
<stop offset="30%" style="stop-color:{colors['body']};stop-opacity:1" />
<stop offset="100%" style="stop-color:{colors['body_dark']};stop-opacity:1" />
</radialGradient>
<!-- Soft shadow -->
<filter id="softShadow-{state}" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="{colors['body_dark']}" flood-opacity="0.3"/>
</filter>
<!-- Glow for happy states -->
<filter id="glow-{state}">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Eye sparkle gradient -->
<radialGradient id="eyeGrad-{state}" cx="30%" cy="30%" r="70%">
<stop offset="0%" style="stop-color:#FFFFFF;stop-opacity:0.9" />
<stop offset="100%" style="stop-color:#FFFFFF;stop-opacity:0" />
</radialGradient>
</defs>
<!-- Main blob body - organic shape -->
<g class="pip-body {animation_class}">
<ellipse
cx="100"
cy="108"
rx="68"
ry="58"
fill="url(#bodyGrad-{state})"
filter="url(#softShadow-{state})"
/>
<!-- Highlight shine on body -->
<ellipse
cx="75"
cy="85"
rx="20"
ry="12"
fill="{colors['highlight']}"
opacity="0.35"
/>
</g>
<!-- Cute rosy cheeks -->
<ellipse cx="52" cy="115" rx="15" ry="10" fill="{colors['cheek']}" opacity="0.5" class="cheek-left"/>
<ellipse cx="148" cy="115" rx="15" ry="10" fill="{colors['cheek']}" opacity="0.5" class="cheek-right"/>
<!-- Eyes -->
{eyes}
<!-- Mouth -->
{mouth}
<!-- Extras (sparkles, effects, etc.) -->
{extras}
</svg>
</div>
'''
return svg
def _get_cute_eyes(state: PipState, colors: dict) -> str:
"""Generate kawaii-style eyes based on emotional state."""
eye_color = colors['eye']
if state in ["happy", "excited"]:
# Happy curved eyes (^_^) - kawaii style
return f'''
<!-- Left happy eye -->
<path d="M 65 95 Q 75 80 85 95" stroke="{eye_color}" stroke-width="4" fill="none" stroke-linecap="round"/>
<!-- Right happy eye -->
<path d="M 115 95 Q 125 80 135 95" stroke="{eye_color}" stroke-width="4" fill="none" stroke-linecap="round"/>
'''
elif state == "sad":
# Sad eyes with tears
return f'''
<!-- Left sad eye -->
<ellipse cx="75" cy="92" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="78" cy="87" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="79" cy="86" rx="2" ry="2.5" fill="white"/>
<!-- Right sad eye -->
<ellipse cx="125" cy="92" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="128" cy="87" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="129" cy="86" rx="2" ry="2.5" fill="white"/>
<!-- Sad eyebrows -->
<path d="M 58 78 Q 70 82 88 82" stroke="{eye_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<path d="M 142 78 Q 130 82 112 82" stroke="{eye_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
'''
elif state == "thinking":
# Looking up/to side eyes
return f'''
<!-- Left thinking eye -->
<ellipse cx="75" cy="90" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="72" cy="84" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="71" cy="83" rx="2" ry="2.5" fill="white"/>
<!-- Right thinking eye -->
<ellipse cx="125" cy="90" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="122" cy="84" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="121" cy="83" rx="2" ry="2.5" fill="white"/>
'''
elif state == "concerned":
# Worried eyes
return f'''
<!-- Left worried eye -->
<ellipse cx="75" cy="95" rx="11" ry="13" fill="{eye_color}"/>
<ellipse cx="77" cy="90" rx="4" ry="5" fill="white" opacity="0.9"/>
<ellipse cx="78" cy="89" rx="1.5" ry="2" fill="white"/>
<!-- Right worried eye -->
<ellipse cx="125" cy="95" rx="11" ry="13" fill="{eye_color}"/>
<ellipse cx="127" cy="90" rx="4" ry="5" fill="white" opacity="0.9"/>
<ellipse cx="128" cy="89" rx="1.5" ry="2" fill="white"/>
<!-- Worried eyebrows -->
<path d="M 60 82 Q 68 78 88 88" stroke="{eye_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<path d="M 140 82 Q 132 78 112 88" stroke="{eye_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
'''
elif state == "sleepy":
# Half-closed sleepy eyes
return f'''
<!-- Left sleepy eye -->
<path d="M 63 95 Q 75 102 87 95" stroke="{eye_color}" stroke-width="4" fill="none" stroke-linecap="round"/>
<!-- Right sleepy eye -->
<path d="M 113 95 Q 125 102 137 95" stroke="{eye_color}" stroke-width="4" fill="none" stroke-linecap="round"/>
'''
elif state in ["listening", "attentive"]:
# Big sparkly attentive eyes
return f'''
<!-- Left big eye -->
<ellipse cx="75" cy="93" rx="14" ry="16" fill="{eye_color}" class="eye-blink"/>
<ellipse cx="79" cy="87" rx="6" ry="7" fill="white" opacity="0.95"/>
<ellipse cx="80" cy="86" rx="2.5" ry="3" fill="white"/>
<ellipse cx="70" cy="96" rx="3" ry="3" fill="white" opacity="0.6"/>
<!-- Right big eye -->
<ellipse cx="125" cy="93" rx="14" ry="16" fill="{eye_color}" class="eye-blink"/>
<ellipse cx="129" cy="87" rx="6" ry="7" fill="white" opacity="0.95"/>
<ellipse cx="130" cy="86" rx="2.5" ry="3" fill="white"/>
<ellipse cx="120" cy="96" rx="3" ry="3" fill="white" opacity="0.6"/>
'''
elif state == "speaking":
# Animated speaking eyes
return f'''
<!-- Left speaking eye -->
<ellipse cx="75" cy="93" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="78" cy="88" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="79" cy="87" rx="2" ry="2.5" fill="white"/>
<!-- Right speaking eye -->
<ellipse cx="125" cy="93" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="128" cy="88" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="129" cy="87" rx="2" ry="2.5" fill="white"/>
'''
else: # neutral
# Normal cute eyes with sparkle
return f'''
<!-- Left eye -->
<ellipse cx="75" cy="93" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="78" cy="88" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="79" cy="87" rx="2" ry="2.5" fill="white"/>
<!-- Right eye -->
<ellipse cx="125" cy="93" rx="12" ry="14" fill="{eye_color}"/>
<ellipse cx="128" cy="88" rx="5" ry="6" fill="white" opacity="0.9"/>
<ellipse cx="129" cy="87" rx="2" ry="2.5" fill="white"/>
'''
def _get_cute_mouth(state: PipState, colors: dict) -> str:
"""Generate cute mouth based on emotional state."""
mouth_color = colors['eye']
if state == "happy":
# Big happy smile
return f'<path d="M 82 120 Q 100 140 118 120" stroke="{mouth_color}" stroke-width="3" fill="none" stroke-linecap="round"/>'
elif state == "excited":
# Open excited smile
return f'''
<path d="M 78 118 Q 100 145 122 118" stroke="{mouth_color}" stroke-width="3" fill="#FF9999" stroke-linecap="round"/>
<ellipse cx="100" cy="130" rx="8" ry="3" fill="#FF6B6B" opacity="0.5"/>
'''
elif state == "sad":
# Sad frown
return f'<path d="M 85 130 Q 100 120 115 130" stroke="{mouth_color}" stroke-width="3" fill="none" stroke-linecap="round"/>'
elif state == "thinking":
# Small 'o' thinking mouth
return f'<ellipse cx="100" cy="125" rx="6" ry="5" fill="{mouth_color}" opacity="0.7"/>'
elif state == "concerned":
# Wavy worried mouth
return f'<path d="M 88 125 Q 94 130 100 125 Q 106 120 112 125" stroke="{mouth_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>'
elif state == "sleepy":
# Small relaxed smile
return f'<path d="M 92 122 Q 100 127 108 122" stroke="{mouth_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>'
elif state in ["listening", "attentive"]:
# Small attentive 'o'
return f'<ellipse cx="100" cy="123" rx="5" ry="4" fill="{mouth_color}" opacity="0.6"/>'
elif state == "speaking":
# Animated speaking mouth
return f'<ellipse cx="100" cy="123" rx="10" ry="7" fill="{mouth_color}" class="mouth-animate" opacity="0.8"/>'
else: # neutral
# Gentle smile
return f'<path d="M 90 120 Q 100 128 110 120" stroke="{mouth_color}" stroke-width="2.5" fill="none" stroke-linecap="round"/>'
def _get_cute_extras(state: PipState, colors: dict) -> str:
"""Generate extra cute decorations based on emotional state."""
if state == "excited":
# Cute sparkles
return '''
<g class="sparkles">
<path d="M 40 60 L 42 68 L 50 68 L 44 73 L 46 81 L 40 76 L 34 81 L 36 73 L 30 68 L 38 68 Z" fill="#FFD700" class="sparkle"/>
<path d="M 160 55 L 162 63 L 170 63 L 164 68 L 166 76 L 160 71 L 154 76 L 156 68 L 150 63 L 158 63 Z" fill="#FFD700" class="sparkle" style="animation-delay: 0.2s"/>
<circle cx="45" cy="45" r="3" fill="#FF69B4" class="sparkle" style="animation-delay: 0.4s"/>
<circle cx="155" cy="40" r="3" fill="#FF69B4" class="sparkle" style="animation-delay: 0.1s"/>
</g>
'''
elif state == "sad":
# Tear drops
return '''
<g class="tears">
<path d="M 68 108 Q 65 118 68 123 Q 71 118 68 108" fill="#89CFF0" class="tear" opacity="0.8"/>
</g>
'''
elif state == "thinking":
# Thought bubbles
return '''
<g class="thought-bubbles">
<circle cx="150" cy="65" r="6" fill="#DDD" opacity="0.8"/>
<circle cx="162" cy="50" r="8" fill="#DDD" opacity="0.8"/>
<circle cx="175" cy="32" r="12" fill="#DDD" opacity="0.8"/>
</g>
'''
elif state == "concerned":
# Sweat drop
return '''
<path d="M 145 70 Q 150 82 145 88 Q 140 82 145 70" fill="#89CFF0" opacity="0.7" class="sweat"/>
'''
elif state == "sleepy":
# Z's floating
return '''
<g class="zzz">
<text x="148" y="68" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#9999CC" class="z1">Z</text>
<text x="160" y="52" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#AAAADD" class="z2">z</text>
<text x="168" y="38" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#BBBBEE" class="z3">z</text>
</g>
'''
elif state in ["listening", "attentive"]:
# Sound/attention waves
return '''
<g class="attention-waves" opacity="0.4">
<path d="M 165 90 Q 175 90 175 105 Q 175 120 165 120" stroke="#666" stroke-width="2" fill="none" class="wave1"/>
<path d="M 170 85 Q 185 85 185 105 Q 185 125 170 125" stroke="#666" stroke-width="2" fill="none" class="wave2"/>
</g>
'''
elif state == "happy":
# Small hearts or sparkles
return '''
<g class="happy-sparkles">
<circle cx="50" cy="55" r="2" fill="#FFB5C5"/>
<circle cx="150" cy="50" r="2" fill="#FFB5C5"/>
</g>
'''
return ""
def _get_animation_class(state: PipState) -> str:
"""Get animation class for the blob body."""
animations = {
"neutral": "anim-gentle",
"happy": "anim-bounce",
"sad": "anim-droop",
"thinking": "anim-sway",
"concerned": "anim-shake",
"excited": "anim-excited",
"sleepy": "anim-breathe",
"listening": "anim-pulse",
"attentive": "anim-lean",
"speaking": "anim-speak",
}
return animations.get(state, "anim-gentle")
def _get_css_animations() -> str:
"""Get all CSS animations for Pip."""
return '''
/* Base animations */
@keyframes gentle-wobble {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-3px) rotate(-1deg); }
75% { transform: translateY(-3px) rotate(1deg); }
}
@keyframes happy-bounce {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-10px) scale(1.03); }
}
@keyframes excited-bounce {
0%, 100% { transform: translateY(0) scale(1) rotate(0deg); }
20% { transform: translateY(-12px) scale(1.05) rotate(-4deg); }
40% { transform: translateY(-6px) scale(1.02) rotate(0deg); }
60% { transform: translateY(-12px) scale(1.05) rotate(4deg); }
80% { transform: translateY(-6px) scale(1.02) rotate(0deg); }
}
@keyframes sad-droop {
0%, 100% { transform: translateY(0) scaleY(1); }
50% { transform: translateY(4px) scaleY(0.97); }
}
@keyframes thinking-sway {
0%, 100% { transform: rotate(0deg) translateX(0); }
25% { transform: rotate(-4deg) translateX(-3px); }
75% { transform: rotate(4deg) translateX(3px); }
}
@keyframes worried-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-2px); }
40% { transform: translateX(2px); }
60% { transform: translateX(-2px); }
80% { transform: translateX(2px); }
}
@keyframes sleepy-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
@keyframes listen-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.04); }
}
@keyframes attentive-lean {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-4px) rotate(3deg); }
}
@keyframes speak-pulse {
0%, 100% { transform: scale(1); }
25% { transform: scale(1.02); }
50% { transform: scale(1); }
75% { transform: scale(1.02); }
}
/* Decoration animations */
@keyframes sparkle {
0%, 100% { opacity: 1; transform: scale(1) rotate(0deg); }
50% { opacity: 0.6; transform: scale(1.3) rotate(15deg); }
}
@keyframes tear-fall {
0% { transform: translateY(0); opacity: 0.8; }
100% { transform: translateY(25px); opacity: 0; }
}
@keyframes float-z {
0% { opacity: 0; transform: translateY(0) translateX(0); }
50% { opacity: 1; }
100% { opacity: 0; transform: translateY(-15px) translateX(5px); }
}
@keyframes wave-pulse {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.1); }
}
@keyframes blink {
0%, 90%, 100% { transform: scaleY(1); }
95% { transform: scaleY(0.1); }
}
@keyframes mouth-speak {
0%, 100% { transform: scaleY(1) scaleX(1); }
25% { transform: scaleY(0.6) scaleX(1.1); }
50% { transform: scaleY(1.1) scaleX(0.9); }
75% { transform: scaleY(0.7) scaleX(1.05); }
}
@keyframes sweat-drop {
0%, 100% { transform: translateY(0); opacity: 0.7; }
50% { transform: translateY(3px); opacity: 0.5; }
}
/* Apply animations */
.pip-body.anim-gentle { animation: gentle-wobble 3s ease-in-out infinite; }
.pip-body.anim-bounce { animation: happy-bounce 0.7s ease-in-out infinite; }
.pip-body.anim-excited { animation: excited-bounce 0.5s ease-in-out infinite; }
.pip-body.anim-droop { animation: sad-droop 4s ease-in-out infinite; }
.pip-body.anim-sway { animation: thinking-sway 3s ease-in-out infinite; }
.pip-body.anim-shake { animation: worried-shake 0.4s ease-in-out infinite; }
.pip-body.anim-breathe { animation: sleepy-breathe 4s ease-in-out infinite; }
.pip-body.anim-pulse { animation: listen-pulse 1.5s ease-in-out infinite; }
.pip-body.anim-lean { animation: attentive-lean 2s ease-in-out infinite; }
.pip-body.anim-speak { animation: speak-pulse 0.35s ease-in-out infinite; }
/* Decoration animations */
.sparkle { animation: sparkle 0.8s ease-in-out infinite; }
.tear { animation: tear-fall 2.5s ease-in infinite; }
.z1 { animation: float-z 2s ease-in-out infinite; }
.z2 { animation: float-z 2s ease-in-out infinite 0.4s; }
.z3 { animation: float-z 2s ease-in-out infinite 0.8s; }
.wave1 { animation: wave-pulse 1.2s ease-in-out infinite; }
.wave2 { animation: wave-pulse 1.2s ease-in-out infinite 0.3s; }
.eye-blink { animation: blink 4s ease-in-out infinite; }
.mouth-animate { animation: mouth-speak 0.3s ease-in-out infinite; }
.sweat { animation: sweat-drop 1s ease-in-out infinite; }
/* Cheek hover effect */
.cheek-left, .cheek-right {
transition: opacity 0.3s ease;
}
'''
def get_all_states_preview() -> str:
"""Generate a preview of all Pip states for testing."""
states = ["neutral", "happy", "sad", "thinking", "concerned",
"excited", "sleepy", "listening", "attentive", "speaking"]
html = '<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; padding: 20px; background: #1a1a2e; border-radius: 12px;">'
for state in states:
html += f'''
<div style="text-align: center;">
{get_pip_svg(state, 100)}
<p style="margin-top: 8px; font-size: 12px; color: #888; font-family: sans-serif;">{state}</p>
</div>
'''
html += '</div>'
html += '<p style="text-align: center; margin-top: 16px; color: #666; font-size: 14px;"><em>Built with 💙 for MCP\'s 1st Birthday Hackathon | Powered by Anthropic, ElevenLabs, OpenAI, Gemini, and HuggingFace</em></p>'
return html
# Map emotions to Pip states
EMOTION_TO_STATE = {
"happy": "happy",
"joy": "happy",
"excited": "excited",
"enthusiastic": "excited",
"proud": "happy",
"grateful": "happy",
"hopeful": "happy",
"content": "happy",
"sad": "sad",
"melancholy": "sad",
"grief": "sad",
"lonely": "sad",
"disappointed": "sad",
"anxious": "concerned",
"worried": "concerned",
"nervous": "concerned",
"stressed": "concerned",
"overwhelmed": "concerned",
"confused": "thinking",
"curious": "thinking",
"thoughtful": "thinking",
"uncertain": "thinking",
"tired": "sleepy",
"exhausted": "sleepy",
"peaceful": "sleepy",
"relaxed": "sleepy",
"calm": "neutral",
"neutral": "neutral",
"angry": "concerned",
"frustrated": "concerned",
"love": "excited",
}
def emotion_to_pip_state(emotions: list, intensity: int = 5) -> PipState:
"""
Convert detected emotions to appropriate Pip visual state.
"""
if not emotions:
return "neutral"
# Get the primary emotion
primary = emotions[0].lower()
# Check for high intensity emotions
if intensity >= 8:
if primary in ["happy", "joy", "enthusiastic", "proud", "grateful"]:
return "excited"
elif primary in ["sad", "grief", "despair", "lonely"]:
return "sad"
elif primary in ["anxious", "worried", "scared", "stressed"]:
return "concerned"
return EMOTION_TO_STATE.get(primary, "neutral")