Spaces:
Running
Running
| """ | |
| Pip - Your Emotional AI Companion | |
| A Gradio app with MCP server for emotional support and creative expression. | |
| """ | |
| import gradio as gr | |
| import asyncio | |
| import base64 | |
| import os | |
| import uuid | |
| import tempfile | |
| import httpx | |
| from typing import Optional | |
| from dotenv import load_dotenv | |
| # Load environment variables | |
| load_dotenv() | |
| # Enable nested event loops for Gradio + asyncio compatibility | |
| import nest_asyncio | |
| nest_asyncio.apply() | |
| from pip_character import get_pip_svg, get_all_states_preview, PipState | |
| from pip_brain import PipBrain, get_brain, PipResponse | |
| from pip_voice import PipVoice, PipEars | |
| # ============================================================================= | |
| # GLOBAL STATE | |
| # ============================================================================= | |
| brain = get_brain() | |
| voice = PipVoice() | |
| ears = PipEars() | |
| # Gallery storage - stores (image_path, caption) tuples | |
| gallery_images: list[tuple[str, str]] = [] | |
| # ============================================================================= | |
| # CORE FUNCTIONS | |
| # ============================================================================= | |
| async def process_message( | |
| message: str, | |
| history: list, | |
| session_id: str, | |
| mode: str, | |
| generate_voice: bool | |
| ) -> tuple: | |
| """ | |
| Process a user message and return Pip's response. | |
| NOTE: No longer generates images automatically - use Visualize button. | |
| Returns: | |
| (updated_history, pip_svg, audio_data, status) | |
| """ | |
| if not message.strip(): | |
| return history, get_pip_svg("neutral"), None, "Please say something!" | |
| # Set mode | |
| brain.set_mode(session_id, mode.lower() if mode != "Auto" else "auto") | |
| # Initialize history | |
| history = history or [] | |
| # Add user message immediately | |
| history.append({"role": "user", "content": message}) | |
| # Process through brain | |
| response = await brain.process( | |
| user_input=message, | |
| session_id=session_id, | |
| generate_voice=generate_voice | |
| ) | |
| # Add Pip's response (with acknowledgment context) | |
| full_response = response.response_text | |
| history.append({"role": "assistant", "content": full_response}) | |
| # Prepare audio - save to temp file for Gradio | |
| audio_data = None | |
| if response.audio and response.audio.audio_bytes: | |
| with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: | |
| f.write(response.audio.audio_bytes) | |
| audio_data = f.name | |
| # Get Pip SVG for current state | |
| pip_svg = get_pip_svg(response.pip_state) | |
| # Status with emotions | |
| emotions = response.emotion_state.get('primary_emotions', ['neutral']) | |
| action = response.action.get('action', 'reflect') | |
| status = f"π {', '.join(emotions)} | π― {action}" | |
| return history, pip_svg, audio_data, status | |
| async def visualize_mood(session_id: str) -> tuple: | |
| """ | |
| Generate an image based on current conversation context. | |
| Called when user clicks "Visualize" button. | |
| Returns: | |
| (image_data, explanation, pip_svg, status) | |
| """ | |
| global gallery_images | |
| try: | |
| # Generate image using full conversation context | |
| image, explanation = await brain.visualize_current_mood(session_id) | |
| if image and image.image_data: | |
| # Save image to temp file | |
| if image.is_url: | |
| img_response = httpx.get(image.image_data, timeout=30) | |
| if img_response.status_code == 200: | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: | |
| f.write(img_response.content) | |
| image_data = f.name | |
| else: | |
| return None, "", get_pip_svg("confused"), "Couldn't download image" | |
| else: | |
| img_bytes = base64.b64decode(image.image_data) | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: | |
| f.write(img_bytes) | |
| image_data = f.name | |
| # Save to gallery | |
| import datetime | |
| timestamp = datetime.datetime.now().strftime("%I:%M %p") | |
| short_explanation = explanation[:50] + "..." if len(explanation) > 50 else explanation | |
| caption = f"Visualization β’ {timestamp}" | |
| gallery_images.append((image_data, caption)) | |
| print(f"Added to gallery: {caption}") | |
| return image_data, explanation, get_pip_svg("happy"), f"β¨ Created with {image.provider}!" | |
| else: | |
| return None, "", get_pip_svg("confused"), "Couldn't generate image. Try again?" | |
| except Exception as e: | |
| print(f"Visualize error: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return None, "", get_pip_svg("confused"), f"Error: {str(e)[:50]}" | |
| def visualize_mood_sync(session_id): | |
| """Synchronous wrapper for visualize_mood.""" | |
| try: | |
| loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| return loop.run_until_complete(visualize_mood(session_id)) | |
| def process_message_sync(message, history, session_id, mode, generate_voice): | |
| """Synchronous wrapper for async process_message.""" | |
| try: | |
| loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| # Returns: (history, pip_svg, audio_data, status) - NO image | |
| return loop.run_until_complete(process_message(message, history, session_id, mode, generate_voice)) | |
| async def process_voice_input(audio_data, history, session_id, mode): | |
| """ | |
| Process voice input - transcribe and respond. | |
| """ | |
| if audio_data is None: | |
| return history, get_pip_svg("neutral"), None, None, "No audio received" | |
| try: | |
| # Transcribe audio | |
| sample_rate, audio_array = audio_data | |
| # Convert to bytes for Whisper | |
| import io | |
| import soundfile as sf | |
| import numpy as np | |
| # Handle different audio formats | |
| if len(audio_array.shape) > 1: | |
| # Stereo to mono | |
| audio_array = audio_array.mean(axis=1) | |
| # Normalize audio to float32 | |
| if audio_array.dtype == np.int16: | |
| audio_array = audio_array.astype(np.float32) / 32768.0 | |
| elif audio_array.dtype == np.int32: | |
| audio_array = audio_array.astype(np.float32) / 2147483648.0 | |
| elif audio_array.dtype != np.float32: | |
| audio_array = audio_array.astype(np.float32) | |
| # Ensure values are in valid range | |
| audio_array = np.clip(audio_array, -1.0, 1.0) | |
| # Write to bytes buffer as WAV | |
| buffer = io.BytesIO() | |
| sf.write(buffer, audio_array, sample_rate, format='WAV', subtype='PCM_16') | |
| buffer.seek(0) # Reset buffer position to start | |
| audio_bytes = buffer.getvalue() | |
| print(f"Voice input: {len(audio_bytes)} bytes, sample rate: {sample_rate}") | |
| # Transcribe | |
| transcription = await ears.listen_bytes(audio_bytes) | |
| if not transcription: | |
| return history, get_pip_svg("confused"), None, "Couldn't understand audio. Try speaking clearly." | |
| print(f"Transcription: {transcription}") | |
| # Process the transcribed text (no image - returns: history, pip_svg, audio, status) | |
| return await process_message(transcription, history, session_id, mode, True) | |
| except Exception as e: | |
| print(f"Voice processing error: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return history, get_pip_svg("confused"), None, f"Voice processing error: {str(e)[:100]}" | |
| def process_voice_sync(audio_data, history, session_id, mode): | |
| """Synchronous wrapper for voice processing.""" | |
| try: | |
| loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| return loop.run_until_complete(process_voice_input(audio_data, history, session_id, mode)) | |
| def create_session_id(): | |
| """Generate a new session ID.""" | |
| return str(uuid.uuid4())[:8] | |
| async def create_memory(session_id: str, history: list) -> tuple: | |
| """ | |
| Create a memory artifact from the conversation. | |
| Returns: (summary_text, image_data, explanation, audio_data, pip_svg, status) | |
| """ | |
| global gallery_images | |
| if not history: | |
| return "No conversation to summarize yet!", None, "", None, get_pip_svg("neutral"), "Start a conversation first!" | |
| try: | |
| # Get memory summary from brain | |
| result = await brain.summarize_conversation(session_id, generate_voice=True) | |
| # Create explanation from the analysis | |
| analysis = result.get("analysis", {}) | |
| emotions = result.get("emotions_journey", ["reflection"]) | |
| explanation = "" | |
| if analysis: | |
| visual_metaphor = analysis.get("visual_metaphor", "") | |
| if visual_metaphor: | |
| explanation = f"This captures your journey: {visual_metaphor[:100]}..." | |
| else: | |
| explanation = f"A visual embrace of your {', '.join(emotions[:2])} today." | |
| else: | |
| explanation = f"A memory of our conversation, holding your {emotions[0] if emotions else 'feelings'}." | |
| # Prepare image - save to temp file | |
| image_data = None | |
| if result.get("image") and result["image"].image_data: | |
| try: | |
| if result["image"].is_url: | |
| img_response = httpx.get(result["image"].image_data, timeout=30) | |
| if img_response.status_code == 200: | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: | |
| f.write(img_response.content) | |
| image_data = f.name | |
| else: | |
| img_bytes = base64.b64decode(result["image"].image_data) | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: | |
| f.write(img_bytes) | |
| image_data = f.name | |
| except Exception as e: | |
| print(f"Error processing memory image: {e}") | |
| image_data = None | |
| # Save to gallery if we have an image | |
| if image_data: | |
| import datetime | |
| timestamp = datetime.datetime.now().strftime("%I:%M %p") | |
| caption = f"Memory β’ {timestamp} β’ {', '.join(emotions[:2])}" | |
| gallery_images.append((image_data, caption)) | |
| print(f"Added to gallery: {caption}") | |
| # Prepare audio | |
| audio_data = None | |
| if result.get("audio") and result["audio"].audio_bytes: | |
| with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: | |
| f.write(result["audio"].audio_bytes) | |
| audio_data = f.name | |
| emotions_str = ", ".join(result.get("emotions_journey", ["reflection"])) | |
| status = f"β¨ Memory created! Emotions: {emotions_str}" | |
| # Return: summary, image, explanation, audio, pip_svg, status | |
| return result.get("summary", ""), image_data, explanation, audio_data, get_pip_svg("happy"), status | |
| except Exception as e: | |
| print(f"Error creating memory: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return "Something went wrong creating your memory.", None, "", None, get_pip_svg("concerned"), f"Error: {str(e)[:50]}" | |
| def create_memory_sync(session_id, history): | |
| """Synchronous wrapper for create_memory.""" | |
| try: | |
| loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| return loop.run_until_complete(create_memory(session_id, history)) | |
| def clear_conversation(session_id): | |
| """Clear conversation history.""" | |
| brain.clear_history(session_id) | |
| # Returns: chatbot, pip_svg, mood_image, image_explanation, audio_output, memory_summary visibility, status | |
| return [], get_pip_svg("neutral"), None, gr.update(visible=False), None, gr.update(visible=False), "Ready to listen..." | |
| def update_pip_state(state: str): | |
| """Update Pip's visual state.""" | |
| return get_pip_svg(state) | |
| def get_gallery_images(): | |
| """Get all images in the gallery.""" | |
| global gallery_images | |
| if not gallery_images: | |
| return [] | |
| # Return list of (image_path, caption) for Gradio Gallery | |
| return [(img, cap) for img, cap in gallery_images if img] | |
| def refresh_gallery(): | |
| """Refresh the gallery display.""" | |
| return get_gallery_images() | |
| # ============================================================================= | |
| # MCP TOOLS (Exposed via Gradio MCP Server) | |
| # ============================================================================= | |
| def chat_with_pip(message: str, session_id: str = "mcp_default") -> dict: | |
| """ | |
| Talk to Pip about how you're feeling. | |
| Pip is an emotional companion who understands your feelings | |
| and responds with warmth, images, and optional voice. | |
| Args: | |
| message: What you want to tell Pip | |
| session_id: Optional session ID for conversation continuity | |
| Returns: | |
| Pip's response including text and generated image | |
| """ | |
| try: | |
| loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| response = loop.run_until_complete(brain.process( | |
| user_input=message, | |
| session_id=session_id, | |
| generate_voice=False | |
| )) | |
| return { | |
| "response": response.response_text, | |
| "emotions_detected": response.emotion_state.get("primary_emotions", []), | |
| "action": response.action.get("action", "reflect"), | |
| "pip_state": response.pip_state, | |
| "image_generated": response.image is not None, | |
| "image_prompt": response.image_prompt | |
| } | |
| def generate_mood_artifact(emotion: str, context: str) -> dict: | |
| """ | |
| Generate a visual artifact that captures an emotional state. | |
| Creates an image that represents or responds to the given emotion and context. | |
| Args: | |
| emotion: The primary emotion (happy, sad, anxious, excited, etc.) | |
| context: Additional context about the emotional state | |
| Returns: | |
| Generated image and metadata | |
| """ | |
| from pip_artist import PipArtist | |
| from pip_prompts import PROMPT_ENHANCER_PROMPT | |
| from services.sambanova_client import SambanovaClient | |
| async def _generate(): | |
| sambanova = SambanovaClient() | |
| artist = PipArtist() | |
| emotion_state = { | |
| "primary_emotions": [emotion], | |
| "intensity": 7 | |
| } | |
| # Generate image prompt | |
| image_prompt = await sambanova.enhance_prompt( | |
| context, emotion_state, "alchemist", PROMPT_ENHANCER_PROMPT | |
| ) | |
| # Generate image | |
| image = await artist.generate_for_mood(image_prompt, "warm", "reflect") | |
| return { | |
| "prompt_used": image_prompt, | |
| "provider": image.provider if image else "none", | |
| "image_generated": image.image_data is not None if image else False | |
| } | |
| try: | |
| loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| return loop.run_until_complete(_generate()) | |
| def get_pip_gallery(session_id: str = "mcp_default") -> list: | |
| """ | |
| Get the conversation history with Pip. | |
| Returns the emotional journey of your conversation. | |
| Args: | |
| session_id: Session to retrieve history for | |
| Returns: | |
| List of conversation messages | |
| """ | |
| return brain.get_history(session_id) | |
| def set_pip_mode(mode: str, session_id: str = "mcp_default") -> str: | |
| """ | |
| Set Pip's interaction mode. | |
| Modes: | |
| - auto: Pip decides the best mode based on context | |
| - alchemist: Transforms emotions into magical artifacts | |
| - artist: Creates day summaries as art | |
| - dream: Visualizes thoughts in surreal imagery | |
| - night: Calming companion for late-night moments | |
| Args: | |
| mode: One of auto, alchemist, artist, dream, night | |
| session_id: Session to set mode for | |
| Returns: | |
| Confirmation message | |
| """ | |
| valid_modes = ["auto", "alchemist", "artist", "dream", "night"] | |
| mode_lower = mode.lower() | |
| if mode_lower not in valid_modes: | |
| return f"Invalid mode. Choose from: {', '.join(valid_modes)}" | |
| brain.set_mode(session_id, mode_lower) | |
| return f"Pip is now in {mode} mode" | |
| # ============================================================================= | |
| # GRADIO UI | |
| # ============================================================================= | |
| # Custom CSS for styling | |
| CUSTOM_CSS = """ | |
| /* Force Dark Theme Defaults */ | |
| body, .gradio-container { | |
| background-color: #1a1a2e !important; | |
| color: #e0e0e0 !important; | |
| } | |
| /* Pip avatar container */ | |
| .pip-container { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 200px; | |
| max-height: 250px; | |
| background: linear-gradient(135deg, #1e2a4a 0%, #16213e 100%); | |
| border-radius: 20px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| margin-bottom: 12px; | |
| transition: transform 0.3s ease; | |
| padding: 16px; | |
| border: 1px solid rgba(255,255,255,0.05); | |
| } | |
| .pip-container:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 24px rgba(108, 92, 231, 0.15); | |
| } | |
| .pip-container svg { | |
| max-width: 180px; | |
| max-height: 180px; | |
| filter: drop-shadow(0 0 10px rgba(108, 92, 231, 0.3)); | |
| } | |
| /* Chat container */ | |
| .chatbot-container { | |
| border-radius: 20px !important; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.2) !important; | |
| border: 1px solid rgba(255,255,255,0.08) !important; | |
| background: #16213e !important; | |
| } | |
| /* Mood image */ | |
| .mood-image { | |
| border-radius: 16px !important; | |
| box-shadow: 0 4px 16px rgba(0,0,0,0.2) !important; | |
| overflow: hidden; | |
| transition: transform 0.3s ease; | |
| border: 1px solid rgba(255,255,255,0.05); | |
| background-color: #16213e; | |
| } | |
| .mood-image:hover { | |
| transform: scale(1.01); | |
| } | |
| /* Image explanation */ | |
| .image-explanation { | |
| text-align: center; | |
| font-style: italic; | |
| color: #b0b0b0; | |
| font-size: 0.9em; | |
| padding: 10px 14px; | |
| margin-top: 8px; | |
| background: linear-gradient(135deg, rgba(108, 92, 231, 0.12) 0%, rgba(168, 230, 207, 0.12) 100%); | |
| border-radius: 10px; | |
| border-left: 3px solid #6c5ce7; | |
| } | |
| /* Status bar */ | |
| .status-bar { | |
| font-size: 0.85em; | |
| color: #b0b0b0; | |
| padding: 10px 14px; | |
| background: #1e2a4a; | |
| border-radius: 12px; | |
| border: 1px solid #2d3a5a; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.1); | |
| } | |
| /* Voice Toggle */ | |
| .voice-toggle { | |
| background: rgba(108, 92, 231, 0.1); | |
| padding: 8px 12px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(108, 92, 231, 0.2); | |
| margin-bottom: 10px; | |
| } | |
| /* Header */ | |
| .header-title { | |
| text-align: center; | |
| margin-bottom: 4px; | |
| font-size: 2.2em !important; | |
| font-weight: 800 !important; | |
| background: linear-gradient(135deg, #6c5ce7, #a8e6cf); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-shadow: 0 0 30px rgba(108, 92, 231, 0.3); | |
| } | |
| .header-subtitle { | |
| text-align: center; | |
| color: #888; | |
| font-size: 1.1em; | |
| margin-top: 0; | |
| margin-bottom: 20px; | |
| font-weight: 300; | |
| } | |
| /* Buttons */ | |
| button.primary { | |
| background: linear-gradient(135deg, #6c5ce7 0%, #a8e6cf 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-weight: 600 !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| button.primary:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(108, 92, 231, 0.3) !important; | |
| } | |
| /* Footer */ | |
| .footer { | |
| text-align: center; | |
| margin-top: 40px; | |
| color: #555; | |
| font-size: 0.8em; | |
| } | |
| """ | |
| # Build the Gradio app | |
| demo = gr.Blocks() | |
| with demo: | |
| # Inject CSS and force dark mode | |
| gr.HTML(f""" | |
| <style>{CUSTOM_CSS}</style> | |
| <script> | |
| // Force dark theme | |
| document.body.classList.add('dark'); | |
| const url = new URL(window.location); | |
| if (url.searchParams.get('__theme') !== 'dark') {{ | |
| url.searchParams.set('__theme', 'dark'); | |
| window.location.replace(url); | |
| }} | |
| </script> | |
| """) | |
| # Session state | |
| session_id = gr.State(create_session_id) | |
| # Header | |
| gr.Markdown("# π«§ Pip", elem_classes=["header-title"]) | |
| gr.Markdown("*Your emotional AI companion*", elem_classes=["header-subtitle"]) | |
| with gr.Tabs(): | |
| # ================================================================= | |
| # MAIN CHAT TAB | |
| # ================================================================= | |
| with gr.Tab("Chat with Pip"): | |
| with gr.Row(equal_height=True): | |
| # Left column - Pip and Controls (40%) | |
| with gr.Column(scale=2, min_width=350): | |
| # Pip Avatar | |
| pip_display = gr.HTML( | |
| get_pip_svg("neutral"), | |
| label="Pip", | |
| elem_classes=["pip-container"] | |
| ) | |
| # Status | |
| status_display = gr.Textbox( | |
| value="Ready to listen...", | |
| label="Current Vibe", | |
| interactive=False, | |
| elem_classes=["status-bar"], | |
| show_label=True | |
| ) | |
| # Voice Toggle (Visible now!) | |
| voice_toggle = gr.Checkbox( | |
| value=False, | |
| label="π£οΈ Enable Voice Response", | |
| info="Pip will speak back to you", | |
| elem_classes=["voice-toggle"] | |
| ) | |
| # Mood Image (moved up - more prominent) | |
| mood_image = gr.Image( | |
| label="Pip's Visualization", | |
| type="filepath", | |
| elem_classes=["mood-image"], | |
| show_label=True, | |
| interactive=False, | |
| height=250 | |
| ) | |
| # Image Explanation - Why this image? | |
| image_explanation = gr.Markdown( | |
| value="", | |
| visible=False, | |
| elem_classes=["image-explanation"] | |
| ) | |
| # Controls Group (moved below image) | |
| with gr.Accordion("βοΈ Advanced Settings", open=False): | |
| mode_selector = gr.Radio( | |
| ["Auto", "Alchemist", "Artist", "Dream", "Night"], | |
| value="Auto", | |
| label="Interaction Mode", | |
| info="How should Pip visualize your feelings?" | |
| ) | |
| # Audio Output | |
| audio_output = gr.Audio( | |
| label="Pip's Voice", | |
| autoplay=True, | |
| visible=False | |
| ) | |
| # Right column - Conversation (60%) | |
| with gr.Column(scale=3): | |
| chatbot = gr.Chatbot( | |
| label="Conversation", | |
| height=450, | |
| elem_classes=["chatbot-container"], | |
| avatar_images=(None, "https://api.dicebear.com/7.x/bottts/svg?seed=Pip&backgroundColor=transparent") | |
| ) | |
| with gr.Group(): | |
| with gr.Row(): | |
| msg_input = gr.Textbox( | |
| placeholder="How are you feeling today?", | |
| label="Your Message", | |
| scale=8, | |
| lines=1, | |
| max_lines=4, | |
| autofocus=True | |
| ) | |
| send_btn = gr.Button("Send", variant="primary", scale=1, min_width=100) | |
| with gr.Row(): | |
| audio_input = gr.Audio( | |
| label="Voice Input", | |
| sources=["microphone"], | |
| type="numpy", | |
| show_label=False, | |
| container=False | |
| ) | |
| voice_send_btn = gr.Button("π€ Send Voice", variant="secondary") | |
| # Action Buttons - Three rows for different actions | |
| with gr.Row(): | |
| visualize_btn = gr.Button("π¨ Visualize", variant="secondary", scale=1) | |
| memory_btn = gr.Button("β¨ Create Memory", variant="primary", scale=2) | |
| clear_btn = gr.Button("ποΈ Clear", variant="stop", scale=1) | |
| # Memory Summary | |
| memory_summary = gr.Textbox( | |
| label="β¨ Memory Summary", | |
| visible=False, | |
| lines=3, | |
| interactive=False, | |
| elem_classes=["status-bar"] | |
| ) | |
| # Event handlers | |
| # Send message - NO image generated (returns: history, pip_svg, audio, status) | |
| send_btn.click( | |
| fn=process_message_sync, | |
| inputs=[msg_input, chatbot, session_id, mode_selector, voice_toggle], | |
| outputs=[chatbot, pip_display, audio_output, status_display] | |
| ).then( | |
| fn=lambda: "", | |
| outputs=[msg_input] | |
| ) | |
| msg_input.submit( | |
| fn=process_message_sync, | |
| inputs=[msg_input, chatbot, session_id, mode_selector, voice_toggle], | |
| outputs=[chatbot, pip_display, audio_output, status_display] | |
| ).then( | |
| fn=lambda: "", | |
| outputs=[msg_input] | |
| ) | |
| # Voice input - also no auto image | |
| voice_send_btn.click( | |
| fn=process_voice_sync, | |
| inputs=[audio_input, chatbot, session_id, mode_selector], | |
| outputs=[chatbot, pip_display, audio_output, status_display] | |
| ) | |
| # Clear conversation - use the function defined earlier | |
| clear_btn.click( | |
| fn=clear_conversation, | |
| inputs=[session_id], | |
| outputs=[chatbot, pip_display, mood_image, image_explanation, audio_output, memory_summary, status_display] | |
| ) | |
| # Visualize button - generates image based on conversation context | |
| def visualize_wrapper(session_id): | |
| image, explanation, pip_svg, status = visualize_mood_sync(session_id) | |
| print(f"[DEBUG] Visualize - explanation: '{explanation}' (len={len(explanation) if explanation else 0})") | |
| # Show explanation as markdown | |
| if explanation and len(explanation.strip()) > 0: | |
| formatted_explanation = f'*"{explanation}"*' | |
| print(f"[DEBUG] Formatted: {formatted_explanation}") | |
| return image, gr.update(value=formatted_explanation, visible=True), pip_svg, status | |
| print("[DEBUG] No explanation - hiding") | |
| return image, gr.update(value="", visible=False), pip_svg, status | |
| visualize_btn.click( | |
| fn=visualize_wrapper, | |
| inputs=[session_id], | |
| outputs=[mood_image, image_explanation, pip_display, status_display] | |
| ) | |
| # Memory button - creates summary with image + audio + explanation | |
| def create_memory_wrapper(session_id, history): | |
| # Returns: summary, image, explanation, audio, pip_svg, status | |
| summary, image, explanation, audio, pip_svg, status = create_memory_sync(session_id, history) | |
| print(f"[DEBUG] Memory - explanation: '{explanation}'") | |
| # Format explanation as italic markdown | |
| if explanation and len(explanation.strip()) > 0: | |
| formatted_explanation = f'*"{explanation}"*' | |
| explanation_update = gr.update(value=formatted_explanation, visible=True) | |
| else: | |
| explanation_update = gr.update(value="", visible=False) | |
| return ( | |
| gr.update(value=summary, visible=True), # memory_summary | |
| image, # mood_image | |
| explanation_update, # image_explanation | |
| audio, # audio_output | |
| gr.update(visible=True if audio else False), # audio visibility | |
| pip_svg, # pip_display | |
| status # status_display | |
| ) | |
| memory_btn.click( | |
| fn=create_memory_wrapper, | |
| inputs=[session_id, chatbot], | |
| outputs=[memory_summary, mood_image, image_explanation, audio_output, audio_output, pip_display, status_display] | |
| ) | |
| voice_toggle.change( | |
| fn=lambda x: gr.update(visible=x), | |
| inputs=[voice_toggle], | |
| outputs=[audio_output] | |
| ) | |
| # ================================================================= | |
| # GALLERY TAB | |
| # ================================================================= | |
| with gr.Tab("Your Gallery") as gallery_tab: | |
| gr.Markdown("### π¨ Your Emotional Artifacts") | |
| gr.Markdown("*Every visualization and memory Pip creates is saved here*") | |
| gallery_display = gr.Gallery( | |
| label="Mood Artifacts", | |
| columns=3, | |
| height="auto", | |
| object_fit="cover", | |
| show_label=False | |
| ) | |
| with gr.Row(): | |
| refresh_gallery_btn = gr.Button("π Refresh Gallery", variant="secondary") | |
| gallery_count = gr.Markdown("*No images yet*") | |
| def refresh_and_count(): | |
| images = get_gallery_images() | |
| count_text = f"*{len(images)} artifact{'s' if len(images) != 1 else ''} in your gallery*" | |
| return images, count_text | |
| refresh_gallery_btn.click( | |
| fn=refresh_and_count, | |
| outputs=[gallery_display, gallery_count] | |
| ) | |
| # Auto-refresh when tab is selected | |
| gallery_tab.select( | |
| fn=refresh_and_count, | |
| outputs=[gallery_display, gallery_count] | |
| ) | |
| # ================================================================= | |
| # PIP STATES PREVIEW | |
| # ================================================================= | |
| with gr.Tab("Meet Pip"): | |
| gr.Markdown("### Pip's Expressions") | |
| gr.Markdown("*Pip has different expressions for different emotions*") | |
| gr.HTML(get_all_states_preview()) | |
| # ================================================================= | |
| # MCP INTEGRATION TAB | |
| # ================================================================= | |
| with gr.Tab("Connect Your AI"): | |
| gr.Markdown("### Use Pip with Your AI Agent") | |
| gr.Markdown(""" | |
| Pip is available as an MCP (Model Context Protocol) server. | |
| Connect your AI agent to Pip and let them chat! | |
| """) | |
| gr.Markdown("#### For clients that support SSE (Cursor, Windsurf, Cline):") | |
| gr.Code( | |
| '''{ | |
| "mcpServers": { | |
| "Pip": { | |
| "url": "https://YOUR-SPACE.hf.space/gradio_api/mcp/" | |
| } | |
| } | |
| }''', | |
| language="json" | |
| ) | |
| gr.Markdown("#### For clients that only support stdio (Claude Desktop):") | |
| gr.Code( | |
| '''{ | |
| "mcpServers": { | |
| "Pip": { | |
| "command": "npx", | |
| "args": [ | |
| "mcp-remote", | |
| "https://YOUR-SPACE.hf.space/gradio_api/mcp/sse", | |
| "--transport", | |
| "sse-only" | |
| ] | |
| } | |
| } | |
| }''', | |
| language="json" | |
| ) | |
| gr.Markdown("#### Available MCP Tools:") | |
| gr.Markdown(""" | |
| - **chat_with_pip**: Talk to Pip about how you're feeling | |
| - **generate_mood_artifact**: Create visual art from emotions | |
| - **get_pip_gallery**: View conversation history | |
| - **set_pip_mode**: Change Pip's interaction mode | |
| """) | |
| # ================================================================= | |
| # SETTINGS TAB - User API Keys | |
| # ================================================================= | |
| with gr.Tab("βοΈ Settings"): | |
| gr.Markdown("### π Use Your Own API Keys") | |
| gr.Markdown(""" | |
| *Want to use your own API credits? Enter your keys below.* | |
| **Privacy:** Keys are stored only in your browser session and never saved on our servers. | |
| **Note:** If you don't provide keys, Pip will use the default (shared) keys when available. | |
| """) | |
| with gr.Group(): | |
| gr.Markdown("#### Primary LLM (Recommended)") | |
| user_google_key = gr.Textbox( | |
| label="Google API Key (Gemini)", | |
| placeholder="AIza...", | |
| type="password", | |
| info="Get from: https://aistudio.google.com/apikey" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("#### Fallback LLM") | |
| user_anthropic_key = gr.Textbox( | |
| label="Anthropic API Key (Claude)", | |
| placeholder="sk-ant-...", | |
| type="password", | |
| info="Get from: https://console.anthropic.com/" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("#### Image Generation") | |
| user_openai_key = gr.Textbox( | |
| label="OpenAI API Key (DALL-E)", | |
| placeholder="sk-...", | |
| type="password", | |
| info="Get from: https://platform.openai.com/api-keys" | |
| ) | |
| user_hf_token = gr.Textbox( | |
| label="HuggingFace Token (Flux)", | |
| placeholder="hf_...", | |
| type="password", | |
| info="Get from: https://huggingface.co/settings/tokens" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("#### Voice") | |
| user_elevenlabs_key = gr.Textbox( | |
| label="ElevenLabs API Key", | |
| placeholder="...", | |
| type="password", | |
| info="Get from: https://elevenlabs.io/app/settings/api-keys" | |
| ) | |
| save_keys_btn = gr.Button("πΎ Save Keys & Restart Pip", variant="primary") | |
| keys_status = gr.Markdown("*Keys not configured - using default*") | |
| def save_user_keys(google_key, anthropic_key, openai_key, hf_token, elevenlabs_key, session_id): | |
| """Save user API keys and reinitialize brain.""" | |
| global brain | |
| # Store keys in environment for this session | |
| # (In production, you'd want proper session management) | |
| if google_key: | |
| os.environ["GOOGLE_API_KEY"] = google_key | |
| if anthropic_key: | |
| os.environ["ANTHROPIC_API_KEY"] = anthropic_key | |
| if openai_key: | |
| os.environ["OPENAI_API_KEY"] = openai_key | |
| if hf_token: | |
| os.environ["HUGGINGFACE_TOKEN"] = hf_token | |
| if elevenlabs_key: | |
| os.environ["ELEVENLABS_API_KEY"] = elevenlabs_key | |
| # Reinitialize brain with new keys | |
| from pip_brain import PipBrain, UserAPIKeys | |
| user_keys = UserAPIKeys( | |
| google_api_key=google_key if google_key else None, | |
| anthropic_api_key=anthropic_key if anthropic_key else None, | |
| openai_api_key=openai_key if openai_key else None, | |
| huggingface_token=hf_token if hf_token else None, | |
| elevenlabs_api_key=elevenlabs_key if elevenlabs_key else None | |
| ) | |
| brain = PipBrain(user_keys=user_keys) | |
| # Build status message | |
| configured = [] | |
| if google_key: | |
| configured.append("β Google/Gemini") | |
| if anthropic_key: | |
| configured.append("β Anthropic/Claude") | |
| if openai_key: | |
| configured.append("β OpenAI/DALL-E") | |
| if hf_token: | |
| configured.append("β HuggingFace/Flux") | |
| if elevenlabs_key: | |
| configured.append("β ElevenLabs") | |
| if configured: | |
| status = f"**Keys saved!** {', '.join(configured)}\n\n*Pip has been reinitialized with your keys.*" | |
| else: | |
| status = "*No keys provided - using default configuration*" | |
| return status | |
| save_keys_btn.click( | |
| fn=save_user_keys, | |
| inputs=[user_google_key, user_anthropic_key, user_openai_key, user_hf_token, user_elevenlabs_key, session_id], | |
| outputs=[keys_status] | |
| ) | |
| # Footer | |
| gr.Markdown("---") | |
| gr.Markdown( | |
| "*Built with π for MCP's 1st Birthday Hackathon | " | |
| "Powered by Gemini, Anthropic, ElevenLabs, OpenAI, and HuggingFace*", | |
| elem_classes=["footer"] | |
| ) | |
| # ============================================================================= | |
| # LAUNCH | |
| # ============================================================================= | |
| if __name__ == "__main__": | |
| demo.launch( | |
| mcp_server=True, # Enable MCP server | |
| share=False, | |
| server_name="0.0.0.0", | |
| server_port=7860 | |
| ) | |