Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -6,7 +6,6 @@ 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,7 +21,6 @@ import soundfile as sf
|
|
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,15 +331,6 @@ def generate_markdown_slides(slides, title, speaker="Prof. AI Feynman", date="Ap
|
|
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:
|
@@ -350,22 +339,12 @@ async def update_audio_preview(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
|
367 |
-
|
368 |
-
|
|
|
369 |
logger.info("Created zip file: %s", zip_path)
|
370 |
return zip_path
|
371 |
|
@@ -431,10 +410,9 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
431 |
|
432 |
progress = 0
|
433 |
label = "Research: in progress..."
|
434 |
-
audio_outputs = [None] * total_slides # Initialize for total_slides
|
435 |
yield (
|
436 |
html_with_progress(label, progress),
|
437 |
-
[],
|
438 |
)
|
439 |
await asyncio.sleep(0.1)
|
440 |
|
@@ -480,7 +458,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
480 |
label = "Slides: generating..."
|
481 |
yield (
|
482 |
html_with_progress(label, progress),
|
483 |
-
[],
|
484 |
)
|
485 |
await asyncio.sleep(0.1)
|
486 |
elif source == "slide_agent" and message.target == "script_agent":
|
@@ -505,7 +483,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
505 |
label = "Scripts: generating..."
|
506 |
yield (
|
507 |
html_with_progress(label, progress),
|
508 |
-
[],
|
509 |
)
|
510 |
await asyncio.sleep(0.1)
|
511 |
elif source == "script_agent" and message.target == "feynman_agent":
|
@@ -519,7 +497,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
519 |
label = "Review: in progress..."
|
520 |
yield (
|
521 |
html_with_progress(label, progress),
|
522 |
-
[],
|
523 |
)
|
524 |
await asyncio.sleep(0.1)
|
525 |
|
@@ -529,7 +507,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
529 |
label = "Slides: generating..."
|
530 |
yield (
|
531 |
html_with_progress(label, progress),
|
532 |
-
[],
|
533 |
)
|
534 |
await asyncio.sleep(0.1)
|
535 |
|
@@ -562,7 +540,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
562 |
label = "Scripts: generating..."
|
563 |
yield (
|
564 |
html_with_progress(label, progress),
|
565 |
-
[],
|
566 |
)
|
567 |
await asyncio.sleep(0.1)
|
568 |
else:
|
@@ -596,7 +574,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
596 |
label = "Scripts generated and saved. Reviewing..."
|
597 |
yield (
|
598 |
html_with_progress(label, progress),
|
599 |
-
[],
|
600 |
)
|
601 |
await asyncio.sleep(0.1)
|
602 |
else:
|
@@ -620,13 +598,11 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
620 |
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
621 |
txt_files.sort() # Sort for consistent display
|
622 |
txt_file_paths = [os.path.join(OUTPUT_DIR, f) for f in txt_files]
|
623 |
-
|
624 |
yield (
|
625 |
html_with_progress(label, progress),
|
626 |
txt_file_paths,
|
627 |
-
|
628 |
-
audio_outputs,
|
629 |
-
zip_file_path
|
630 |
)
|
631 |
await asyncio.sleep(0.1)
|
632 |
|
@@ -642,7 +618,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
642 |
logger.debug("Message from %s, type: %s, content: %s", source, type(msg), msg.to_text() if hasattr(msg, 'to_text') else str(msg))
|
643 |
yield (
|
644 |
error_html,
|
645 |
-
[],
|
646 |
)
|
647 |
return
|
648 |
|
@@ -655,7 +631,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
655 |
<p style="margin-top: 20px;">Expected {total_slides} slides, but generated {len(slides)}. Please try again.</p>
|
656 |
</div>
|
657 |
""",
|
658 |
-
[],
|
659 |
)
|
660 |
return
|
661 |
|
@@ -668,7 +644,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
668 |
<p style="margin-top: 20px;">Scripts must be a list of strings. Please try again.</p>
|
669 |
</div>
|
670 |
""",
|
671 |
-
[],
|
672 |
)
|
673 |
return
|
674 |
|
@@ -681,7 +657,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
681 |
<p style="margin-top: 20px;">Generated {len(slides)} slides but {len(scripts)} scripts. Please try again.</p>
|
682 |
</div>
|
683 |
""",
|
684 |
-
[],
|
685 |
)
|
686 |
return
|
687 |
|
@@ -695,26 +671,30 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
695 |
<p style="margin-top: 20px;">Please try again.</p>
|
696 |
</div>
|
697 |
""",
|
698 |
-
[],
|
699 |
)
|
700 |
return
|
701 |
|
702 |
-
#
|
703 |
-
|
|
|
|
|
|
|
704 |
|
705 |
-
#
|
706 |
-
|
|
|
|
|
|
|
707 |
|
708 |
# Yield the lecture materials immediately after slides and scripts are ready
|
709 |
-
slides_info = json.dumps({"slides":
|
710 |
html_output = f"""
|
711 |
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
712 |
-
<div id="slide-content" style="flex: 1; overflow: auto; padding: 20px; text-align: center; background-color: #fff; color: #333;">
|
713 |
-
{html_slides[0] if html_slides else "<p>No slide content available</p>"}
|
714 |
-
</div>
|
715 |
<div style="padding: 20px; text-align: center;">
|
716 |
-
<div
|
717 |
-
{
|
718 |
</div>
|
719 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
720 |
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
@@ -731,28 +711,21 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
731 |
let audioElements = [];
|
732 |
let isPlaying = false;
|
733 |
|
734 |
-
//
|
735 |
-
|
736 |
-
|
737 |
-
|
738 |
-
// Gradio assigns IDs like "audio-<component_id>-<index>"
|
739 |
-
const container = document.getElementById(`audio-container-${{i+1}}`);
|
740 |
-
if (container) {{
|
741 |
-
const audio = container.querySelector('audio');
|
742 |
-
if (audio) {{
|
743 |
-
audioElements.push(audio);
|
744 |
-
}}
|
745 |
-
}}
|
746 |
-
}}
|
747 |
-
console.log("Updated audio elements:", audioElements);
|
748 |
}}
|
749 |
|
750 |
function renderSlide() {{
|
751 |
const slideContent = document.getElementById('slide-content');
|
752 |
if (lectureData.slides[currentSlide]) {{
|
753 |
-
|
|
|
|
|
754 |
}} else {{
|
755 |
-
slideContent.innerHTML = '<
|
|
|
756 |
}}
|
757 |
}}
|
758 |
|
@@ -766,6 +739,17 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
766 |
}});
|
767 |
}}
|
768 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
769 |
function prevSlide() {{
|
770 |
if (currentSlide > 0) {{
|
771 |
currentSlide--;
|
@@ -847,20 +831,19 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
847 |
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
848 |
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullScreen);
|
849 |
|
850 |
-
// Initialize
|
851 |
-
|
852 |
</script>
|
853 |
"""
|
854 |
logger.info("Yielding lecture materials before audio generation")
|
855 |
yield (
|
856 |
html_output,
|
857 |
txt_file_paths,
|
858 |
-
|
859 |
-
audio_files,
|
860 |
-
zip_file_path
|
861 |
)
|
862 |
|
863 |
# Now generate audio files progressively
|
|
|
864 |
validated_speaker_wav = await validate_and_convert_speaker_audio(speaker_audio)
|
865 |
if not validated_speaker_wav:
|
866 |
logger.error("Invalid speaker audio after conversion, skipping TTS")
|
@@ -871,7 +854,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
871 |
<p style="margin-top: 20px;">Please upload a valid MP3 or WAV audio file and try again.</p>
|
872 |
</div>
|
873 |
""",
|
874 |
-
[],
|
875 |
)
|
876 |
return
|
877 |
|
@@ -889,15 +872,14 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
889 |
|
890 |
if not cleaned_script:
|
891 |
logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
|
892 |
-
audio_files
|
|
|
893 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
894 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
895 |
yield (
|
896 |
html_output,
|
897 |
txt_file_paths,
|
898 |
-
|
899 |
-
audio_files,
|
900 |
-
zip_file_path
|
901 |
)
|
902 |
await asyncio.sleep(0.1)
|
903 |
continue
|
@@ -917,15 +899,172 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
917 |
raise RuntimeError("TTS generation failed")
|
918 |
|
919 |
logger.info("Generated audio for slide %d: %s", i + 1, audio_file)
|
920 |
-
audio_files
|
|
|
921 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
922 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
923 |
yield (
|
924 |
html_output,
|
925 |
txt_file_paths,
|
926 |
-
|
927 |
-
audio_files,
|
928 |
-
zip_file_path
|
929 |
)
|
930 |
await asyncio.sleep(0.1)
|
931 |
break
|
@@ -933,15 +1072,14 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
933 |
logger.error("Error generating audio for slide %d (attempt %d): %s\n%s", i + 1, attempt, str(e), traceback.format_exc())
|
934 |
if attempt == max_audio_retries:
|
935 |
logger.error("Max retries reached for slide %d, skipping", i + 1)
|
936 |
-
audio_files
|
|
|
937 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
938 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
939 |
yield (
|
940 |
html_output,
|
941 |
txt_file_paths,
|
942 |
-
|
943 |
-
audio_files,
|
944 |
-
zip_file_path
|
945 |
)
|
946 |
await asyncio.sleep(0.1)
|
947 |
break
|
@@ -958,7 +1096,7 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
958 |
<p style="margin-top: 20px;">Please try again or adjust your inputs.</p>
|
959 |
</div>
|
960 |
""",
|
961 |
-
[],
|
962 |
)
|
963 |
return
|
964 |
|
@@ -995,9 +1133,6 @@ with gr.Blocks(title="Agent Feynman") as demo:
|
|
995 |
"""
|
996 |
slide_display = gr.HTML(label="Lecture Slides", value=default_slide_html)
|
997 |
file_output = gr.File(label="Download Generated Files")
|
998 |
-
# Create a list of audio components for each slide
|
999 |
-
audio_outputs = [gr.Audio(label=f"Slide {i+1} Audio", visible=True) for i in range(20)] # Max 20 slides
|
1000 |
-
slide_index = gr.State(value=0)
|
1001 |
zip_output = gr.File(label="Download All Files as ZIP")
|
1002 |
|
1003 |
speaker_audio.change(
|
@@ -1009,7 +1144,7 @@ with gr.Blocks(title="Agent Feynman") as demo:
|
|
1009 |
generate_btn.click(
|
1010 |
fn=on_generate,
|
1011 |
inputs=[api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, speaker_audio, num_slides],
|
1012 |
-
outputs=[slide_display, file_output,
|
1013 |
)
|
1014 |
|
1015 |
if __name__ == "__main__":
|
|
|
6 |
import logging
|
7 |
import torch
|
8 |
import zipfile
|
|
|
9 |
from serpapi import GoogleSearch
|
10 |
from pydantic import BaseModel
|
11 |
from autogen_agentchat.agents import AssistantAgent
|
|
|
21 |
import tempfile
|
22 |
from pydub import AudioSegment
|
23 |
from TTS.api import TTS
|
|
|
24 |
|
25 |
# Set up logging
|
26 |
logging.basicConfig(
|
|
|
331 |
logger.error(traceback.format_exc())
|
332 |
return None
|
333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
# Async function to update audio preview
|
335 |
async def update_audio_preview(audio_file):
|
336 |
if audio_file:
|
|
|
339 |
return None
|
340 |
|
341 |
# Function to create a zip file of all .txt files
|
342 |
+
def create_zip_of_txt_files(txt_file_paths):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
zip_path = os.path.join(OUTPUT_DIR, "lecture_files.zip")
|
344 |
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
345 |
+
for file_path in txt_file_paths:
|
346 |
+
if os.path.exists(file_path):
|
347 |
+
zipf.write(file_path, os.path.basename(file_path))
|
348 |
logger.info("Created zip file: %s", zip_path)
|
349 |
return zip_path
|
350 |
|
|
|
410 |
|
411 |
progress = 0
|
412 |
label = "Research: in progress..."
|
|
|
413 |
yield (
|
414 |
html_with_progress(label, progress),
|
415 |
+
[], None
|
416 |
)
|
417 |
await asyncio.sleep(0.1)
|
418 |
|
|
|
458 |
label = "Slides: generating..."
|
459 |
yield (
|
460 |
html_with_progress(label, progress),
|
461 |
+
[], None
|
462 |
)
|
463 |
await asyncio.sleep(0.1)
|
464 |
elif source == "slide_agent" and message.target == "script_agent":
|
|
|
483 |
label = "Scripts: generating..."
|
484 |
yield (
|
485 |
html_with_progress(label, progress),
|
486 |
+
[], None
|
487 |
)
|
488 |
await asyncio.sleep(0.1)
|
489 |
elif source == "script_agent" and message.target == "feynman_agent":
|
|
|
497 |
label = "Review: in progress..."
|
498 |
yield (
|
499 |
html_with_progress(label, progress),
|
500 |
+
[], None
|
501 |
)
|
502 |
await asyncio.sleep(0.1)
|
503 |
|
|
|
507 |
label = "Slides: generating..."
|
508 |
yield (
|
509 |
html_with_progress(label, progress),
|
510 |
+
[], None
|
511 |
)
|
512 |
await asyncio.sleep(0.1)
|
513 |
|
|
|
540 |
label = "Scripts: generating..."
|
541 |
yield (
|
542 |
html_with_progress(label, progress),
|
543 |
+
[], None
|
544 |
)
|
545 |
await asyncio.sleep(0.1)
|
546 |
else:
|
|
|
574 |
label = "Scripts generated and saved. Reviewing..."
|
575 |
yield (
|
576 |
html_with_progress(label, progress),
|
577 |
+
[], None
|
578 |
)
|
579 |
await asyncio.sleep(0.1)
|
580 |
else:
|
|
|
598 |
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
599 |
txt_files.sort() # Sort for consistent display
|
600 |
txt_file_paths = [os.path.join(OUTPUT_DIR, f) for f in txt_files]
|
601 |
+
zip_file = create_zip_of_txt_files(txt_file_paths)
|
602 |
yield (
|
603 |
html_with_progress(label, progress),
|
604 |
txt_file_paths,
|
605 |
+
zip_file
|
|
|
|
|
606 |
)
|
607 |
await asyncio.sleep(0.1)
|
608 |
|
|
|
618 |
logger.debug("Message from %s, type: %s, content: %s", source, type(msg), msg.to_text() if hasattr(msg, 'to_text') else str(msg))
|
619 |
yield (
|
620 |
error_html,
|
621 |
+
[], None
|
622 |
)
|
623 |
return
|
624 |
|
|
|
631 |
<p style="margin-top: 20px;">Expected {total_slides} slides, but generated {len(slides)}. Please try again.</p>
|
632 |
</div>
|
633 |
""",
|
634 |
+
[], None
|
635 |
)
|
636 |
return
|
637 |
|
|
|
644 |
<p style="margin-top: 20px;">Scripts must be a list of strings. Please try again.</p>
|
645 |
</div>
|
646 |
""",
|
647 |
+
[], None
|
648 |
)
|
649 |
return
|
650 |
|
|
|
657 |
<p style="margin-top: 20px;">Generated {len(slides)} slides but {len(scripts)} scripts. Please try again.</p>
|
658 |
</div>
|
659 |
""",
|
660 |
+
[], None
|
661 |
)
|
662 |
return
|
663 |
|
|
|
671 |
<p style="margin-top: 20px;">Please try again.</p>
|
672 |
</div>
|
673 |
""",
|
674 |
+
[], None
|
675 |
)
|
676 |
return
|
677 |
|
678 |
+
# Generate initial audio timeline with placeholders
|
679 |
+
audio_urls = [None] * len(scripts)
|
680 |
+
audio_timeline = ""
|
681 |
+
for i in range(len(scripts)):
|
682 |
+
audio_timeline += f'<audio id="audio-{i+1}" controls src="" style="display: inline-block; margin: 0 10px; width: 200px;"><span>Loading...</span></audio>'
|
683 |
|
684 |
+
# Collect .txt files for download (already done above, but ensure it's available)
|
685 |
+
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
686 |
+
txt_files.sort() # Sort for consistent display
|
687 |
+
txt_file_paths = [os.path.join(OUTPUT_DIR, f) for f in txt_files]
|
688 |
+
zip_file = create_zip_of_txt_files(txt_file_paths)
|
689 |
|
690 |
# Yield the lecture materials immediately after slides and scripts are ready
|
691 |
+
slides_info = json.dumps({"slides": markdown_slides, "audioFiles": audio_urls})
|
692 |
html_output = f"""
|
693 |
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
694 |
+
<div id="slide-content" style="flex: 1; overflow: auto; padding: 20px; text-align: center; background-color: #fff; color: #333;"></div>
|
|
|
|
|
695 |
<div style="padding: 20px; text-align: center;">
|
696 |
+
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
697 |
+
{audio_timeline}
|
698 |
</div>
|
699 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
700 |
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
|
|
711 |
let audioElements = [];
|
712 |
let isPlaying = false;
|
713 |
|
714 |
+
// Populate audio elements
|
715 |
+
for (let i = 0; i < totalSlides; i++) {{
|
716 |
+
const audio = document.getElementById(`audio-${{i+1}}`);
|
717 |
+
audioElements.push(audio);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
718 |
}}
|
719 |
|
720 |
function renderSlide() {{
|
721 |
const slideContent = document.getElementById('slide-content');
|
722 |
if (lectureData.slides[currentSlide]) {{
|
723 |
+
// Since the content is already Markdown-rendered by Gradio, we can set it directly
|
724 |
+
slideContent.innerHTML = lectureData.slides[currentSlide].replace(/\\n/g, '<br>');
|
725 |
+
console.log("Rendering slide:", lectureData.slides[currentSlide]);
|
726 |
}} else {{
|
727 |
+
slideContent.innerHTML = '<h2>No slide content available</h2>';
|
728 |
+
console.log("No slide content for index:", currentSlide);
|
729 |
}}
|
730 |
}}
|
731 |
|
|
|
739 |
}});
|
740 |
}}
|
741 |
|
742 |
+
function updateAudioSources(audioUrls) {{
|
743 |
+
audioUrls.forEach((url, index) => {{
|
744 |
+
const audio = audioElements[index];
|
745 |
+
if (audio && url && audio.src !== url) {{
|
746 |
+
audio.src = url;
|
747 |
+
audio.load(); // Force reload the audio element
|
748 |
+
console.log(`Updated audio-${{index+1}} src to:`, url);
|
749 |
+
}}
|
750 |
+
}});
|
751 |
+
}}
|
752 |
+
|
753 |
function prevSlide() {{
|
754 |
if (currentSlide > 0) {{
|
755 |
currentSlide--;
|
|
|
831 |
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
832 |
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullScreen);
|
833 |
|
834 |
+
// Initialize first slide
|
835 |
+
renderSlide();
|
836 |
</script>
|
837 |
"""
|
838 |
logger.info("Yielding lecture materials before audio generation")
|
839 |
yield (
|
840 |
html_output,
|
841 |
txt_file_paths,
|
842 |
+
zip_file
|
|
|
|
|
843 |
)
|
844 |
|
845 |
# Now generate audio files progressively
|
846 |
+
audio_files = []
|
847 |
validated_speaker_wav = await validate_and_convert_speaker_audio(speaker_audio)
|
848 |
if not validated_speaker_wav:
|
849 |
logger.error("Invalid speaker audio after conversion, skipping TTS")
|
|
|
854 |
<p style="margin-top: 20px;">Please upload a valid MP3 or WAV audio file and try again.</p>
|
855 |
</div>
|
856 |
""",
|
857 |
+
[], None
|
858 |
)
|
859 |
return
|
860 |
|
|
|
872 |
|
873 |
if not cleaned_script:
|
874 |
logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
|
875 |
+
audio_files.append(None)
|
876 |
+
audio_urls[i] = None
|
877 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
878 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
879 |
yield (
|
880 |
html_output,
|
881 |
txt_file_paths,
|
882 |
+
zip_file
|
|
|
|
|
883 |
)
|
884 |
await asyncio.sleep(0.1)
|
885 |
continue
|
|
|
899 |
raise RuntimeError("TTS generation failed")
|
900 |
|
901 |
logger.info("Generated audio for slide %d: %s", i + 1, audio_file)
|
902 |
+
audio_files.append(audio_file)
|
903 |
+
audio_urls[i] = f"/gradio_api/file={audio_file}"
|
904 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
905 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
906 |
+
|
907 |
+
// Update audio timeline with the new audio URL
|
908 |
+
audio_timeline = ""
|
909 |
+
for j, url in enumerate(audio_urls):
|
910 |
+
if url:
|
911 |
+
audio_timeline += f'<audio id="audio-{j+1}" controls src="{url}" style="display: inline-block; margin: 0 10px; width: 200px;"></audio>'
|
912 |
+
else:
|
913 |
+
audio_timeline += f'<audio id="audio-{j+1}" controls src="" style="display: inline-block; margin: 0 10px; width: 200px;"><span>Loading...</span></audio>'
|
914 |
+
|
915 |
+
html_output = f"""
|
916 |
+
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
917 |
+
<div id="slide-content" style="flex: 1; overflow: auto; padding: 20px; text-align: center; background-color: #fff; color: #333;"></div>
|
918 |
+
<div style="padding: 20px; text-align: center;">
|
919 |
+
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
920 |
+
{audio_timeline}
|
921 |
+
</div>
|
922 |
+
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
923 |
+
<button id="prev-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
924 |
+
<button id="play-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏯</button>
|
925 |
+
<button id="next-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
926 |
+
<button id="fullscreen-btn" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">☐</button>
|
927 |
+
</div>
|
928 |
+
</div>
|
929 |
+
</div>
|
930 |
+
<script>
|
931 |
+
const lectureData = {slides_info};
|
932 |
+
let currentSlide = 0;
|
933 |
+
const totalSlides = lectureData.slides.length;
|
934 |
+
let audioElements = [];
|
935 |
+
let isPlaying = false;
|
936 |
+
|
937 |
+
// Populate audio elements
|
938 |
+
for (let i = 0; i < totalSlides; i++) {{
|
939 |
+
const audio = document.getElementById(`audio-${{i+1}}`);
|
940 |
+
audioElements.push(audio);
|
941 |
+
}}
|
942 |
+
|
943 |
+
// Update audio sources dynamically
|
944 |
+
lectureData.audioFiles = {json.dumps(audio_urls)};
|
945 |
+
updateAudioSources(lectureData.audioFiles);
|
946 |
+
|
947 |
+
function renderSlide() {{
|
948 |
+
const slideContent = document.getElementById('slide-content');
|
949 |
+
if (lectureData.slides[currentSlide]) {{
|
950 |
+
slideContent.innerHTML = lectureData.slides[currentSlide].replace(/\\n/g, '<br>');
|
951 |
+
console.log("Rendering slide:", lectureData.slides[currentSlide]);
|
952 |
+
}} else {{
|
953 |
+
slideContent.innerHTML = '<h2>No slide content available</h2>';
|
954 |
+
console.log("No slide content for index:", currentSlide);
|
955 |
+
}}
|
956 |
+
}}
|
957 |
+
|
958 |
+
function updateSlide() {{
|
959 |
+
renderSlide();
|
960 |
+
audioElements.forEach(audio => {{
|
961 |
+
if (audio && audio.pause) {{
|
962 |
+
audio.pause();
|
963 |
+
audio.currentTime = 0;
|
964 |
+
}}
|
965 |
+
}});
|
966 |
+
}}
|
967 |
+
|
968 |
+
function updateAudioSources(audioUrls) {{
|
969 |
+
audioUrls.forEach((url, index) => {{
|
970 |
+
const audio = audioElements[index];
|
971 |
+
if (audio && url && audio.src !== url) {{
|
972 |
+
audio.src = url;
|
973 |
+
audio.load();
|
974 |
+
console.log(`Updated audio-${{index+1}} src to:`, url);
|
975 |
+
}}
|
976 |
+
}});
|
977 |
+
}}
|
978 |
+
|
979 |
+
function prevSlide() {{
|
980 |
+
if (currentSlide > 0) {{
|
981 |
+
currentSlide--;
|
982 |
+
updateSlide();
|
983 |
+
const audio = audioElements[currentSlide];
|
984 |
+
if (audio && audio.play && isPlaying) {{
|
985 |
+
audio.play().catch(e => console.error('Audio play failed:', e));
|
986 |
+
}}
|
987 |
+
}}
|
988 |
+
}}
|
989 |
+
|
990 |
+
function nextSlide() {{
|
991 |
+
if (currentSlide < totalSlides - 1) {{
|
992 |
+
currentSlide++;
|
993 |
+
updateSlide();
|
994 |
+
const audio = audioElements[currentSlide];
|
995 |
+
if (audio && audio.play && isPlaying) {{
|
996 |
+
audio.play().catch(e => console.error('Audio play failed:', e));
|
997 |
+
}}
|
998 |
+
}}
|
999 |
+
}}
|
1000 |
+
|
1001 |
+
function playAll() {{
|
1002 |
+
isPlaying = !isPlaying;
|
1003 |
+
const playBtn = document.getElementById('play-btn');
|
1004 |
+
playBtn.textContent = isPlaying ? '⏸' : '⏯';
|
1005 |
+
if (!isPlaying) {{
|
1006 |
+
audioElements.forEach(audio => {{
|
1007 |
+
if (audio && audio.pause) {{
|
1008 |
+
audio.pause();
|
1009 |
+
audio.currentTime = 0;
|
1010 |
+
}}
|
1011 |
+
}});
|
1012 |
+
return;
|
1013 |
+
}}
|
1014 |
+
let index = currentSlide;
|
1015 |
+
function playNext() {{
|
1016 |
+
if (index >= totalSlides || !isPlaying) {{
|
1017 |
+
isPlaying = false;
|
1018 |
+
playBtn.textContent = '⏯';
|
1019 |
+
return;
|
1020 |
+
}}
|
1021 |
+
currentSlide = index;
|
1022 |
+
updateSlide();
|
1023 |
+
const audio = audioElements[index];
|
1024 |
+
if (audio && audio.play) {{
|
1025 |
+
audio.play().then(() => {{
|
1026 |
+
audio.addEventListener('ended', () => {{
|
1027 |
+
index++;
|
1028 |
+
playNext();
|
1029 |
+
}}, {{ once: true }});
|
1030 |
+
}}).catch(e => {{
|
1031 |
+
console.error('Audio play failed:', e);
|
1032 |
+
index++;
|
1033 |
+
playNext();
|
1034 |
+
}});
|
1035 |
+
}} else {{
|
1036 |
+
index++;
|
1037 |
+
playNext();
|
1038 |
+
}}
|
1039 |
+
}}
|
1040 |
+
playNext();
|
1041 |
+
}}
|
1042 |
+
|
1043 |
+
function toggleFullScreen() {{
|
1044 |
+
const container = document.getElementById('lecture-container');
|
1045 |
+
if (!document.fullscreenElement) {{
|
1046 |
+
container.requestFullscreen().catch(err => {{
|
1047 |
+
console.error('Error attempting to enable full-screen mode:', err);
|
1048 |
+
}});
|
1049 |
+
}} else {{
|
1050 |
+
document.exitFullscreen();
|
1051 |
+
}}
|
1052 |
+
}}
|
1053 |
+
|
1054 |
+
// Attach event listeners
|
1055 |
+
document.getElementById('prev-btn').addEventListener('click', prevSlide);
|
1056 |
+
document.getElementById('play-btn').addEventListener('click', playAll);
|
1057 |
+
document.getElementById('next-btn').addEventListener('click', nextSlide);
|
1058 |
+
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullScreen);
|
1059 |
+
|
1060 |
+
// Initialize first slide
|
1061 |
+
renderSlide();
|
1062 |
+
</script>
|
1063 |
+
"""
|
1064 |
yield (
|
1065 |
html_output,
|
1066 |
txt_file_paths,
|
1067 |
+
zip_file
|
|
|
|
|
1068 |
)
|
1069 |
await asyncio.sleep(0.1)
|
1070 |
break
|
|
|
1072 |
logger.error("Error generating audio for slide %d (attempt %d): %s\n%s", i + 1, attempt, str(e), traceback.format_exc())
|
1073 |
if attempt == max_audio_retries:
|
1074 |
logger.error("Max retries reached for slide %d, skipping", i + 1)
|
1075 |
+
audio_files.append(None)
|
1076 |
+
audio_urls[i] = None
|
1077 |
progress = 90 + ((i + 1) / len(scripts)) * 10
|
1078 |
label = f"Generated audio for slide {i + 1}/{len(scripts)}..."
|
1079 |
yield (
|
1080 |
html_output,
|
1081 |
txt_file_paths,
|
1082 |
+
zip_file
|
|
|
|
|
1083 |
)
|
1084 |
await asyncio.sleep(0.1)
|
1085 |
break
|
|
|
1096 |
<p style="margin-top: 20px;">Please try again or adjust your inputs.</p>
|
1097 |
</div>
|
1098 |
""",
|
1099 |
+
[], None
|
1100 |
)
|
1101 |
return
|
1102 |
|
|
|
1133 |
"""
|
1134 |
slide_display = gr.HTML(label="Lecture Slides", value=default_slide_html)
|
1135 |
file_output = gr.File(label="Download Generated Files")
|
|
|
|
|
|
|
1136 |
zip_output = gr.File(label="Download All Files as ZIP")
|
1137 |
|
1138 |
speaker_audio.change(
|
|
|
1144 |
generate_btn.click(
|
1145 |
fn=on_generate,
|
1146 |
inputs=[api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, speaker_audio, num_slides],
|
1147 |
+
outputs=[slide_display, file_output, zip_output]
|
1148 |
)
|
1149 |
|
1150 |
if __name__ == "__main__":
|