Spaces:
Sleeping
Sleeping
| """Explainor - AI Agent that explains any topic in fun persona voices. | |
| MCP's 1st Birthday Hackathon Submission | |
| Track: MCP in Action (Creative) | |
| Team: kaiser-data | |
| """ | |
| import os | |
| import tempfile | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| from src.personas import PERSONAS, get_persona_names, get_persona | |
| from src.agent import run_agent | |
| from src.tts import generate_speech | |
| # Load environment variables | |
| load_dotenv() | |
| def format_sources(sources: list[dict]) -> str: | |
| """Format sources as markdown.""" | |
| if not sources: | |
| return "*No external sources used*" | |
| md = "" | |
| for i, src in enumerate(sources, 1): | |
| if src.get("url"): | |
| md += f"{i}. [{src['title']}]({src['url']})\n" | |
| else: | |
| md += f"{i}. {src['title']} ({src.get('source', 'General')})\n" | |
| return md | |
| def explain_topic(topic: str, persona_name: str, progress=gr.Progress()): | |
| """Main function to explain a topic in a persona's voice. | |
| Returns: (explanation_text, audio_path, sources_md, steps_md) | |
| """ | |
| if not topic.strip(): | |
| return ( | |
| "Please enter a topic to explain!", | |
| None, | |
| "", | |
| "β No topic provided", | |
| ) | |
| if not persona_name: | |
| persona_name = "5-Year-Old" | |
| steps_log = [] | |
| explanation = "" | |
| sources = [] | |
| voice_id = None | |
| # Run the agent pipeline | |
| progress(0, desc="Starting...") | |
| for update in run_agent(topic, persona_name): | |
| if update["type"] == "step": | |
| step_text = f"**{update['title']}**\n{update['content']}" | |
| steps_log.append(step_text) | |
| if update["step"] == "research": | |
| progress(0.2, desc="Researching...") | |
| elif update["step"] == "research_done": | |
| progress(0.4, desc="Research complete") | |
| if "sources" in update: | |
| sources = update["sources"] | |
| elif update["step"] == "generating": | |
| progress(0.6, desc="Generating explanation...") | |
| elif update["type"] == "result": | |
| explanation = update["explanation"] | |
| sources = update.get("sources", sources) | |
| voice_id = update["voice_id"] | |
| progress(0.8, desc="Explanation ready!") | |
| # Format the steps log | |
| steps_md = "\n\n---\n\n".join(steps_log) | |
| # Generate audio | |
| audio_path = None | |
| if explanation and voice_id: | |
| progress(0.9, desc="Generating audio...") | |
| try: | |
| audio_bytes = generate_speech(explanation, voice_id) | |
| # Save to temp file for Gradio | |
| with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: | |
| f.write(audio_bytes) | |
| audio_path = f.name | |
| progress(1.0, desc="Done!") | |
| except Exception as e: | |
| steps_log.append(f"**β οΈ Audio generation failed**\n{str(e)}") | |
| steps_md = "\n\n---\n\n".join(steps_log) | |
| progress(1.0, desc="Done (no audio)") | |
| # Format sources | |
| sources_md = format_sources(sources) | |
| return explanation, audio_path, sources_md, steps_md | |
| # Build the Gradio interface | |
| def create_app(): | |
| """Create and configure the Gradio app.""" | |
| # Custom CSS for better styling | |
| css = """ | |
| .gradio-container { | |
| max-width: 900px !important; | |
| margin: auto !important; | |
| } | |
| .persona-dropdown { | |
| font-size: 1.1em !important; | |
| } | |
| #title-row { | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| } | |
| .explanation-box { | |
| font-size: 1.15em; | |
| line-height: 1.6; | |
| padding: 1rem; | |
| background: linear-gradient(135deg, #667eea11 0%, #764ba211 100%); | |
| border-radius: 10px; | |
| } | |
| .steps-accordion { | |
| font-size: 0.95em; | |
| } | |
| """ | |
| with gr.Blocks( | |
| title="Explainor - AI Persona Explanations", | |
| css=css, | |
| theme=gr.themes.Soft( | |
| primary_hue="violet", | |
| secondary_hue="blue", | |
| ), | |
| ) as app: | |
| # Header | |
| gr.Markdown( | |
| """ | |
| # π Explainor | |
| **Learn anything through the voice of your favorite characters!** | |
| Enter any topic and choose a persona. The AI will research your topic, | |
| transform the explanation into that character's unique voice, and read it aloud. | |
| """, | |
| elem_id="title-row", | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| topic_input = gr.Textbox( | |
| label="π What do you want to learn about?", | |
| placeholder="e.g., Blockchain, Photosynthesis, Black Holes...", | |
| lines=1, | |
| max_lines=1, | |
| ) | |
| with gr.Column(scale=1): | |
| # Build persona choices with emojis | |
| persona_choices = [ | |
| f"{PERSONAS[name]['emoji']} {name}" | |
| for name in get_persona_names() | |
| ] | |
| persona_dropdown = gr.Dropdown( | |
| choices=persona_choices, | |
| value=persona_choices[0], | |
| label="π Choose your explainer", | |
| elem_classes=["persona-dropdown"], | |
| ) | |
| explain_btn = gr.Button( | |
| "β¨ Explain it to me!", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| # Output section | |
| with gr.Row(): | |
| with gr.Column(): | |
| explanation_output = gr.Textbox( | |
| label="π Explanation", | |
| lines=8, | |
| max_lines=15, | |
| elem_classes=["explanation-box"], | |
| show_copy_button=True, | |
| ) | |
| audio_output = gr.Audio( | |
| label="π Listen to the explanation", | |
| type="filepath", | |
| autoplay=False, | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| with gr.Accordion("π Sources", open=False): | |
| sources_output = gr.Markdown("") | |
| with gr.Column(): | |
| with gr.Accordion("π§ Agent Reasoning", open=False): | |
| steps_output = gr.Markdown( | |
| "", | |
| elem_classes=["steps-accordion"], | |
| ) | |
| # Example topics | |
| gr.Examples( | |
| examples=[ | |
| ["Quantum Computing", f"{PERSONAS['5-Year-Old']['emoji']} 5-Year-Old"], | |
| ["Blockchain", f"{PERSONAS['Gordon Ramsay']['emoji']} Gordon Ramsay"], | |
| ["Black Holes", f"{PERSONAS['Pirate']['emoji']} Pirate"], | |
| ["Machine Learning", f"{PERSONAS['Shakespeare']['emoji']} Shakespeare"], | |
| ["Climate Change", f"{PERSONAS['Surfer Dude']['emoji']} Surfer Dude"], | |
| ["The Force", f"{PERSONAS['Yoda']['emoji']} Yoda"], | |
| ], | |
| inputs=[topic_input, persona_dropdown], | |
| label="Try these examples:", | |
| ) | |
| # Footer | |
| gr.Markdown( | |
| """ | |
| --- | |
| **Built for MCP's 1st Birthday Hackathon** | Track: MCP in Action (Creative) | |
| Powered by: [Nebius AI](https://nebius.com) (LLM) + [ElevenLabs](https://elevenlabs.io) (TTS) | |
| Made with β€οΈ by **kaiser-data** | |
| """, | |
| elem_id="footer", | |
| ) | |
| # Event handler | |
| def process_and_explain(topic, persona_with_emoji): | |
| # Extract persona name (remove emoji prefix) | |
| persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji | |
| return explain_topic(topic, persona_name) | |
| explain_btn.click( | |
| fn=process_and_explain, | |
| inputs=[topic_input, persona_dropdown], | |
| outputs=[explanation_output, audio_output, sources_output, steps_output], | |
| ) | |
| # Also trigger on Enter key in topic input | |
| topic_input.submit( | |
| fn=process_and_explain, | |
| inputs=[topic_input, persona_dropdown], | |
| outputs=[explanation_output, audio_output, sources_output, steps_output], | |
| ) | |
| return app | |
| # Create the app | |
| app = create_app() | |
| if __name__ == "__main__": | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| ) | |