Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -340,22 +340,28 @@ async def update_audio_preview(audio_file):
|
|
340 |
# Async function to generate lecture materials and audio
|
341 |
async def on_generate(api_service, api_key, serpapi_key, title, topic, instructions, lecture_type, speaker_audio, num_slides):
|
342 |
if not serpapi_key:
|
343 |
-
yield
|
344 |
-
|
345 |
-
<
|
346 |
-
|
347 |
-
|
348 |
-
|
|
|
|
|
|
|
349 |
return
|
350 |
|
351 |
if tts is None:
|
352 |
-
yield
|
353 |
-
|
354 |
-
<
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
|
|
|
|
|
|
359 |
return
|
360 |
|
361 |
model_client = get_model_client(api_service, api_key)
|
@@ -418,7 +424,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
418 |
|
419 |
progress = 0
|
420 |
label = "Research: in progress..."
|
421 |
-
yield
|
|
|
|
|
|
|
422 |
await asyncio.sleep(0.1)
|
423 |
|
424 |
initial_message = f"""
|
@@ -458,7 +467,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
458 |
if source == "research_agent" and message.target == "slide_agent":
|
459 |
progress = 25
|
460 |
label = "Slides: generating..."
|
461 |
-
yield
|
|
|
|
|
|
|
462 |
await asyncio.sleep(0.1)
|
463 |
elif source == "slide_agent" and message.target == "script_agent":
|
464 |
if slides is None:
|
@@ -480,7 +492,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
480 |
continue
|
481 |
progress = 50
|
482 |
label = "Scripts: generating..."
|
483 |
-
yield
|
|
|
|
|
|
|
484 |
await asyncio.sleep(0.1)
|
485 |
elif source == "script_agent" and message.target == "feynman_agent":
|
486 |
if scripts is None:
|
@@ -491,14 +506,20 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
491 |
logger.info("Extracted scripts JSON from HandoffMessage context: %s", scripts)
|
492 |
progress = 75
|
493 |
label = "Review: in progress..."
|
494 |
-
yield
|
|
|
|
|
|
|
495 |
await asyncio.sleep(0.1)
|
496 |
|
497 |
elif source == "research_agent" and isinstance(message, TextMessage) and "handoff_to_slide_agent" in message.content:
|
498 |
logger.info("Research Agent completed research")
|
499 |
progress = 25
|
500 |
label = "Slides: generating..."
|
501 |
-
yield
|
|
|
|
|
|
|
502 |
await asyncio.sleep(0.1)
|
503 |
|
504 |
elif source == "slide_agent" and isinstance(message, (TextMessage, StructuredMessage)):
|
@@ -528,7 +549,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
528 |
logger.error("Error saving slide content to %s: %s", content_file, str(e))
|
529 |
progress = 50
|
530 |
label = "Scripts: generating..."
|
531 |
-
yield
|
|
|
|
|
|
|
532 |
await asyncio.sleep(0.1)
|
533 |
else:
|
534 |
logger.warning("No JSON extracted from slide_agent message")
|
@@ -559,7 +583,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
559 |
logger.error("Error saving raw script to %s: %s", script_file, str(e))
|
560 |
progress = 75
|
561 |
label = "Scripts generated and saved. Reviewing..."
|
562 |
-
yield
|
|
|
|
|
|
|
563 |
await asyncio.sleep(0.1)
|
564 |
else:
|
565 |
logger.warning("No JSON extracted from script_agent message")
|
@@ -578,7 +605,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
578 |
logger.info("Feynman Agent completed lecture review: %s", message.content)
|
579 |
progress = 90
|
580 |
label = "Lecture materials ready. Generating audio..."
|
581 |
-
yield
|
|
|
|
|
|
|
582 |
await asyncio.sleep(0.1)
|
583 |
|
584 |
logger.info("Slides state: %s", "Generated" if slides else "None")
|
@@ -591,65 +621,83 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
591 |
for msg in task_result.messages:
|
592 |
source = getattr(msg, 'source', getattr(msg, 'sender', None))
|
593 |
logger.debug("Message from %s, type: %s, content: %s", source, type(msg), msg.to_text() if hasattr(msg, 'to_text') else str(msg))
|
594 |
-
yield
|
595 |
-
|
596 |
-
<
|
597 |
-
|
598 |
-
|
599 |
-
|
|
|
|
|
|
|
600 |
return
|
601 |
|
602 |
if len(slides) != total_slides:
|
603 |
logger.error("Expected %d slides, but received %d", total_slides, len(slides))
|
604 |
-
yield
|
605 |
-
|
606 |
-
<
|
607 |
-
|
608 |
-
|
609 |
-
|
|
|
|
|
|
|
610 |
return
|
611 |
|
612 |
if not isinstance(scripts, list) or not all(isinstance(s, str) for s in scripts):
|
613 |
logger.error("Scripts are not a list of strings: %s", scripts)
|
614 |
-
yield
|
615 |
-
|
616 |
-
<
|
617 |
-
|
618 |
-
|
619 |
-
|
|
|
|
|
|
|
620 |
return
|
621 |
|
622 |
if len(scripts) != total_slides:
|
623 |
logger.error("Mismatch between number of slides (%d) and scripts (%d)", len(slides), len(scripts))
|
624 |
-
yield
|
625 |
-
|
626 |
-
<
|
627 |
-
|
628 |
-
|
629 |
-
|
|
|
|
|
|
|
630 |
return
|
631 |
|
632 |
markdown_slides = generate_markdown_slides(slides, title)
|
633 |
if not markdown_slides:
|
634 |
logger.error("Failed to generate Markdown slides")
|
635 |
-
yield
|
636 |
-
|
637 |
-
<
|
638 |
-
|
639 |
-
|
640 |
-
|
|
|
|
|
|
|
641 |
return
|
642 |
|
643 |
audio_files = []
|
644 |
validated_speaker_wav = await validate_and_convert_speaker_audio(speaker_audio)
|
645 |
if not validated_speaker_wav:
|
646 |
logger.error("Invalid speaker audio after conversion, skipping TTS")
|
647 |
-
yield
|
648 |
-
|
649 |
-
<
|
650 |
-
|
651 |
-
|
652 |
-
|
|
|
|
|
|
|
653 |
return
|
654 |
|
655 |
for i, script in enumerate(scripts):
|
@@ -668,8 +716,11 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
668 |
logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
|
669 |
audio_files.append(None)
|
670 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
671 |
-
label = f"
|
672 |
-
yield
|
|
|
|
|
|
|
673 |
await asyncio.sleep(0.1)
|
674 |
continue
|
675 |
|
@@ -691,7 +742,10 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
691 |
audio_files.append(audio_file)
|
692 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
693 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
694 |
-
yield
|
|
|
|
|
|
|
695 |
await asyncio.sleep(0.1)
|
696 |
break
|
697 |
except Exception as e:
|
@@ -701,25 +755,25 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
701 |
audio_files.append(None)
|
702 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
703 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
704 |
-
yield
|
|
|
|
|
|
|
705 |
await asyncio.sleep(0.1)
|
706 |
break
|
707 |
|
708 |
# Collect .txt files for download
|
709 |
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
710 |
txt_files.sort() # Sort for consistent display
|
711 |
-
|
712 |
-
for txt_file in txt_files:
|
713 |
-
file_path = os.path.join(OUTPUT_DIR, txt_file)
|
714 |
-
txt_links += f'<a href="file/{file_path}" download>{txt_file}</a> '
|
715 |
|
716 |
-
# Generate audio timeline
|
717 |
audio_timeline = ""
|
718 |
for i, audio_file in enumerate(audio_files):
|
719 |
if audio_file:
|
720 |
-
audio_timeline += f'<
|
721 |
else:
|
722 |
-
audio_timeline += f'<span id="audio-{i+1}">slide_{i+1}.mp3</span>
|
723 |
|
724 |
slides_info = json.dumps({"slides": markdown_slides, "audioFiles": audio_files})
|
725 |
|
@@ -733,16 +787,12 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
733 |
{audio_timeline}
|
734 |
</div>
|
735 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
736 |
-
<button
|
737 |
-
<button
|
738 |
-
<button
|
739 |
<button style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">☐</button>
|
740 |
</div>
|
741 |
</div>
|
742 |
-
<div style="padding: 10px; text-align: center;">
|
743 |
-
<h4>Download Generated Files:</h4>
|
744 |
-
{txt_links}
|
745 |
-
</div>
|
746 |
</div>
|
747 |
<script>
|
748 |
const lectureData = {slides_info};
|
@@ -752,12 +802,8 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
752 |
let isPlayingAll = false;
|
753 |
|
754 |
for (let i = 0; i < totalSlides; i++) {{
|
755 |
-
|
756 |
-
|
757 |
-
audioElements.push(audio);
|
758 |
-
}} else {{
|
759 |
-
audioElements.push(null);
|
760 |
-
}}
|
761 |
}}
|
762 |
|
763 |
function renderSlide() {{
|
@@ -768,7 +814,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
768 |
function updateSlide() {{
|
769 |
renderSlide();
|
770 |
audioElements.forEach(audio => {{
|
771 |
-
if (audio) {{
|
772 |
audio.pause();
|
773 |
audio.currentTime = 0;
|
774 |
}}
|
@@ -792,13 +838,15 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
792 |
function playAll() {{
|
793 |
if (isPlayingAll) {{
|
794 |
audioElements.forEach(audio => {{
|
795 |
-
if (audio) audio.pause();
|
796 |
}});
|
797 |
isPlayingAll = false;
|
|
|
798 |
return;
|
799 |
}}
|
800 |
|
801 |
isPlayingAll = true;
|
|
|
802 |
currentSlide = 0;
|
803 |
updateSlide();
|
804 |
playCurrentSlide();
|
@@ -807,11 +855,12 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
807 |
function playCurrentSlide() {{
|
808 |
if (!isPlayingAll || currentSlide >= totalSlides) {{
|
809 |
isPlayingAll = false;
|
|
|
810 |
return;
|
811 |
}}
|
812 |
|
813 |
const audio = audioElements[currentSlide];
|
814 |
-
if (audio) {{
|
815 |
audio.play().then(() => {{
|
816 |
audio.addEventListener('ended', () => {{
|
817 |
currentSlide++;
|
@@ -820,6 +869,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
820 |
playCurrentSlide();
|
821 |
}} else {{
|
822 |
isPlayingAll = false;
|
|
|
823 |
}}
|
824 |
}}, {{ once: true }});
|
825 |
}}).catch(e => {{
|
@@ -833,22 +883,33 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
833 |
}}
|
834 |
}}
|
835 |
|
|
|
|
|
|
|
|
|
|
|
836 |
// Initialize first slide
|
837 |
renderSlide();
|
838 |
</script>
|
839 |
"""
|
840 |
logger.info("Lecture generation completed successfully")
|
841 |
-
yield
|
|
|
|
|
|
|
842 |
|
843 |
except Exception as e:
|
844 |
logger.error("Error during lecture generation: %s\n%s", str(e), traceback.format_exc())
|
845 |
-
yield
|
846 |
-
|
847 |
-
<
|
848 |
-
|
849 |
-
|
850 |
-
|
851 |
-
|
|
|
|
|
|
|
852 |
return
|
853 |
|
854 |
# Gradio interface
|
@@ -884,6 +945,7 @@ with gr.Blocks(title="Agent Feynman") as demo:
|
|
884 |
</div>
|
885 |
"""
|
886 |
slide_display = gr.HTML(label="Lecture Slides", value=default_slide_html)
|
|
|
887 |
|
888 |
speaker_audio.change(
|
889 |
fn=update_audio_preview,
|
@@ -894,7 +956,7 @@ with gr.Blocks(title="Agent Feynman") as demo:
|
|
894 |
generate_btn.click(
|
895 |
fn=on_generate,
|
896 |
inputs=[api_service, api_key, serpapi_key, title, topic, instructions, lecture_type, speaker_audio, num_slides],
|
897 |
-
outputs=[slide_display]
|
898 |
)
|
899 |
|
900 |
if __name__ == "__main__":
|
|
|
340 |
# Async function to generate lecture materials and audio
|
341 |
async def on_generate(api_service, api_key, serpapi_key, title, topic, instructions, lecture_type, speaker_audio, num_slides):
|
342 |
if not serpapi_key:
|
343 |
+
yield {
|
344 |
+
"html": f"""
|
345 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
346 |
+
<h2 style="color: #d9534f;">SerpApi key required</h2>
|
347 |
+
<p style="margin-top: 20px;">Please provide a valid SerpApi key and try again.</p>
|
348 |
+
</div>
|
349 |
+
""",
|
350 |
+
"files": []
|
351 |
+
}
|
352 |
return
|
353 |
|
354 |
if tts is None:
|
355 |
+
yield {
|
356 |
+
"html": f"""
|
357 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
358 |
+
<h2 style="color: #d9534f;">TTS model initialization failed</h2>
|
359 |
+
<p style="margin-top: 20px;">The TTS model failed to initialize at startup.</p>
|
360 |
+
<p>Please ensure the Coqui TTS model is properly installed and try restarting the application.</p>
|
361 |
+
</div>
|
362 |
+
""",
|
363 |
+
"files": []
|
364 |
+
}
|
365 |
return
|
366 |
|
367 |
model_client = get_model_client(api_service, api_key)
|
|
|
424 |
|
425 |
progress = 0
|
426 |
label = "Research: in progress..."
|
427 |
+
yield {
|
428 |
+
"html": html_with_progress(label, progress),
|
429 |
+
"files": []
|
430 |
+
}
|
431 |
await asyncio.sleep(0.1)
|
432 |
|
433 |
initial_message = f"""
|
|
|
467 |
if source == "research_agent" and message.target == "slide_agent":
|
468 |
progress = 25
|
469 |
label = "Slides: generating..."
|
470 |
+
yield {
|
471 |
+
"html": html_with_progress(label, progress),
|
472 |
+
"files": []
|
473 |
+
}
|
474 |
await asyncio.sleep(0.1)
|
475 |
elif source == "slide_agent" and message.target == "script_agent":
|
476 |
if slides is None:
|
|
|
492 |
continue
|
493 |
progress = 50
|
494 |
label = "Scripts: generating..."
|
495 |
+
yield {
|
496 |
+
"html": html_with_progress(label, progress),
|
497 |
+
"files": []
|
498 |
+
}
|
499 |
await asyncio.sleep(0.1)
|
500 |
elif source == "script_agent" and message.target == "feynman_agent":
|
501 |
if scripts is None:
|
|
|
506 |
logger.info("Extracted scripts JSON from HandoffMessage context: %s", scripts)
|
507 |
progress = 75
|
508 |
label = "Review: in progress..."
|
509 |
+
yield {
|
510 |
+
"html": html_with_progress(label, progress),
|
511 |
+
"files": []
|
512 |
+
}
|
513 |
await asyncio.sleep(0.1)
|
514 |
|
515 |
elif source == "research_agent" and isinstance(message, TextMessage) and "handoff_to_slide_agent" in message.content:
|
516 |
logger.info("Research Agent completed research")
|
517 |
progress = 25
|
518 |
label = "Slides: generating..."
|
519 |
+
yield {
|
520 |
+
"html": html_with_progress(label, progress),
|
521 |
+
"files": []
|
522 |
+
}
|
523 |
await asyncio.sleep(0.1)
|
524 |
|
525 |
elif source == "slide_agent" and isinstance(message, (TextMessage, StructuredMessage)):
|
|
|
549 |
logger.error("Error saving slide content to %s: %s", content_file, str(e))
|
550 |
progress = 50
|
551 |
label = "Scripts: generating..."
|
552 |
+
yield {
|
553 |
+
"html": html_with_progress(label, progress),
|
554 |
+
"files": []
|
555 |
+
}
|
556 |
await asyncio.sleep(0.1)
|
557 |
else:
|
558 |
logger.warning("No JSON extracted from slide_agent message")
|
|
|
583 |
logger.error("Error saving raw script to %s: %s", script_file, str(e))
|
584 |
progress = 75
|
585 |
label = "Scripts generated and saved. Reviewing..."
|
586 |
+
yield {
|
587 |
+
"html": html_with_progress(label, progress),
|
588 |
+
"files": []
|
589 |
+
}
|
590 |
await asyncio.sleep(0.1)
|
591 |
else:
|
592 |
logger.warning("No JSON extracted from script_agent message")
|
|
|
605 |
logger.info("Feynman Agent completed lecture review: %s", message.content)
|
606 |
progress = 90
|
607 |
label = "Lecture materials ready. Generating audio..."
|
608 |
+
yield {
|
609 |
+
"html": html_with_progress(label, progress),
|
610 |
+
"files": []
|
611 |
+
}
|
612 |
await asyncio.sleep(0.1)
|
613 |
|
614 |
logger.info("Slides state: %s", "Generated" if slides else "None")
|
|
|
621 |
for msg in task_result.messages:
|
622 |
source = getattr(msg, 'source', getattr(msg, 'sender', None))
|
623 |
logger.debug("Message from %s, type: %s, content: %s", source, type(msg), msg.to_text() if hasattr(msg, 'to_text') else str(msg))
|
624 |
+
yield {
|
625 |
+
"html": f"""
|
626 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
627 |
+
<h2 style="color: #d9534f;">{error_message}</h2>
|
628 |
+
<p style="margin-top: 20px;">Please try again with a different model (e.g., Anthropic-claude-3-sonnet-20240229) or simplify the topic/instructions.</p>
|
629 |
+
</div>
|
630 |
+
""",
|
631 |
+
"files": []
|
632 |
+
}
|
633 |
return
|
634 |
|
635 |
if len(slides) != total_slides:
|
636 |
logger.error("Expected %d slides, but received %d", total_slides, len(slides))
|
637 |
+
yield {
|
638 |
+
"html": f"""
|
639 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
640 |
+
<h2 style="color: #d9534f;">Incorrect number of slides</h2>
|
641 |
+
<p style="margin-top: 20px;">Expected {total_slides} slides, but generated {len(slides)}. Please try again.</p>
|
642 |
+
</div>
|
643 |
+
""",
|
644 |
+
"files": []
|
645 |
+
}
|
646 |
return
|
647 |
|
648 |
if not isinstance(scripts, list) or not all(isinstance(s, str) for s in scripts):
|
649 |
logger.error("Scripts are not a list of strings: %s", scripts)
|
650 |
+
yield {
|
651 |
+
"html": f"""
|
652 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
653 |
+
<h2 style="color: #d9534f;">Invalid script format</h2>
|
654 |
+
<p style="margin-top: 20px;">Scripts must be a list of strings. Please try again.</p>
|
655 |
+
</div>
|
656 |
+
""",
|
657 |
+
"files": []
|
658 |
+
}
|
659 |
return
|
660 |
|
661 |
if len(scripts) != total_slides:
|
662 |
logger.error("Mismatch between number of slides (%d) and scripts (%d)", len(slides), len(scripts))
|
663 |
+
yield {
|
664 |
+
"html": f"""
|
665 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
666 |
+
<h2 style="color: #d9534f;">Mismatch in slides and scripts</h2>
|
667 |
+
<p style="margin-top: 20px;">Generated {len(slides)} slides but {len(scripts)} scripts. Please try again.</p>
|
668 |
+
</div>
|
669 |
+
""",
|
670 |
+
"files": []
|
671 |
+
}
|
672 |
return
|
673 |
|
674 |
markdown_slides = generate_markdown_slides(slides, title)
|
675 |
if not markdown_slides:
|
676 |
logger.error("Failed to generate Markdown slides")
|
677 |
+
yield {
|
678 |
+
"html": f"""
|
679 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
680 |
+
<h2 style="color: #d9534f;">Failed to generate slides</h2>
|
681 |
+
<p style="margin-top: 20px;">Please try again.</p>
|
682 |
+
</div>
|
683 |
+
""",
|
684 |
+
"files": []
|
685 |
+
}
|
686 |
return
|
687 |
|
688 |
audio_files = []
|
689 |
validated_speaker_wav = await validate_and_convert_speaker_audio(speaker_audio)
|
690 |
if not validated_speaker_wav:
|
691 |
logger.error("Invalid speaker audio after conversion, skipping TTS")
|
692 |
+
yield {
|
693 |
+
"html": f"""
|
694 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
695 |
+
<h2 style="color: #d9534f;">Invalid speaker audio</h2>
|
696 |
+
<p style="margin-top: 20px;">Please upload a valid MP3 or WAV audio file and try again.</p>
|
697 |
+
</div>
|
698 |
+
""",
|
699 |
+
"files": []
|
700 |
+
}
|
701 |
return
|
702 |
|
703 |
for i, script in enumerate(scripts):
|
|
|
716 |
logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
|
717 |
audio_files.append(None)
|
718 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
719 |
+
label = f"Generating speech for slide {i + 1}/{len(scripts)}..."
|
720 |
+
yield {
|
721 |
+
"html": html_with_progress(label, progress),
|
722 |
+
"files": []
|
723 |
+
}
|
724 |
await asyncio.sleep(0.1)
|
725 |
continue
|
726 |
|
|
|
742 |
audio_files.append(audio_file)
|
743 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
744 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
745 |
+
yield {
|
746 |
+
"html": html_with_progress(label, progress),
|
747 |
+
"files": []
|
748 |
+
}
|
749 |
await asyncio.sleep(0.1)
|
750 |
break
|
751 |
except Exception as e:
|
|
|
755 |
audio_files.append(None)
|
756 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
757 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
758 |
+
yield {
|
759 |
+
"html": html_with_progress(label, progress),
|
760 |
+
"files": []
|
761 |
+
}
|
762 |
await asyncio.sleep(0.1)
|
763 |
break
|
764 |
|
765 |
# Collect .txt files for download
|
766 |
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
767 |
txt_files.sort() # Sort for consistent display
|
768 |
+
txt_file_paths = [os.path.join(OUTPUT_DIR, f) for f in txt_files]
|
|
|
|
|
|
|
769 |
|
770 |
+
# Generate audio timeline with playable audio elements
|
771 |
audio_timeline = ""
|
772 |
for i, audio_file in enumerate(audio_files):
|
773 |
if audio_file:
|
774 |
+
audio_timeline += f'<audio id="audio-{i+1}" controls src="{audio_file}" style="margin: 0 5px;"></audio>'
|
775 |
else:
|
776 |
+
audio_timeline += f'<span id="audio-{i+1}" style="margin: 0 5px;">slide_{i+1}.mp3 (not generated)</span>'
|
777 |
|
778 |
slides_info = json.dumps({"slides": markdown_slides, "audioFiles": audio_files})
|
779 |
|
|
|
787 |
{audio_timeline}
|
788 |
</div>
|
789 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
790 |
+
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
791 |
+
<button id="play-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏯</button>
|
792 |
+
<button id="next-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
793 |
<button style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">☐</button>
|
794 |
</div>
|
795 |
</div>
|
|
|
|
|
|
|
|
|
796 |
</div>
|
797 |
<script>
|
798 |
const lectureData = {slides_info};
|
|
|
802 |
let isPlayingAll = false;
|
803 |
|
804 |
for (let i = 0; i < totalSlides; i++) {{
|
805 |
+
const audio = document.getElementById(`audio-${{i+1}}`);
|
806 |
+
audioElements.push(audio);
|
|
|
|
|
|
|
|
|
807 |
}}
|
808 |
|
809 |
function renderSlide() {{
|
|
|
814 |
function updateSlide() {{
|
815 |
renderSlide();
|
816 |
audioElements.forEach(audio => {{
|
817 |
+
if (audio && audio.pause) {{
|
818 |
audio.pause();
|
819 |
audio.currentTime = 0;
|
820 |
}}
|
|
|
838 |
function playAll() {{
|
839 |
if (isPlayingAll) {{
|
840 |
audioElements.forEach(audio => {{
|
841 |
+
if (audio && audio.pause) audio.pause();
|
842 |
}});
|
843 |
isPlayingAll = false;
|
844 |
+
document.getElementById('play-btn').innerText = '⏯';
|
845 |
return;
|
846 |
}}
|
847 |
|
848 |
isPlayingAll = true;
|
849 |
+
document.getElementById('play-btn').innerText = '⏸';
|
850 |
currentSlide = 0;
|
851 |
updateSlide();
|
852 |
playCurrentSlide();
|
|
|
855 |
function playCurrentSlide() {{
|
856 |
if (!isPlayingAll || currentSlide >= totalSlides) {{
|
857 |
isPlayingAll = false;
|
858 |
+
document.getElementById('play-btn').innerText = '⏯';
|
859 |
return;
|
860 |
}}
|
861 |
|
862 |
const audio = audioElements[currentSlide];
|
863 |
+
if (audio && audio.play) {{
|
864 |
audio.play().then(() => {{
|
865 |
audio.addEventListener('ended', () => {{
|
866 |
currentSlide++;
|
|
|
869 |
playCurrentSlide();
|
870 |
}} else {{
|
871 |
isPlayingAll = false;
|
872 |
+
document.getElementById('play-btn').innerText = '⏯';
|
873 |
}}
|
874 |
}}, {{ once: true }});
|
875 |
}}).catch(e => {{
|
|
|
883 |
}}
|
884 |
}}
|
885 |
|
886 |
+
// Attach event listeners
|
887 |
+
document.getElementById('prev-btn').addEventListener('click', prevSlide);
|
888 |
+
document.getElementById('play-btn').addEventListener('click', playAll);
|
889 |
+
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
890 |
+
|
891 |
// Initialize first slide
|
892 |
renderSlide();
|
893 |
</script>
|
894 |
"""
|
895 |
logger.info("Lecture generation completed successfully")
|
896 |
+
yield {
|
897 |
+
"html": html_output,
|
898 |
+
"files": txt_file_paths
|
899 |
+
}
|
900 |
|
901 |
except Exception as e:
|
902 |
logger.error("Error during lecture generation: %s\n%s", str(e), traceback.format_exc())
|
903 |
+
yield {
|
904 |
+
"html": f"""
|
905 |
+
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; min-height: 700px; padding: 20px; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
906 |
+
<h2 style="color: #d9534f;">Error during lecture generation</h2>
|
907 |
+
<p style="margin-top: 10px; font-size: 16px;">{str(e)}</p>
|
908 |
+
<p style="margin-top: 20px;">Please try again or adjust your inputs.</p>
|
909 |
+
</div>
|
910 |
+
""",
|
911 |
+
"files": []
|
912 |
+
}
|
913 |
return
|
914 |
|
915 |
# Gradio interface
|
|
|
945 |
</div>
|
946 |
"""
|
947 |
slide_display = gr.HTML(label="Lecture Slides", value=default_slide_html)
|
948 |
+
file_output = gr.File(label="Download Generated Files")
|
949 |
|
950 |
speaker_audio.change(
|
951 |
fn=update_audio_preview,
|
|
|
956 |
generate_btn.click(
|
957 |
fn=on_generate,
|
958 |
inputs=[api_service, api_key, serpapi_key, title, topic, instructions, lecture_type, speaker_audio, num_slides],
|
959 |
+
outputs=[slide_display, file_output]
|
960 |
)
|
961 |
|
962 |
if __name__ == "__main__":
|