Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -5,6 +5,8 @@ import gradio as gr
|
|
5 |
import asyncio
|
6 |
import logging
|
7 |
import torch
|
|
|
|
|
8 |
from serpapi import GoogleSearch
|
9 |
from pydantic import BaseModel
|
10 |
from autogen_agentchat.agents import AssistantAgent
|
@@ -20,6 +22,7 @@ import soundfile as sf
|
|
20 |
import tempfile
|
21 |
from pydub import AudioSegment
|
22 |
from TTS.api import TTS
|
|
|
23 |
|
24 |
# Set up logging
|
25 |
logging.basicConfig(
|
@@ -330,6 +333,15 @@ def generate_markdown_slides(slides, title, speaker="Prof. AI Feynman", date="Ap
|
|
330 |
logger.error(traceback.format_exc())
|
331 |
return None
|
332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
333 |
# Async function to update audio preview
|
334 |
async def update_audio_preview(audio_file):
|
335 |
if audio_file:
|
@@ -337,6 +349,26 @@ async def update_audio_preview(audio_file):
|
|
337 |
return audio_file
|
338 |
return None
|
339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
340 |
# Async function to generate lecture materials and audio
|
341 |
async def on_generate(api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, speaker_audio, num_slides):
|
342 |
model_client = get_model_client(api_service, api_key)
|
@@ -401,9 +433,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
401 |
label = "Research: in progress..."
|
402 |
yield (
|
403 |
html_with_progress(label, progress),
|
404 |
-
[],
|
405 |
-
"",
|
406 |
-
[]
|
407 |
)
|
408 |
await asyncio.sleep(0.1)
|
409 |
|
@@ -449,9 +479,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
449 |
label = "Slides: generating..."
|
450 |
yield (
|
451 |
html_with_progress(label, progress),
|
452 |
-
[],
|
453 |
-
"",
|
454 |
-
[]
|
455 |
)
|
456 |
await asyncio.sleep(0.1)
|
457 |
elif source == "slide_agent" and message.target == "script_agent":
|
@@ -476,9 +504,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
476 |
label = "Scripts: generating..."
|
477 |
yield (
|
478 |
html_with_progress(label, progress),
|
479 |
-
[],
|
480 |
-
"",
|
481 |
-
[]
|
482 |
)
|
483 |
await asyncio.sleep(0.1)
|
484 |
elif source == "script_agent" and message.target == "feynman_agent":
|
@@ -492,9 +518,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
492 |
label = "Review: in progress..."
|
493 |
yield (
|
494 |
html_with_progress(label, progress),
|
495 |
-
[],
|
496 |
-
"",
|
497 |
-
[]
|
498 |
)
|
499 |
await asyncio.sleep(0.1)
|
500 |
|
@@ -504,9 +528,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
504 |
label = "Slides: generating..."
|
505 |
yield (
|
506 |
html_with_progress(label, progress),
|
507 |
-
[],
|
508 |
-
"",
|
509 |
-
[]
|
510 |
)
|
511 |
await asyncio.sleep(0.1)
|
512 |
|
@@ -539,9 +561,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
539 |
label = "Scripts: generating..."
|
540 |
yield (
|
541 |
html_with_progress(label, progress),
|
542 |
-
[],
|
543 |
-
"",
|
544 |
-
[]
|
545 |
)
|
546 |
await asyncio.sleep(0.1)
|
547 |
else:
|
@@ -575,9 +595,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
575 |
label = "Scripts generated and saved. Reviewing..."
|
576 |
yield (
|
577 |
html_with_progress(label, progress),
|
578 |
-
[],
|
579 |
-
"",
|
580 |
-
[]
|
581 |
)
|
582 |
await asyncio.sleep(0.1)
|
583 |
else:
|
@@ -597,11 +615,17 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
597 |
logger.info("Feynman Agent completed lecture review: %s", message.content)
|
598 |
progress = 90
|
599 |
label = "Lecture materials ready. Generating audio..."
|
|
|
|
|
|
|
|
|
|
|
600 |
yield (
|
601 |
html_with_progress(label, progress),
|
602 |
-
|
603 |
-
|
604 |
-
[]
|
|
|
605 |
)
|
606 |
await asyncio.sleep(0.1)
|
607 |
|
@@ -617,9 +641,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
617 |
logger.debug("Message from %s, type: %s, content: %s", source, type(msg), msg.to_text() if hasattr(msg, 'to_text') else str(msg))
|
618 |
yield (
|
619 |
error_html,
|
620 |
-
[],
|
621 |
-
"",
|
622 |
-
[]
|
623 |
)
|
624 |
return
|
625 |
|
@@ -632,9 +654,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
632 |
<p style="margin-top: 20px;">Expected {total_slides} slides, but generated {len(slides)}. Please try again.</p>
|
633 |
</div>
|
634 |
""",
|
635 |
-
[],
|
636 |
-
"",
|
637 |
-
[]
|
638 |
)
|
639 |
return
|
640 |
|
@@ -647,9 +667,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
647 |
<p style="margin-top: 20px;">Scripts must be a list of strings. Please try again.</p>
|
648 |
</div>
|
649 |
""",
|
650 |
-
[],
|
651 |
-
"",
|
652 |
-
[]
|
653 |
)
|
654 |
return
|
655 |
|
@@ -662,9 +680,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
662 |
<p style="margin-top: 20px;">Generated {len(slides)} slides but {len(scripts)} scripts. Please try again.</p>
|
663 |
</div>
|
664 |
""",
|
665 |
-
[],
|
666 |
-
"",
|
667 |
-
[]
|
668 |
)
|
669 |
return
|
670 |
|
@@ -678,60 +694,68 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
678 |
<p style="margin-top: 20px;">Please try again.</p>
|
679 |
</div>
|
680 |
""",
|
681 |
-
[],
|
682 |
-
"",
|
683 |
-
[]
|
684 |
)
|
685 |
return
|
686 |
|
687 |
-
#
|
688 |
-
|
689 |
-
txt_files.sort() # Sort for consistent display
|
690 |
-
txt_file_paths = [os.path.join(OUTPUT_DIR, f) for f in txt_files]
|
691 |
|
692 |
-
# Initialize audio
|
693 |
-
|
694 |
-
audio_timeline = ""
|
695 |
-
for i in range(len(scripts)):
|
696 |
-
audio_timeline += f'<audio id="audio-{i+1}" controls style="display: inline-block; margin: 0 10px; width: 200px;"><source src="" type="audio/mpeg"></audio>'
|
697 |
|
698 |
-
#
|
699 |
-
|
700 |
-
|
701 |
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
|
|
|
|
|
|
702 |
<div style="padding: 20px; text-align: center;">
|
703 |
<div id="audio-timeline" style="display: flex; justify-content: center; margin-bottom: 10px;">
|
704 |
-
|
705 |
</div>
|
706 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
707 |
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
708 |
<button id="play-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏯</button>
|
709 |
<button id="next-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
710 |
-
<button id="fullscreen-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;"
|
711 |
</div>
|
712 |
</div>
|
713 |
</div>
|
714 |
<script>
|
715 |
-
const lectureData = {
|
716 |
let currentSlide = 0;
|
717 |
const totalSlides = lectureData.slides.length;
|
718 |
let audioElements = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
719 |
|
720 |
-
|
721 |
-
|
722 |
-
|
723 |
-
|
|
|
|
|
|
|
724 |
}}
|
725 |
|
726 |
-
function
|
727 |
-
|
728 |
-
audioElements.forEach(
|
729 |
if (audio && audio.pause) {{
|
730 |
audio.pause();
|
731 |
audio.currentTime = 0;
|
732 |
-
if (index === currentSlide && audio.src) {{
|
733 |
-
audio.play().catch(e => console.error('Audio play failed:', e));
|
734 |
-
}}
|
735 |
}}
|
736 |
}});
|
737 |
}}
|
@@ -739,25 +763,49 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
739 |
function prevSlide() {{
|
740 |
if (currentSlide > 0) {{
|
741 |
currentSlide--;
|
742 |
-
|
|
|
|
|
|
|
|
|
743 |
}}
|
744 |
}}
|
745 |
|
746 |
function nextSlide() {{
|
747 |
if (currentSlide < totalSlides - 1) {{
|
748 |
currentSlide++;
|
749 |
-
|
|
|
|
|
|
|
|
|
750 |
}}
|
751 |
}}
|
752 |
|
753 |
function playAll() {{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
754 |
let index = currentSlide;
|
755 |
function playNext() {{
|
756 |
-
if (index >= totalSlides)
|
|
|
|
|
|
|
|
|
757 |
currentSlide = index;
|
758 |
-
|
759 |
const audio = audioElements[index];
|
760 |
-
if (audio && audio.
|
761 |
audio.play().then(() => {{
|
762 |
audio.addEventListener('ended', () => {{
|
763 |
index++;
|
@@ -780,7 +828,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
780 |
const container = document.getElementById('lecture-container');
|
781 |
if (!document.fullscreenElement) {{
|
782 |
container.requestFullscreen().catch(err => {{
|
783 |
-
console.error(
|
784 |
}});
|
785 |
}} else {{
|
786 |
document.exitFullscreen();
|
@@ -793,27 +841,31 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
793 |
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
794 |
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullScreen);
|
795 |
|
796 |
-
// Initialize
|
797 |
-
|
798 |
</script>
|
799 |
"""
|
|
|
800 |
yield (
|
801 |
-
|
802 |
txt_file_paths,
|
803 |
-
|
804 |
-
|
|
|
805 |
)
|
806 |
|
807 |
-
#
|
808 |
-
audio_files = []
|
809 |
validated_speaker_wav = await validate_and_convert_speaker_audio(speaker_audio)
|
810 |
if not validated_speaker_wav:
|
811 |
logger.error("Invalid speaker audio after conversion, skipping TTS")
|
812 |
yield (
|
813 |
-
|
814 |
-
|
815 |
-
|
816 |
-
|
|
|
|
|
|
|
817 |
)
|
818 |
return
|
819 |
|
@@ -831,15 +883,15 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
831 |
|
832 |
if not cleaned_script:
|
833 |
logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
|
834 |
-
audio_files
|
835 |
-
audio_urls[i] = None
|
836 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
837 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
838 |
yield (
|
839 |
-
|
840 |
txt_file_paths,
|
841 |
-
|
842 |
-
|
|
|
843 |
)
|
844 |
await asyncio.sleep(0.1)
|
845 |
continue
|
@@ -859,122 +911,15 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
859 |
raise RuntimeError("TTS generation failed")
|
860 |
|
861 |
logger.info("Generated audio for slide %d: %s", i + 1, audio_file)
|
862 |
-
audio_files
|
863 |
-
audio_urls[i] = f"/gradio_api/file={audio_file}"
|
864 |
-
# Update the audio element's src
|
865 |
-
audio_timeline = ""
|
866 |
-
for j, url in enumerate(audio_urls):
|
867 |
-
if url:
|
868 |
-
audio_timeline += f'<audio id="audio-{j+1}" controls src="{url}" style="display: inline-block; margin: 0 10px; width: 200px;"></audio>'
|
869 |
-
else:
|
870 |
-
audio_timeline += f'<audio id="audio-{j+1}" controls style="display: inline-block; margin: 0 10px; width: 200px;"><source src="" type="audio/mpeg"></audio>'
|
871 |
-
html_controls = f"""
|
872 |
-
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
873 |
-
<div style="padding: 20px; text-align: center;">
|
874 |
-
<div id="audio-timeline" style="display: flex; justify-content: center; margin-bottom: 10px;">
|
875 |
-
{audio_timeline}
|
876 |
-
</div>
|
877 |
-
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
878 |
-
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
879 |
-
<button id="play-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏯</button>
|
880 |
-
<button id="next-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
881 |
-
<button id="fullscreen-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">🖥️</button>
|
882 |
-
</div>
|
883 |
-
</div>
|
884 |
-
</div>
|
885 |
-
<script>
|
886 |
-
const lectureData = {json.dumps({"slides": markdown_slides, "audioFiles": audio_urls})};
|
887 |
-
let currentSlide = {currentSlide if 'currentSlide' in locals() else 0};
|
888 |
-
const totalSlides = lectureData.slides.length;
|
889 |
-
let audioElements = [];
|
890 |
-
|
891 |
-
// Populate audio elements
|
892 |
-
for (let i = 0; i < totalSlides; i++) {{
|
893 |
-
const audio = document.getElementById(`audio-${{i+1}}`);
|
894 |
-
audioElements.push(audio);
|
895 |
-
}}
|
896 |
-
|
897 |
-
function updateSlideDisplay() {{
|
898 |
-
window.updateSlideContent(lectureData.slides[currentSlide]);
|
899 |
-
audioElements.forEach((audio, index) => {{
|
900 |
-
if (audio && audio.pause) {{
|
901 |
-
audio.pause();
|
902 |
-
audio.currentTime = 0;
|
903 |
-
if (index === currentSlide && audio.src) {{
|
904 |
-
audio.play().catch(e => console.error('Audio play failed:', e));
|
905 |
-
}}
|
906 |
-
}}
|
907 |
-
}});
|
908 |
-
}}
|
909 |
-
|
910 |
-
function prevSlide() {{
|
911 |
-
if (currentSlide > 0) {{
|
912 |
-
currentSlide--;
|
913 |
-
updateSlideDisplay();
|
914 |
-
}}
|
915 |
-
}}
|
916 |
-
|
917 |
-
function nextSlide() {{
|
918 |
-
if (currentSlide < totalSlides - 1) {{
|
919 |
-
currentSlide++;
|
920 |
-
updateSlideDisplay();
|
921 |
-
}}
|
922 |
-
}}
|
923 |
-
|
924 |
-
function playAll() {{
|
925 |
-
let index = currentSlide;
|
926 |
-
function playNext() {{
|
927 |
-
if (index >= totalSlides) return;
|
928 |
-
currentSlide = index;
|
929 |
-
updateSlideDisplay();
|
930 |
-
const audio = audioElements[index];
|
931 |
-
if (audio && audio.src) {{
|
932 |
-
audio.play().then(() => {{
|
933 |
-
audio.addEventListener('ended', () => {{
|
934 |
-
index++;
|
935 |
-
playNext();
|
936 |
-
}}, {{ once: true }});
|
937 |
-
}}).catch(e => {{
|
938 |
-
console.error('Audio play failed:', e);
|
939 |
-
index++;
|
940 |
-
playNext();
|
941 |
-
}});
|
942 |
-
}} else {{
|
943 |
-
index++;
|
944 |
-
playNext();
|
945 |
-
}}
|
946 |
-
}}
|
947 |
-
playNext();
|
948 |
-
}}
|
949 |
-
|
950 |
-
function toggleFullScreen() {{
|
951 |
-
const container = document.getElementById('lecture-container');
|
952 |
-
if (!document.fullscreenElement) {{
|
953 |
-
container.requestFullscreen().catch(err => {{
|
954 |
-
console.error(`Error attempting to enable full-screen mode: ${{err.message}}`);
|
955 |
-
}});
|
956 |
-
}} else {{
|
957 |
-
document.exitFullscreen();
|
958 |
-
}}
|
959 |
-
}}
|
960 |
-
|
961 |
-
// Attach event listeners
|
962 |
-
document.getElementById('prev-btn').addEventListener('click', prevSlide);
|
963 |
-
document.getElementById('play-btn').addEventListener('click', playAll);
|
964 |
-
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
965 |
-
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullScreen);
|
966 |
-
|
967 |
-
// Initialize first slide
|
968 |
-
updateSlideDisplay();
|
969 |
-
</script>
|
970 |
-
"""
|
971 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
972 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
973 |
yield (
|
974 |
-
|
975 |
txt_file_paths,
|
976 |
-
|
977 |
-
|
|
|
978 |
)
|
979 |
await asyncio.sleep(0.1)
|
980 |
break
|
@@ -982,15 +927,15 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
982 |
logger.error("Error generating audio for slide %d (attempt %d): %s\n%s", i + 1, attempt, str(e), traceback.format_exc())
|
983 |
if attempt == max_audio_retries:
|
984 |
logger.error("Max retries reached for slide %d, skipping", i + 1)
|
985 |
-
audio_files
|
986 |
-
audio_urls[i] = None
|
987 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
988 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
989 |
yield (
|
990 |
-
|
991 |
txt_file_paths,
|
992 |
-
|
993 |
-
|
|
|
994 |
)
|
995 |
await asyncio.sleep(0.1)
|
996 |
break
|
@@ -1007,9 +952,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
1007 |
<p style="margin-top: 20px;">Please try again or adjust your inputs.</p>
|
1008 |
</div>
|
1009 |
""",
|
1010 |
-
[],
|
1011 |
-
"",
|
1012 |
-
[]
|
1013 |
)
|
1014 |
return
|
1015 |
|
@@ -1044,9 +987,11 @@ with gr.Blocks(title="Agent Feynman") as demo:
|
|
1044 |
<p style="margin-top: 10px; font-size: 16px;">Please Generate lecture content via the form on the left first before lecture begins</p>
|
1045 |
</div>
|
1046 |
"""
|
1047 |
-
slide_display = gr.
|
1048 |
-
controls_display = gr.HTML(label="Controls", value=default_slide_html)
|
1049 |
file_output = gr.File(label="Download Generated Files")
|
|
|
|
|
|
|
1050 |
|
1051 |
speaker_audio.change(
|
1052 |
fn=update_audio_preview,
|
@@ -1054,26 +999,11 @@ with gr.Blocks(title="Agent Feynman") as demo:
|
|
1054 |
outputs=speaker_audio
|
1055 |
)
|
1056 |
|
1057 |
-
# JavaScript to update slide content dynamically
|
1058 |
-
demo.load(
|
1059 |
-
fn=None,
|
1060 |
-
inputs=None,
|
1061 |
-
outputs=None,
|
1062 |
-
js="""
|
1063 |
-
() => {
|
1064 |
-
window.updateSlideContent = (content) => {
|
1065 |
-
document.querySelector('#slide-display textarea').value = content;
|
1066 |
-
document.querySelector('#slide-display').dispatchEvent(new Event('input'));
|
1067 |
-
};
|
1068 |
-
}
|
1069 |
-
"""
|
1070 |
-
)
|
1071 |
-
|
1072 |
generate_btn.click(
|
1073 |
fn=on_generate,
|
1074 |
inputs=[api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, speaker_audio, num_slides],
|
1075 |
-
outputs=[
|
1076 |
)
|
1077 |
|
1078 |
if __name__ == "__main__":
|
1079 |
-
demo.launch(allowed_paths=[OUTPUT_DIR])
|
|
|
5 |
import asyncio
|
6 |
import logging
|
7 |
import torch
|
8 |
+
import zipfile
|
9 |
+
import io
|
10 |
from serpapi import GoogleSearch
|
11 |
from pydantic import BaseModel
|
12 |
from autogen_agentchat.agents import AssistantAgent
|
|
|
22 |
import tempfile
|
23 |
from pydub import AudioSegment
|
24 |
from TTS.api import TTS
|
25 |
+
import markdown
|
26 |
|
27 |
# Set up logging
|
28 |
logging.basicConfig(
|
|
|
333 |
logger.error(traceback.format_exc())
|
334 |
return None
|
335 |
|
336 |
+
# Function to convert Markdown to HTML
|
337 |
+
def markdown_to_html(md_text):
|
338 |
+
try:
|
339 |
+
html = markdown.markdown(md_text)
|
340 |
+
return html
|
341 |
+
except Exception as e:
|
342 |
+
logger.error(f"Failed to convert Markdown to HTML: {str(e)}")
|
343 |
+
return "<p>Error rendering slide content</p>"
|
344 |
+
|
345 |
# Async function to update audio preview
|
346 |
async def update_audio_preview(audio_file):
|
347 |
if audio_file:
|
|
|
349 |
return audio_file
|
350 |
return None
|
351 |
|
352 |
+
# Function to create a zip file of all .txt files
|
353 |
+
def create_zip_of_txt_files():
|
354 |
+
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
355 |
+
if not txt_files:
|
356 |
+
return None
|
357 |
+
|
358 |
+
zip_buffer = io.BytesIO()
|
359 |
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
360 |
+
for txt_file in txt_files:
|
361 |
+
file_path = os.path.join(OUTPUT_DIR, txt_file)
|
362 |
+
zip_file.write(file_path, txt_file)
|
363 |
+
|
364 |
+
zip_buffer.seek(0)
|
365 |
+
zip_path = os.path.join(OUTPUT_DIR, "lecture_files.zip")
|
366 |
+
with open(zip_path, "wb") as f:
|
367 |
+
f.write(zip_buffer.getvalue())
|
368 |
+
|
369 |
+
logger.info("Created zip file: %s", zip_path)
|
370 |
+
return zip_path
|
371 |
+
|
372 |
# Async function to generate lecture materials and audio
|
373 |
async def on_generate(api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, speaker_audio, num_slides):
|
374 |
model_client = get_model_client(api_service, api_key)
|
|
|
433 |
label = "Research: in progress..."
|
434 |
yield (
|
435 |
html_with_progress(label, progress),
|
436 |
+
[], 0, [], None
|
|
|
|
|
437 |
)
|
438 |
await asyncio.sleep(0.1)
|
439 |
|
|
|
479 |
label = "Slides: generating..."
|
480 |
yield (
|
481 |
html_with_progress(label, progress),
|
482 |
+
[], 0, [], None
|
|
|
|
|
483 |
)
|
484 |
await asyncio.sleep(0.1)
|
485 |
elif source == "slide_agent" and message.target == "script_agent":
|
|
|
504 |
label = "Scripts: generating..."
|
505 |
yield (
|
506 |
html_with_progress(label, progress),
|
507 |
+
[], 0, [], None
|
|
|
|
|
508 |
)
|
509 |
await asyncio.sleep(0.1)
|
510 |
elif source == "script_agent" and message.target == "feynman_agent":
|
|
|
518 |
label = "Review: in progress..."
|
519 |
yield (
|
520 |
html_with_progress(label, progress),
|
521 |
+
[], 0, [], None
|
|
|
|
|
522 |
)
|
523 |
await asyncio.sleep(0.1)
|
524 |
|
|
|
528 |
label = "Slides: generating..."
|
529 |
yield (
|
530 |
html_with_progress(label, progress),
|
531 |
+
[], 0, [], None
|
|
|
|
|
532 |
)
|
533 |
await asyncio.sleep(0.1)
|
534 |
|
|
|
561 |
label = "Scripts: generating..."
|
562 |
yield (
|
563 |
html_with_progress(label, progress),
|
564 |
+
[], 0, [], None
|
|
|
|
|
565 |
)
|
566 |
await asyncio.sleep(0.1)
|
567 |
else:
|
|
|
595 |
label = "Scripts generated and saved. Reviewing..."
|
596 |
yield (
|
597 |
html_with_progress(label, progress),
|
598 |
+
[], 0, [], None
|
|
|
|
|
599 |
)
|
600 |
await asyncio.sleep(0.1)
|
601 |
else:
|
|
|
615 |
logger.info("Feynman Agent completed lecture review: %s", message.content)
|
616 |
progress = 90
|
617 |
label = "Lecture materials ready. Generating audio..."
|
618 |
+
# Collect .txt files for download
|
619 |
+
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
620 |
+
txt_files.sort() # Sort for consistent display
|
621 |
+
txt_file_paths = [os.path.join(OUTPUT_DIR, f) for f in txt_files]
|
622 |
+
zip_file_path = create_zip_of_txt_files()
|
623 |
yield (
|
624 |
html_with_progress(label, progress),
|
625 |
+
txt_file_paths,
|
626 |
+
0,
|
627 |
+
[None] * total_slides,
|
628 |
+
zip_file_path
|
629 |
)
|
630 |
await asyncio.sleep(0.1)
|
631 |
|
|
|
641 |
logger.debug("Message from %s, type: %s, content: %s", source, type(msg), msg.to_text() if hasattr(msg, 'to_text') else str(msg))
|
642 |
yield (
|
643 |
error_html,
|
644 |
+
[], 0, [], None
|
|
|
|
|
645 |
)
|
646 |
return
|
647 |
|
|
|
654 |
<p style="margin-top: 20px;">Expected {total_slides} slides, but generated {len(slides)}. Please try again.</p>
|
655 |
</div>
|
656 |
""",
|
657 |
+
[], 0, [], None
|
|
|
|
|
658 |
)
|
659 |
return
|
660 |
|
|
|
667 |
<p style="margin-top: 20px;">Scripts must be a list of strings. Please try again.</p>
|
668 |
</div>
|
669 |
""",
|
670 |
+
[], 0, [], None
|
|
|
|
|
671 |
)
|
672 |
return
|
673 |
|
|
|
680 |
<p style="margin-top: 20px;">Generated {len(slides)} slides but {len(scripts)} scripts. Please try again.</p>
|
681 |
</div>
|
682 |
""",
|
683 |
+
[], 0, [], None
|
|
|
|
|
684 |
)
|
685 |
return
|
686 |
|
|
|
694 |
<p style="margin-top: 20px;">Please try again.</p>
|
695 |
</div>
|
696 |
""",
|
697 |
+
[], 0, [], None
|
|
|
|
|
698 |
)
|
699 |
return
|
700 |
|
701 |
+
# Convert Markdown slides to HTML for rendering
|
702 |
+
html_slides = [markdown_to_html(md) for md in markdown_slides]
|
|
|
|
|
703 |
|
704 |
+
# Initialize audio files list with None
|
705 |
+
audio_files = [None] * len(scripts)
|
|
|
|
|
|
|
706 |
|
707 |
+
# Yield the lecture materials immediately after slides and scripts are ready
|
708 |
+
slides_info = json.dumps({"slides": html_slides, "audioFiles": audio_files})
|
709 |
+
html_output = f"""
|
710 |
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
711 |
+
<div id="slide-content" style="flex: 1; overflow: auto; padding: 20px; text-align: center; background-color: #fff; color: #333;">
|
712 |
+
{html_slides[0] if html_slides else "<p>No slide content available</p>"}
|
713 |
+
</div>
|
714 |
<div style="padding: 20px; text-align: center;">
|
715 |
<div id="audio-timeline" style="display: flex; justify-content: center; margin-bottom: 10px;">
|
716 |
+
<!-- Audio components will be rendered here by Gradio -->
|
717 |
</div>
|
718 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
719 |
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
720 |
<button id="play-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏯</button>
|
721 |
<button id="next-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
722 |
+
<button id="fullscreen-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">☐</button>
|
723 |
</div>
|
724 |
</div>
|
725 |
</div>
|
726 |
<script>
|
727 |
+
const lectureData = {slides_info};
|
728 |
let currentSlide = 0;
|
729 |
const totalSlides = lectureData.slides.length;
|
730 |
let audioElements = [];
|
731 |
+
let isPlaying = false;
|
732 |
+
|
733 |
+
// Function to populate audio elements (will be updated by Gradio)
|
734 |
+
function updateAudioElements() {{
|
735 |
+
audioElements = [];
|
736 |
+
for (let i = 0; i < totalSlides; i++) {{
|
737 |
+
const audio = document.getElementById(`audio-${{i+1}}`);
|
738 |
+
if (audio) {{
|
739 |
+
audioElements.push(audio);
|
740 |
+
}}
|
741 |
+
}}
|
742 |
+
}}
|
743 |
|
744 |
+
function renderSlide() {{
|
745 |
+
const slideContent = document.getElementById('slide-content');
|
746 |
+
if (lectureData.slides[currentSlide]) {{
|
747 |
+
slideContent.innerHTML = lectureData.slides[currentSlide];
|
748 |
+
}} else {{
|
749 |
+
slideContent.innerHTML = '<p>No slide content available</p>';
|
750 |
+
}}
|
751 |
}}
|
752 |
|
753 |
+
function updateSlide() {{
|
754 |
+
renderSlide();
|
755 |
+
audioElements.forEach(audio => {{
|
756 |
if (audio && audio.pause) {{
|
757 |
audio.pause();
|
758 |
audio.currentTime = 0;
|
|
|
|
|
|
|
759 |
}}
|
760 |
}});
|
761 |
}}
|
|
|
763 |
function prevSlide() {{
|
764 |
if (currentSlide > 0) {{
|
765 |
currentSlide--;
|
766 |
+
updateSlide();
|
767 |
+
const audio = audioElements[currentSlide];
|
768 |
+
if (audio && audio.play && isPlaying) {{
|
769 |
+
audio.play().catch(e => console.error('Audio play failed:', e));
|
770 |
+
}}
|
771 |
}}
|
772 |
}}
|
773 |
|
774 |
function nextSlide() {{
|
775 |
if (currentSlide < totalSlides - 1) {{
|
776 |
currentSlide++;
|
777 |
+
updateSlide();
|
778 |
+
const audio = audioElements[currentSlide];
|
779 |
+
if (audio && audio.play && isPlaying) {{
|
780 |
+
audio.play().catch(e => console.error('Audio play failed:', e));
|
781 |
+
}}
|
782 |
}}
|
783 |
}}
|
784 |
|
785 |
function playAll() {{
|
786 |
+
isPlaying = !isPlaying;
|
787 |
+
const playBtn = document.getElementById('play-btn');
|
788 |
+
playBtn.textContent = isPlaying ? '⏸' : '⏯';
|
789 |
+
if (!isPlaying) {{
|
790 |
+
audioElements.forEach(audio => {{
|
791 |
+
if (audio && audio.pause) {{
|
792 |
+
audio.pause();
|
793 |
+
audio.currentTime = 0;
|
794 |
+
}}
|
795 |
+
}});
|
796 |
+
return;
|
797 |
+
}}
|
798 |
let index = currentSlide;
|
799 |
function playNext() {{
|
800 |
+
if (index >= totalSlides || !isPlaying) {{
|
801 |
+
isPlaying = false;
|
802 |
+
playBtn.textContent = '⏯';
|
803 |
+
return;
|
804 |
+
}}
|
805 |
currentSlide = index;
|
806 |
+
updateSlide();
|
807 |
const audio = audioElements[index];
|
808 |
+
if (audio && audio.play) {{
|
809 |
audio.play().then(() => {{
|
810 |
audio.addEventListener('ended', () => {{
|
811 |
index++;
|
|
|
828 |
const container = document.getElementById('lecture-container');
|
829 |
if (!document.fullscreenElement) {{
|
830 |
container.requestFullscreen().catch(err => {{
|
831 |
+
console.error('Error attempting to enable full-screen mode:', err);
|
832 |
}});
|
833 |
}} else {{
|
834 |
document.exitFullscreen();
|
|
|
841 |
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
842 |
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullScreen);
|
843 |
|
844 |
+
// Initialize
|
845 |
+
updateAudioElements();
|
846 |
</script>
|
847 |
"""
|
848 |
+
logger.info("Yielding lecture materials before audio generation")
|
849 |
yield (
|
850 |
+
html_output,
|
851 |
txt_file_paths,
|
852 |
+
0,
|
853 |
+
audio_files,
|
854 |
+
zip_file_path
|
855 |
)
|
856 |
|
857 |
+
# Now generate audio files progressively
|
|
|
858 |
validated_speaker_wav = await validate_and_convert_speaker_audio(speaker_audio)
|
859 |
if not validated_speaker_wav:
|
860 |
logger.error("Invalid speaker audio after conversion, skipping TTS")
|
861 |
yield (
|
862 |
+
f"""
|
863 |
+
<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;">
|
864 |
+
<h2 style="color: #d9534f;">Invalid speaker audio</h2>
|
865 |
+
<p style="margin-top: 20px;">Please upload a valid MP3 or WAV audio file and try again.</p>
|
866 |
+
</div>
|
867 |
+
""",
|
868 |
+
[], 0, [], None
|
869 |
)
|
870 |
return
|
871 |
|
|
|
883 |
|
884 |
if not cleaned_script:
|
885 |
logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
|
886 |
+
audio_files[i] = None
|
|
|
887 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
888 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
889 |
yield (
|
890 |
+
html_output,
|
891 |
txt_file_paths,
|
892 |
+
0,
|
893 |
+
audio_files,
|
894 |
+
zip_file_path
|
895 |
)
|
896 |
await asyncio.sleep(0.1)
|
897 |
continue
|
|
|
911 |
raise RuntimeError("TTS generation failed")
|
912 |
|
913 |
logger.info("Generated audio for slide %d: %s", i + 1, audio_file)
|
914 |
+
audio_files[i] = audio_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
915 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
916 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
917 |
yield (
|
918 |
+
html_output,
|
919 |
txt_file_paths,
|
920 |
+
0,
|
921 |
+
audio_files,
|
922 |
+
zip_file_path
|
923 |
)
|
924 |
await asyncio.sleep(0.1)
|
925 |
break
|
|
|
927 |
logger.error("Error generating audio for slide %d (attempt %d): %s\n%s", i + 1, attempt, str(e), traceback.format_exc())
|
928 |
if attempt == max_audio_retries:
|
929 |
logger.error("Max retries reached for slide %d, skipping", i + 1)
|
930 |
+
audio_files[i] = None
|
|
|
931 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
932 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
933 |
yield (
|
934 |
+
html_output,
|
935 |
txt_file_paths,
|
936 |
+
0,
|
937 |
+
audio_files,
|
938 |
+
zip_file_path
|
939 |
)
|
940 |
await asyncio.sleep(0.1)
|
941 |
break
|
|
|
952 |
<p style="margin-top: 20px;">Please try again or adjust your inputs.</p>
|
953 |
</div>
|
954 |
""",
|
955 |
+
[], 0, [], None
|
|
|
|
|
956 |
)
|
957 |
return
|
958 |
|
|
|
987 |
<p style="margin-top: 10px; font-size: 16px;">Please Generate lecture content via the form on the left first before lecture begins</p>
|
988 |
</div>
|
989 |
"""
|
990 |
+
slide_display = gr.HTML(label="Lecture Slides", value=default_slide_html)
|
|
|
991 |
file_output = gr.File(label="Download Generated Files")
|
992 |
+
audio_outputs = gr.Audio(label="Slide Audio", visible=False)
|
993 |
+
slide_index = gr.State(value=0)
|
994 |
+
zip_output = gr.File(label="Download All Files as ZIP")
|
995 |
|
996 |
speaker_audio.change(
|
997 |
fn=update_audio_preview,
|
|
|
999 |
outputs=speaker_audio
|
1000 |
)
|
1001 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1002 |
generate_btn.click(
|
1003 |
fn=on_generate,
|
1004 |
inputs=[api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, speaker_audio, num_slides],
|
1005 |
+
outputs=[slide_display, file_output, slide_index, audio_outputs, zip_output]
|
1006 |
)
|
1007 |
|
1008 |
if __name__ == "__main__":
|
1009 |
+
demo.launch(allowed_paths=[OUTPUT_DIR], max_file_size="5mb")
|