Emperor555 Claude commited on
Commit
9783711
·
1 Parent(s): 2ce77eb

Rewrite Modal app with inline Gradio for ASGI compatibility

Browse files

Avoid file import issues and SSE streaming problems by defining
the Gradio app inline in the Modal function.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. modal_app.py +108 -6
modal_app.py CHANGED
@@ -31,18 +31,120 @@ image = (
31
  modal.Secret.from_name("elevenlabs-api-key"),
32
  ],
33
  timeout=600,
34
- container_idle_timeout=300,
35
  )
36
- @modal.web_server(port=7860, startup_timeout=120)
37
  def serve():
38
- """Serve the Gradio app via web_server."""
39
- import subprocess
40
  import os
 
41
 
42
  # Disable MCP server on Modal (causes issues)
43
  os.environ["ENABLE_MCP_SERVER"] = "false"
44
- os.chdir("/app")
45
- subprocess.Popen(["python", "app.py"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
 
48
  # For local testing
 
31
  modal.Secret.from_name("elevenlabs-api-key"),
32
  ],
33
  timeout=600,
34
+ scaledown_window=300,
35
  )
36
+ @modal.asgi_app()
37
  def serve():
38
+ """Serve the Gradio app as ASGI."""
39
+ import sys
40
  import os
41
+ sys.path.insert(0, "/app")
42
 
43
  # Disable MCP server on Modal (causes issues)
44
  os.environ["ENABLE_MCP_SERVER"] = "false"
45
+
46
+ import gradio as gr
47
+ from src.personas import PERSONAS, get_persona_names, get_persona
48
+ from src.agent import run_agent
49
+ from src.tts import generate_speech
50
+ import tempfile
51
+
52
+ def format_sources(sources):
53
+ if not sources:
54
+ return "*No external sources used*"
55
+ md = ""
56
+ for i, src in enumerate(sources, 1):
57
+ if src.get("url"):
58
+ md += f"{i}. [{src['title']}]({src['url']})\n"
59
+ else:
60
+ md += f"{i}. {src['title']} ({src.get('source', 'General')})\n"
61
+ return md
62
+
63
+ def format_mcp_tools(tools):
64
+ if not tools:
65
+ return "*No tools used*"
66
+ md = "**Agent Tool Calls:**\n\n"
67
+ for tool in tools:
68
+ md += f"| {tool['icon']} | `{tool['name']}` | {tool['desc']} |\n"
69
+ return md
70
+
71
+ def explain_topic(topic, persona_name, audience=""):
72
+ if not topic.strip():
73
+ return "Please enter a topic!", "", "", ""
74
+ if not persona_name:
75
+ persona_name = "5-Year-Old"
76
+
77
+ steps_log = []
78
+ explanation = ""
79
+ sources = []
80
+ mcp_tools = []
81
+
82
+ for update in run_agent(topic, persona_name, audience):
83
+ if update["type"] == "step":
84
+ steps_log.append(f"**{update['title']}**\n{update['content']}")
85
+ if update["step"] == "research_done" and "sources" in update:
86
+ sources = update["sources"]
87
+ elif update["type"] == "result":
88
+ explanation = update["explanation"]
89
+ sources = update.get("sources", sources)
90
+ mcp_tools = update.get("mcp_tools", [])
91
+
92
+ return explanation, format_sources(sources), "\n\n---\n\n".join(steps_log), format_mcp_tools(mcp_tools)
93
+
94
+ def generate_audio(explanation, persona_name):
95
+ if not explanation or not explanation.strip():
96
+ return None
97
+ if not persona_name:
98
+ persona_name = "5-Year-Old"
99
+ persona = get_persona(persona_name)
100
+ voice_id = persona["voice_id"]
101
+ voice_settings = persona.get("voice_settings")
102
+ audio_bytes = generate_speech(explanation, voice_id, voice_settings)
103
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
104
+ f.write(audio_bytes)
105
+ return f.name
106
+
107
+ with gr.Blocks(title="Explainor - AI Persona Explanations") as demo:
108
+ gr.Markdown("# Explainor\n### Learn anything through the voice of your favorite characters!")
109
+
110
+ with gr.Row():
111
+ topic_input = gr.Textbox(label="What do you want to learn about?", placeholder="e.g., Quantum Computing")
112
+ persona_choices = [f"{PERSONAS[name]['emoji']} {name}" for name in get_persona_names()]
113
+ persona_dropdown = gr.Dropdown(choices=persona_choices, value=persona_choices[0], label="Choose your explainer")
114
+
115
+ audience_dropdown = gr.Dropdown(
116
+ choices=["Just me", "My confused grandmother", "A skeptical robot", "An alien visiting Earth"],
117
+ value="Just me",
118
+ label="Who's listening?"
119
+ )
120
+
121
+ explain_btn = gr.Button("Explain it to me!", variant="primary")
122
+
123
+ explanation_output = gr.Textbox(label="Explanation", lines=6)
124
+ read_aloud_btn = gr.Button("Read Aloud", variant="secondary")
125
+ audio_output = gr.Audio(label="Listen", type="filepath", autoplay=True)
126
+
127
+ with gr.Accordion("Agent Tool Calls", open=False):
128
+ mcp_output = gr.Markdown("")
129
+ with gr.Accordion("Sources", open=False):
130
+ sources_output = gr.Markdown("")
131
+ with gr.Accordion("Execution Trace", open=False):
132
+ steps_output = gr.Markdown("")
133
+
134
+ def process_explain(topic, persona_with_emoji, audience):
135
+ persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji
136
+ aud = "" if "Just me" in audience else audience
137
+ return explain_topic(topic, persona_name, aud)
138
+
139
+ def process_audio(explanation, persona_with_emoji):
140
+ persona_name = persona_with_emoji.split(" ", 1)[1] if " " in persona_with_emoji else persona_with_emoji
141
+ return generate_audio(explanation, persona_name)
142
+
143
+ explain_btn.click(fn=process_explain, inputs=[topic_input, persona_dropdown, audience_dropdown],
144
+ outputs=[explanation_output, sources_output, steps_output, mcp_output])
145
+ read_aloud_btn.click(fn=process_audio, inputs=[explanation_output, persona_dropdown], outputs=[audio_output])
146
+
147
+ return demo.queue()
148
 
149
 
150
  # For local testing