Jaward commited on
Commit
14dfce0
·
verified ·
1 Parent(s): a85ca37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +227 -92
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 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
 
@@ -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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- zip_file_path = create_zip_of_txt_files()
624
  yield (
625
  html_with_progress(label, progress),
626
  txt_file_paths,
627
- 0,
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
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
- [], 0, audio_outputs, None
699
  )
700
  return
701
 
702
- # Convert Markdown slides to HTML for rendering
703
- html_slides = [markdown_to_html(md) for md in markdown_slides]
 
 
 
704
 
705
- # Initialize audio files list with None
706
- audio_files = [None] * len(scripts)
 
 
 
707
 
708
  # Yield the lecture materials immediately after slides and scripts are ready
709
- slides_info = json.dumps({"slides": html_slides, "audioFiles": audio_files})
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 id="audio-timeline" style="display: flex; justify-content: center; margin-bottom: 10px;">
717
- {"".join([f'<div id="audio-container-{i+1}" style="display: inline-block; margin: 0 10px; width: 200px;"></div>' for i in range(total_slides)])}
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
- // Function to populate audio elements
735
- function updateAudioElements() {{
736
- audioElements = [];
737
- for (let i = 0; i < totalSlides; i++) {{
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
- slideContent.innerHTML = lectureData.slides[currentSlide];
 
 
754
  }} else {{
755
- slideContent.innerHTML = '<p>No slide content available</p>';
 
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 audio elements after DOM updates
851
- setTimeout(updateAudioElements, 1000); // Delay to ensure Gradio renders the audio components
852
  </script>
853
  """
854
  logger.info("Yielding lecture materials before audio generation")
855
  yield (
856
  html_output,
857
  txt_file_paths,
858
- 0,
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
- [], 0, audio_files, None
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[i] = None
 
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
- 0,
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[i] = audio_file
 
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
- 0,
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[i] = None
 
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
- 0,
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
- [], 0, audio_outputs, None
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, slide_index] + audio_outputs[:20] + [zip_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__":