""" 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'''
{eyes} {mouth} {extras}
''' 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")