Update app.py
Browse files
app.py
CHANGED
@@ -1,12 +1,10 @@
|
|
1 |
#!/usr/bin/env python3
|
2 |
"""
|
3 |
Launches a single page Gradio app that:
|
4 |
-
1. Accepts a topic/question.
|
5 |
2. Generates slide markdown + TTS for each slide.
|
6 |
3. Lets the user page through slides and hear the narration.
|
7 |
-
|
8 |
-
Requires: generate_slideshow.py in the same directory and a valid
|
9 |
-
GEMINI_KEY in your environment.
|
10 |
"""
|
11 |
|
12 |
import asyncio
|
@@ -30,6 +28,19 @@ custom_css = """
|
|
30 |
.input-row {
|
31 |
margin-bottom: 10px;
|
32 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
.demo-row {
|
34 |
margin-bottom: 20px;
|
35 |
display: flex;
|
@@ -122,11 +133,14 @@ function animateSlideTransition() {
|
|
122 |
}
|
123 |
"""
|
124 |
|
125 |
-
async def generate_presentation_async(topic: str, session_id=None):
|
126 |
"""Async version: Generate slides, audio, and images for all slides; initialise UI with slide 0."""
|
127 |
topic = (topic or "").strip()
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
130 |
|
131 |
# Create or get a session ID
|
132 |
if session_id is None:
|
@@ -137,8 +151,8 @@ async def generate_presentation_async(topic: str, session_id=None):
|
|
137 |
active_sessions[session_id] = {}
|
138 |
active_sessions[session_id]["temp_dir"] = tempfile.mkdtemp(prefix=f"gradio_session_{session_id}_")
|
139 |
|
140 |
-
# Call the async version with the session ID
|
141 |
-
slides, audio_files, slide_images = await generate_slideshow_with_audio_async(topic, session_id=session_id)
|
142 |
|
143 |
# Basic sanity - keep list lengths aligned for audio
|
144 |
if len(audio_files) < len(slides):
|
@@ -160,10 +174,10 @@ async def generate_presentation_async(topic: str, session_id=None):
|
|
160 |
return slides, audio_files, slide_images, 0, slides[0], audio_files[0], initial_image, progress_text, session_id
|
161 |
|
162 |
|
163 |
-
def generate_presentation(topic: str, session_id=None):
|
164 |
"""Synchronous wrapper for the async presentation generator."""
|
165 |
# Run the async function and handle empty topic case
|
166 |
-
return asyncio.run(generate_presentation_async(topic, session_id=session_id))
|
167 |
|
168 |
|
169 |
def next_slide(slides, audio, images, idx, session_id):
|
@@ -306,10 +320,14 @@ atexit.register(cleanup_all_sessions)
|
|
306 |
dino_audio_cache = {}
|
307 |
dino_image_cache = {}
|
308 |
|
309 |
-
def load_rise_fall_slideshow():
|
310 |
"""Load cached dinosaur slideshow demo and hide the demo button and instruction."""
|
311 |
global dino_audio_cache, dino_image_cache
|
312 |
|
|
|
|
|
|
|
|
|
313 |
# Clear any previous cache
|
314 |
dino_audio_cache = {}
|
315 |
dino_image_cache = {}
|
@@ -420,6 +438,22 @@ with gr.Blocks(
|
|
420 |
)
|
421 |
|
422 |
with gr.Column(elem_classes="container"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
423 |
# First row for topic and generate button - increased horizontal width
|
424 |
with gr.Row(elem_classes="input-row"):
|
425 |
topic_box = gr.Textbox(
|
@@ -460,7 +494,7 @@ with gr.Blocks(
|
|
460 |
session_state = gr.State(None)
|
461 |
|
462 |
# Wiring
|
463 |
-
def prepare_for_generation(topic, session_id):
|
464 |
"""First step: clear the view and prepare for generation"""
|
465 |
# First check if the topic is empty
|
466 |
if not (topic or "").strip():
|
@@ -475,9 +509,34 @@ with gr.Blocks(
|
|
475 |
False # should_generate
|
476 |
)
|
477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
478 |
# Validate the topic using the Gemini Flash input guard
|
479 |
-
|
480 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
481 |
return (
|
482 |
[], [], [], 0, "", None, None, "", session_id,
|
483 |
gr.update(visible=True), # Show topic box
|
@@ -501,7 +560,7 @@ with gr.Blocks(
|
|
501 |
True # should_generate
|
502 |
)
|
503 |
|
504 |
-
def _run_with_new_session(topic, session_id, should_generate):
|
505 |
"""Second step: actually generate the slideshow"""
|
506 |
if not should_generate:
|
507 |
# This case should ideally not be hit if UI updates from prepare_for_generation are correct
|
@@ -516,15 +575,27 @@ with gr.Blocks(
|
|
516 |
False # should_generate (though this output isn't strictly used here, keeping tuple size consistent)
|
517 |
)
|
518 |
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
528 |
|
529 |
# Two-step process for generation
|
530 |
# 1. First clear the UI & validate
|
@@ -532,7 +603,7 @@ with gr.Blocks(
|
|
532 |
|
533 |
gen_btn.click(
|
534 |
prepare_for_generation,
|
535 |
-
inputs=[topic_box, session_state],
|
536 |
outputs=[
|
537 |
slides_state,
|
538 |
audio_state,
|
@@ -552,7 +623,7 @@ with gr.Blocks(
|
|
552 |
]
|
553 |
).then( # 2. Then (conditionally) generate the slideshow
|
554 |
_run_with_new_session,
|
555 |
-
inputs=[topic_box, session_state, should_generate_state],
|
556 |
outputs=[
|
557 |
slides_state,
|
558 |
audio_state,
|
@@ -587,11 +658,11 @@ with gr.Blocks(
|
|
587 |
# Load cached demo slideshow
|
588 |
demo_btn.click(
|
589 |
load_rise_fall_slideshow,
|
590 |
-
inputs=[],
|
591 |
outputs=[slides_state, audio_state, images_state, index_state, slide_markdown, audio_player, title_image,
|
592 |
progress_indicator, session_state, demo_btn, demo_instruction],
|
593 |
)
|
594 |
|
595 |
|
596 |
if __name__ == "__main__":
|
597 |
-
demo.launch()
|
|
|
1 |
#!/usr/bin/env python3
|
2 |
"""
|
3 |
Launches a single page Gradio app that:
|
4 |
+
1. Accepts a topic/question and Gemini API key.
|
5 |
2. Generates slide markdown + TTS for each slide.
|
6 |
3. Lets the user page through slides and hear the narration.
|
7 |
+
Requires: generate_slideshow.py in the same directory
|
|
|
|
|
8 |
"""
|
9 |
|
10 |
import asyncio
|
|
|
28 |
.input-row {
|
29 |
margin-bottom: 10px;
|
30 |
}
|
31 |
+
.api-key-section {
|
32 |
+
margin-bottom: 20px;
|
33 |
+
padding: 15px;
|
34 |
+
border: 1px solid #ddd;
|
35 |
+
border-radius: 8px;
|
36 |
+
background-color: #f9f9f9;
|
37 |
+
}
|
38 |
+
.api-key-note {
|
39 |
+
font-size: 0.9em;
|
40 |
+
color: #666;
|
41 |
+
margin-top: 5px;
|
42 |
+
line-height: 1.5;
|
43 |
+
}
|
44 |
.demo-row {
|
45 |
margin-bottom: 20px;
|
46 |
display: flex;
|
|
|
133 |
}
|
134 |
"""
|
135 |
|
136 |
+
async def generate_presentation_async(topic: str, api_key: str, session_id=None):
|
137 |
"""Async version: Generate slides, audio, and images for all slides; initialise UI with slide 0."""
|
138 |
topic = (topic or "").strip()
|
139 |
+
api_key = (api_key or "").strip()
|
140 |
+
|
141 |
+
# Validate API key
|
142 |
+
if not api_key:
|
143 |
+
raise gr.Error("Please enter your Gemini API key")
|
144 |
|
145 |
# Create or get a session ID
|
146 |
if session_id is None:
|
|
|
151 |
active_sessions[session_id] = {}
|
152 |
active_sessions[session_id]["temp_dir"] = tempfile.mkdtemp(prefix=f"gradio_session_{session_id}_")
|
153 |
|
154 |
+
# Call the async version with the session ID and API key
|
155 |
+
slides, audio_files, slide_images = await generate_slideshow_with_audio_async(topic, api_key, session_id=session_id)
|
156 |
|
157 |
# Basic sanity - keep list lengths aligned for audio
|
158 |
if len(audio_files) < len(slides):
|
|
|
174 |
return slides, audio_files, slide_images, 0, slides[0], audio_files[0], initial_image, progress_text, session_id
|
175 |
|
176 |
|
177 |
+
def generate_presentation(topic: str, api_key: str, session_id=None):
|
178 |
"""Synchronous wrapper for the async presentation generator."""
|
179 |
# Run the async function and handle empty topic case
|
180 |
+
return asyncio.run(generate_presentation_async(topic, api_key, session_id=session_id))
|
181 |
|
182 |
|
183 |
def next_slide(slides, audio, images, idx, session_id):
|
|
|
320 |
dino_audio_cache = {}
|
321 |
dino_image_cache = {}
|
322 |
|
323 |
+
def load_rise_fall_slideshow(api_key):
|
324 |
"""Load cached dinosaur slideshow demo and hide the demo button and instruction."""
|
325 |
global dino_audio_cache, dino_image_cache
|
326 |
|
327 |
+
# API key is required even for demo
|
328 |
+
if not api_key:
|
329 |
+
raise gr.Error("Please enter your Gemini API key first")
|
330 |
+
|
331 |
# Clear any previous cache
|
332 |
dino_audio_cache = {}
|
333 |
dino_image_cache = {}
|
|
|
438 |
)
|
439 |
|
440 |
with gr.Column(elem_classes="container"):
|
441 |
+
# API Key section
|
442 |
+
with gr.Group(elem_classes="api-key-section"):
|
443 |
+
api_key_input = gr.Textbox(
|
444 |
+
label="Gemini API Key",
|
445 |
+
placeholder="Enter your Gemini API key here",
|
446 |
+
type="password",
|
447 |
+
scale=1
|
448 |
+
)
|
449 |
+
gr.HTML(
|
450 |
+
"""<div class="api-key-note">
|
451 |
+
<strong>Note:</strong> You need a Gemini API key with billing enabled to use this app.<br>
|
452 |
+
Get your API key from <a href="https://aistudio.google.com/app/apikey" target="_blank">Google AI Studio</a>.<br>
|
453 |
+
<em>Important: Make sure to enable billing on your Google Cloud account for API access.</em>
|
454 |
+
</div>"""
|
455 |
+
)
|
456 |
+
|
457 |
# First row for topic and generate button - increased horizontal width
|
458 |
with gr.Row(elem_classes="input-row"):
|
459 |
topic_box = gr.Textbox(
|
|
|
494 |
session_state = gr.State(None)
|
495 |
|
496 |
# Wiring
|
497 |
+
def prepare_for_generation(topic, api_key, session_id):
|
498 |
"""First step: clear the view and prepare for generation"""
|
499 |
# First check if the topic is empty
|
500 |
if not (topic or "").strip():
|
|
|
509 |
False # should_generate
|
510 |
)
|
511 |
|
512 |
+
# Check if API key is provided
|
513 |
+
if not (api_key or "").strip():
|
514 |
+
gr.Info("Please enter your Gemini API key.")
|
515 |
+
return (
|
516 |
+
[], [], [], 0, "", None, None, "", session_id,
|
517 |
+
gr.update(visible=True), # Show topic box
|
518 |
+
gr.update(visible=True), # Show generate button
|
519 |
+
gr.update(value="Generate", interactive=True), # Reset button state
|
520 |
+
gr.update(visible=True), # Show dinosaur button
|
521 |
+
gr.update(visible=True), # Show instruction
|
522 |
+
False # should_generate
|
523 |
+
)
|
524 |
+
|
525 |
# Validate the topic using the Gemini Flash input guard
|
526 |
+
try:
|
527 |
+
if not validate_topic(topic, api_key):
|
528 |
+
gr.Info("Please enter a valid topic or question.")
|
529 |
+
return (
|
530 |
+
[], [], [], 0, "", None, None, "", session_id,
|
531 |
+
gr.update(visible=True), # Show topic box
|
532 |
+
gr.update(visible=True), # Show generate button
|
533 |
+
gr.update(value="Generate", interactive=True), # Reset button state
|
534 |
+
gr.update(visible=True), # Show dinosaur button
|
535 |
+
gr.update(visible=True), # Show instruction
|
536 |
+
False # should_generate
|
537 |
+
)
|
538 |
+
except Exception as e:
|
539 |
+
gr.Error(f"Error validating topic: {str(e)}. Please check your API key.")
|
540 |
return (
|
541 |
[], [], [], 0, "", None, None, "", session_id,
|
542 |
gr.update(visible=True), # Show topic box
|
|
|
560 |
True # should_generate
|
561 |
)
|
562 |
|
563 |
+
def _run_with_new_session(topic, api_key, session_id, should_generate):
|
564 |
"""Second step: actually generate the slideshow"""
|
565 |
if not should_generate:
|
566 |
# This case should ideally not be hit if UI updates from prepare_for_generation are correct
|
|
|
575 |
False # should_generate (though this output isn't strictly used here, keeping tuple size consistent)
|
576 |
)
|
577 |
|
578 |
+
try:
|
579 |
+
results = generate_presentation(topic, api_key, session_id)
|
580 |
+
return (*results,
|
581 |
+
gr.update(visible=False),
|
582 |
+
gr.update(visible=False),
|
583 |
+
gr.update(value="Generate", interactive=True),
|
584 |
+
gr.update(visible=False),
|
585 |
+
gr.update(visible=False),
|
586 |
+
True # should_generate (maintaining tuple size, actual value less critical here)
|
587 |
+
)
|
588 |
+
except Exception as e:
|
589 |
+
gr.Error(f"Error generating slideshow: {str(e)}")
|
590 |
+
return (
|
591 |
+
[], [], [], 0, "Error generating slideshow. Please check your API key and try again.", None, None, "", session_id,
|
592 |
+
gr.update(visible=True),
|
593 |
+
gr.update(visible=True),
|
594 |
+
gr.update(value="Generate", interactive=True),
|
595 |
+
gr.update(visible=True),
|
596 |
+
gr.update(visible=True),
|
597 |
+
False
|
598 |
+
)
|
599 |
|
600 |
# Two-step process for generation
|
601 |
# 1. First clear the UI & validate
|
|
|
603 |
|
604 |
gen_btn.click(
|
605 |
prepare_for_generation,
|
606 |
+
inputs=[topic_box, api_key_input, session_state],
|
607 |
outputs=[
|
608 |
slides_state,
|
609 |
audio_state,
|
|
|
623 |
]
|
624 |
).then( # 2. Then (conditionally) generate the slideshow
|
625 |
_run_with_new_session,
|
626 |
+
inputs=[topic_box, api_key_input, session_state, should_generate_state],
|
627 |
outputs=[
|
628 |
slides_state,
|
629 |
audio_state,
|
|
|
658 |
# Load cached demo slideshow
|
659 |
demo_btn.click(
|
660 |
load_rise_fall_slideshow,
|
661 |
+
inputs=[api_key_input],
|
662 |
outputs=[slides_state, audio_state, images_state, index_state, slide_markdown, audio_player, title_image,
|
663 |
progress_indicator, session_state, demo_btn, demo_instruction],
|
664 |
)
|
665 |
|
666 |
|
667 |
if __name__ == "__main__":
|
668 |
+
demo.launch()
|