Spaces:
Sleeping
Sleeping
| """Modal deployment configuration for Explainor. | |
| Deploy with: modal deploy modal_app.py | |
| Run locally: modal serve modal_app.py | |
| """ | |
| import modal | |
| # Define the Modal app | |
| app = modal.App("explainor-v6") | |
| # Create image with dependencies | |
| image = ( | |
| modal.Image.debian_slim(python_version="3.11") | |
| .pip_install( | |
| "gradio==4.44.1", # Use older stable version without aggressive SSE | |
| "elevenlabs>=1.0.0", | |
| "httpx>=0.25.0", | |
| "python-dotenv>=1.0.0", | |
| "fastapi", | |
| "uvicorn", | |
| ) | |
| .add_local_dir("src", remote_path="/app/src", copy=True) | |
| ) | |
| def serve(): | |
| """Serve the Gradio app via web_server.""" | |
| import subprocess | |
| import sys | |
| import os | |
| os.chdir("/app") | |
| sys.path.insert(0, "/app") | |
| # Write a standalone gradio script | |
| script = ''' | |
| import sys | |
| sys.path.insert(0, "/app") | |
| import os | |
| import tempfile | |
| import gradio as gr | |
| from src.personas import get_persona_names, get_persona | |
| from src.agent import run_agent | |
| from src.tts import generate_speech | |
| def format_sources(sources): | |
| 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 format_mcp_tools(tools): | |
| if not tools: | |
| return "*No tools used*" | |
| md = "**Agent Tool Calls:**\\n\\n" | |
| for tool in tools: | |
| md += f"| {tool['icon']} | `{tool['name']}` | {tool['desc']} |\\n" | |
| return md | |
| def explain_topic(topic, persona_name, audience=""): | |
| import traceback | |
| if not topic.strip(): | |
| return "Please enter a topic!", "", "", "" | |
| if not persona_name: | |
| persona_name = "5-Year-Old" | |
| # Check API key | |
| nebius_key = os.getenv("NEBIUS_API_KEY") | |
| if not nebius_key: | |
| available_keys = [k for k in os.environ.keys() if "KEY" in k or "API" in k or "NEBIUS" in k] | |
| return f"Error: NEBIUS_API_KEY not found. Available: {available_keys}", "", "", "" | |
| steps_log = [] | |
| explanation = "" | |
| sources = [] | |
| mcp_tools = [] | |
| try: | |
| for update in run_agent(topic, persona_name, audience): | |
| if update["type"] == "step": | |
| steps_log.append(f"**{update['title']}**\\n{update['content']}") | |
| if update["step"] == "research_done" and "sources" in update: | |
| sources = update["sources"] | |
| elif update["type"] == "result": | |
| explanation = update["explanation"] | |
| sources = update.get("sources", sources) | |
| mcp_tools = update.get("mcp_tools", []) | |
| except Exception as e: | |
| return f"Error: {str(e)}\\n\\n{traceback.format_exc()}", "", "\\n\\n---\\n\\n".join(steps_log), "" | |
| return explanation, format_sources(sources), "\\n\\n---\\n\\n".join(steps_log), format_mcp_tools(mcp_tools) | |
| def generate_audio(explanation, persona_name): | |
| if not explanation or not explanation.strip(): | |
| return None | |
| if not persona_name: | |
| persona_name = "5-Year-Old" | |
| try: | |
| persona = get_persona(persona_name) | |
| audio_bytes = generate_speech(explanation, persona["voice_id"], persona.get("voice_settings")) | |
| with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: | |
| f.write(audio_bytes) | |
| return f.name | |
| except Exception as e: | |
| raise gr.Error(f"Audio generation failed: {str(e)}") | |
| # Get persona names as a static list | |
| persona_names = list(get_persona_names()) | |
| with gr.Blocks(title="Explainor") as demo: | |
| gr.Markdown("# Explainor\\n### Learn anything through the voice of your favorite characters!") | |
| with gr.Row(): | |
| topic_input = gr.Textbox(label="Topic", placeholder="e.g., Quantum Computing") | |
| persona_dropdown = gr.Dropdown(choices=persona_names, value="5-Year-Old", label="Persona") | |
| audience_dropdown = gr.Dropdown( | |
| choices=["Just me", "Confused grandmother", "Skeptical robot", "Alien"], | |
| value="Just me", | |
| label="Audience" | |
| ) | |
| explain_btn = gr.Button("Explain!", variant="primary") | |
| explanation_output = gr.Textbox(label="Explanation", lines=6) | |
| read_aloud_btn = gr.Button("Read Aloud") | |
| audio_output = gr.Audio(label="Listen", type="filepath", autoplay=True) | |
| with gr.Accordion("Tools", open=False): | |
| mcp_output = gr.Markdown("") | |
| with gr.Accordion("Sources", open=False): | |
| sources_output = gr.Markdown("") | |
| with gr.Accordion("Trace", open=False): | |
| steps_output = gr.Markdown("") | |
| def do_explain(topic, persona, audience): | |
| aud = "" if "Just me" in audience else audience | |
| return explain_topic(topic, persona, aud) | |
| explain_btn.click( | |
| fn=do_explain, | |
| inputs=[topic_input, persona_dropdown, audience_dropdown], | |
| outputs=[explanation_output, sources_output, steps_output, mcp_output], | |
| ) | |
| read_aloud_btn.click( | |
| fn=generate_audio, | |
| inputs=[explanation_output, persona_dropdown], | |
| outputs=[audio_output], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) | |
| ''' | |
| # Write to file and run | |
| with open("/app/run_gradio.py", "w") as f: | |
| f.write(script) | |
| # Run the script with environment variables | |
| env = os.environ.copy() | |
| subprocess.Popen([sys.executable, "/app/run_gradio.py"], env=env) | |