"""
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'''
'''
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'''
'''
elif state == "sad":
# Sad eyes with tears
return f'''
'''
elif state == "thinking":
# Looking up/to side eyes
return f'''
'''
elif state == "concerned":
# Worried eyes
return f'''
'''
elif state == "sleepy":
# Half-closed sleepy eyes
return f'''
'''
elif state in ["listening", "attentive"]:
# Big sparkly attentive eyes
return f'''
'''
elif state == "speaking":
# Animated speaking eyes
return f'''
'''
else: # neutral
# Normal cute eyes with sparkle
return f'''
'''
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''
elif state == "excited":
# Open excited smile
return f'''
'''
elif state == "sad":
# Sad frown
return f''
elif state == "thinking":
# Small 'o' thinking mouth
return f''
elif state == "concerned":
# Wavy worried mouth
return f''
elif state == "sleepy":
# Small relaxed smile
return f''
elif state in ["listening", "attentive"]:
# Small attentive 'o'
return f''
elif state == "speaking":
# Animated speaking mouth
return f''
else: # neutral
# Gentle smile
return f''
def _get_cute_extras(state: PipState, colors: dict) -> str:
"""Generate extra cute decorations based on emotional state."""
if state == "excited":
# Cute sparkles
return '''
'''
elif state == "sad":
# Tear drops
return '''
'''
elif state == "thinking":
# Thought bubbles
return '''
'''
elif state == "concerned":
# Sweat drop
return '''
'''
elif state == "sleepy":
# Z's floating
return '''
Z
z
z
'''
elif state in ["listening", "attentive"]:
# Sound/attention waves
return '''
'''
elif state == "happy":
# Small hearts or sparkles
return '''
'''
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 = ''
for state in states:
html += f'''
{get_pip_svg(state, 100)}
{state}
'''
html += '
'
html += 'Built with 💙 for MCP\'s 1st Birthday Hackathon | Powered by Anthropic, ElevenLabs, OpenAI, Gemini, and HuggingFace
'
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")