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__":
|