Jaward commited on
Commit
878a2e5
·
verified ·
1 Parent(s): 7b87169

Update app.py

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