Jaward commited on
Commit
65a26d2
·
verified ·
1 Parent(s): 9786116

visualize stage logs

Browse files
Files changed (1) hide show
  1. app.py +67 -42
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # Lectūra Research Demo: A Multi-Agent Tool for Self-taught Mastery.
2
  # Author: Jaward Sesay
3
- # © Lectūra Labs. All rights reserved.
4
  import os
5
  import json
6
  import re
@@ -170,14 +170,48 @@ def create_slides(slides: list[dict], title: str, instructor_name: str, output_d
170
  logger.error("Failed to create HTML slides: %s", str(e))
171
  return []
172
 
173
- # progress bar
 
 
 
 
 
 
 
 
174
  def html_with_progress(label, progress):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  return f"""
176
  <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;">
177
  <div style="width: 70%; background-color: lightgrey; border-radius: 80px; overflow: hidden; margin-bottom: 20px;">
178
  <div style="width: {progress}%; height: 15px; background-color: #4CAF50; border-radius: 80px;"></div>
179
  </div>
180
  <h2 style="font-style: italic; color: #555 !important;">{label}</h2>
 
181
  </div>
182
  """
183
 
@@ -280,7 +314,7 @@ def generate_xtts_audio(tts, text, speaker_wav, output_path):
280
  return False
281
  try:
282
  tts.tts_to_file(text=text, speaker_wav=speaker_wav, language="en", file_path=output_path)
283
- logger.info("Generated audio for %s", output_path)
284
  return True
285
  except Exception as e:
286
  logger.error("Failed to generate audio for %s: %s", output_path, str(e))
@@ -418,7 +452,15 @@ def get_gradio_file_url(local_path):
418
  # Async generate lecture materials and audio
419
  async def on_generate(api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, lecture_style, speaker_audio, num_slides):
420
  print(f"Received serpapi_key: '{serpapi_key}' (type: {type(serpapi_key)}, length: {len(serpapi_key) if serpapi_key else 0})")
421
-
 
 
 
 
 
 
 
 
422
  model_client = get_model_client(api_service, api_key)
423
 
424
  # Get the speaker from the speaker_audio path
@@ -576,6 +618,10 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
576
 
577
  progress = 0
578
  label = "Researching lecture topic..."
 
 
 
 
579
  yield (
580
  html_with_progress(label, progress),
581
  []
@@ -624,6 +670,8 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
624
  if source == "research_agent" and message.target == "slide_agent":
625
  progress = 25
626
  label = "Slides: generating..."
 
 
627
  yield (
628
  html_with_progress(label, progress),
629
  []
@@ -636,6 +684,7 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
636
  if extracted_json:
637
  slides = extracted_json
638
  logger.info("Extracted slides JSON from HandoffMessage context: %s", slides)
 
639
  if slides is None or len(slides) != total_slides:
640
  if slide_retry_count < max_retries:
641
  slide_retry_count += 1
@@ -661,6 +710,7 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
661
  if extracted_json:
662
  scripts = extracted_json
663
  logger.info("Extracted scripts JSON from HandoffMessage context: %s", scripts)
 
664
  progress = 75
665
  label = "Review: in progress..."
666
  yield (
@@ -669,22 +719,13 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
669
  )
670
  await asyncio.sleep(0.1)
671
 
672
- elif source == "research_agent" and isinstance(message, TextMessage) and "handoff_to_slide_agent" in message.content:
673
- logger.info("Research Agent completed research")
674
- progress = 25
675
- label = "Research complete. Generating slides..."
676
- yield (
677
- html_with_progress(label, progress),
678
- []
679
- )
680
- await asyncio.sleep(0.1)
681
-
682
  elif source == "slide_agent" and isinstance(message, (TextMessage, StructuredMessage)):
683
  logger.debug("Slide Agent message received")
684
  extracted_json = extract_json_from_message(message)
685
  if extracted_json:
686
  slides = extracted_json
687
  logger.info("Slide Agent generated %d slides: %s", len(slides), slides)
 
688
  if len(slides) != total_slides:
689
  if slide_retry_count < max_retries:
690
  slide_retry_count += 1
@@ -700,6 +741,9 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
700
  html_files = create_slides(slides, title, instructor_name)
701
  if not html_files:
702
  logger.error("Failed to generate HTML slides")
 
 
 
703
  progress = 50
704
  label = "Scripts: generating..."
705
  yield (
@@ -726,14 +770,17 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
726
  if extracted_json:
727
  scripts = extracted_json
728
  logger.info("Script Agent generated scripts for %d slides: %s", len(scripts), scripts)
 
729
  for i, script in enumerate(scripts):
730
  script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_script.txt")
731
  try:
732
  with open(script_file, "w", encoding="utf-8") as f:
733
  f.write(script)
734
  logger.info("Saved script to %s", script_file)
 
735
  except Exception as e:
736
  logger.error("Error saving script to %s: %s", script_file, str(e))
 
737
  progress = 75
738
  label = "Script complete. Reviewing lecture materials..."
739
  yield (
@@ -758,6 +805,7 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
758
  logger.info("Instructor Agent completed lecture review: %s", message.content)
759
  progress = 90
760
  label = "Lecture materials ready. Generating lecture speech..."
 
761
  file_paths = [f for f in os.listdir(OUTPUT_DIR) if f.endswith(('.md', '.txt'))]
762
  file_paths.sort()
763
  file_paths = [os.path.join(OUTPUT_DIR, f) for f in file_paths]
@@ -852,32 +900,8 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
852
 
853
  for i, script in enumerate(scripts):
854
  cleaned_script = clean_script_text(script)
855
- audio_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}.mp3")
856
- script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_script.txt")
857
-
858
- try:
859
- with open(script_file, "w", encoding="utf-8") as f:
860
- f.write(cleaned_script or "")
861
- logger.info("Saved script to %s: %s", script_file, cleaned_script)
862
- except Exception as e:
863
- logger.error("Error saving script to %s: %s",
864
- script_file, str(e))
865
 
866
- if not cleaned_script:
867
- logger.error("Skipping audio for slide %d due to empty or invalid script", i + 1)
868
- audio_files.append(None)
869
- audio_urls[i] = None
870
- progress = 90 + ((i + 1) / len(scripts)) * 10
871
- label = f"Generating lecture speech for slide {i + 1}/{len(scripts)}..."
872
- yield (
873
- html_with_progress(label, progress),
874
- file_paths,
875
- None
876
- )
877
- await asyncio.sleep(0.1)
878
- continue
879
-
880
- max_audio_retries = 5
881
  for attempt in range(max_audio_retries + 1):
882
  try:
883
  current_text = cleaned_script
@@ -891,11 +915,12 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
891
  if not success:
892
  raise RuntimeError("TTS generation failed")
893
 
894
- logger.info("Generated audio for slide %d: %s", i + 1, audio_file)
895
  audio_files.append(audio_file)
896
- audio_urls[i] = f"/gradio_api/file={audio_file}"
897
  progress = 90 + ((i + 1) / len(scripts)) * 10
898
  label = f"Generating lecture speech for slide {i + 1}/{len(scripts)}..."
 
899
  file_paths.append(audio_file)
900
  yield (
901
  html_with_progress(label, progress),
@@ -912,6 +937,7 @@ Example: 'Received {total_slides} slides, {total_slides} scripts, and HTML files
912
  audio_urls[i] = None
913
  progress = 90 + ((i + 1) / len(scripts)) * 10
914
  label = f"Generating lecture speech for slide {i + 1}/{len(scripts)}..."
 
915
  yield (
916
  html_with_progress(label, progress),
917
  file_paths,
@@ -2241,7 +2267,6 @@ with gr.Blocks(
2241
  return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(value=quiz, visible=True)
2242
 
2243
  # Helper to get fallback lecture context from form fields
2244
-
2245
  def get_fallback_lecture_context(lecture_context, title_val, desc_val, style_val, audience_val):
2246
  # If slides/scripts missing, use form fields
2247
  if lecture_context and (lecture_context.get("slides") or lecture_context.get("scripts")):
 
1
  # Lectūra Research Demo: A Multi-Agent Tool for Self-taught Mastery.
2
  # Author: Jaward Sesay
3
+ # © Lectūra Labs 2025. All rights reserved.
4
  import os
5
  import json
6
  import re
 
170
  logger.error("Failed to create HTML slides: %s", str(e))
171
  return []
172
 
173
+ # Track stage outputs
174
+ stage_outputs = {}
175
+
176
+ def add_stage_output(stage_name, output):
177
+ if stage_name not in stage_outputs:
178
+ stage_outputs[stage_name] = []
179
+ stage_outputs[stage_name].append(output)
180
+
181
+ # Dynamic progress bar with accordion
182
  def html_with_progress(label, progress):
183
+ accordion_html = ""
184
+ if stage_outputs:
185
+ accordion_html = """
186
+ <div style="width: 70%; margin-top: 20px; border: 1px solid #ddd; border-radius: 25px; overflow: hidden;">
187
+ """
188
+ for stage_name, outputs in stage_outputs.items():
189
+ accordion_html += f"""
190
+ <div style="border-bottom: 1px solid #ddd;">
191
+ <div style="padding: 10px; background-color: #f8f9fa; cursor: pointer;" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">
192
+ <h4 style="margin: 0; color: #555;">{stage_name}</h4>
193
+ </div>
194
+ <div style="padding: 10px; display: none; background-color: white;">
195
+ """
196
+ for output in outputs:
197
+ accordion_html += f"""
198
+ <div style="margin-bottom: 10px; padding: 10px; background-color: #f8f9fa; border-radius: 4px;">
199
+ <pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{output}</pre>
200
+ </div>
201
+ """
202
+ accordion_html += """
203
+ </div>
204
+ </div>
205
+ """
206
+ accordion_html += "</div>"
207
+
208
  return f"""
209
  <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;">
210
  <div style="width: 70%; background-color: lightgrey; border-radius: 80px; overflow: hidden; margin-bottom: 20px;">
211
  <div style="width: {progress}%; height: 15px; background-color: #4CAF50; border-radius: 80px;"></div>
212
  </div>
213
  <h2 style="font-style: italic; color: #555 !important;">{label}</h2>
214
+ {accordion_html}
215
  </div>
216
  """
217
 
 
314
  return False
315
  try:
316
  tts.tts_to_file(text=text, speaker_wav=speaker_wav, language="en", file_path=output_path)
317
+ logger.info("Generated speech for %s", output_path)
318
  return True
319
  except Exception as e:
320
  logger.error("Failed to generate audio for %s: %s", output_path, str(e))
 
452
  # Async generate lecture materials and audio
453
  async def on_generate(api_service, api_key, serpapi_key, title, lecture_content_description, lecture_type, lecture_style, speaker_audio, num_slides):
454
  print(f"Received serpapi_key: '{serpapi_key}' (type: {type(serpapi_key)}, length: {len(serpapi_key) if serpapi_key else 0})")
455
+
456
+ # Reset stage outputs at the start of generation
457
+ global stage_outputs
458
+ stage_outputs = {}
459
+
460
+ # Define constants
461
+ max_audio_retries = 5
462
+ max_retries = 3
463
+
464
  model_client = get_model_client(api_service, api_key)
465
 
466
  # Get the speaker from the speaker_audio path
 
618
 
619
  progress = 0
620
  label = "Researching lecture topic..."
621
+ if not serpapi_key:
622
+ add_stage_output("Research", "No API key for research was provided, proceeding with slide generation.")
623
+ else:
624
+ add_stage_output("Research", "Starting research phase...")
625
  yield (
626
  html_with_progress(label, progress),
627
  []
 
670
  if source == "research_agent" and message.target == "slide_agent":
671
  progress = 25
672
  label = "Slides: generating..."
673
+ if hasattr(message, 'content'):
674
+ add_stage_output("Research", message.content)
675
  yield (
676
  html_with_progress(label, progress),
677
  []
 
684
  if extracted_json:
685
  slides = extracted_json
686
  logger.info("Extracted slides JSON from HandoffMessage context: %s", slides)
687
+ add_stage_output("Slides", json.dumps(slides, indent=2))
688
  if slides is None or len(slides) != total_slides:
689
  if slide_retry_count < max_retries:
690
  slide_retry_count += 1
 
710
  if extracted_json:
711
  scripts = extracted_json
712
  logger.info("Extracted scripts JSON from HandoffMessage context: %s", scripts)
713
+ add_stage_output("Scripts", json.dumps(scripts, indent=2))
714
  progress = 75
715
  label = "Review: in progress..."
716
  yield (
 
719
  )
720
  await asyncio.sleep(0.1)
721
 
 
 
 
 
 
 
 
 
 
 
722
  elif source == "slide_agent" and isinstance(message, (TextMessage, StructuredMessage)):
723
  logger.debug("Slide Agent message received")
724
  extracted_json = extract_json_from_message(message)
725
  if extracted_json:
726
  slides = extracted_json
727
  logger.info("Slide Agent generated %d slides: %s", len(slides), slides)
728
+ add_stage_output("Slides", json.dumps(slides, indent=2))
729
  if len(slides) != total_slides:
730
  if slide_retry_count < max_retries:
731
  slide_retry_count += 1
 
741
  html_files = create_slides(slides, title, instructor_name)
742
  if not html_files:
743
  logger.error("Failed to generate HTML slides")
744
+ add_stage_output("Slides", "Failed to generate HTML slides")
745
+ else:
746
+ add_stage_output("Slides", f"Successfully generated {len(html_files)} HTML slides")
747
  progress = 50
748
  label = "Scripts: generating..."
749
  yield (
 
770
  if extracted_json:
771
  scripts = extracted_json
772
  logger.info("Script Agent generated scripts for %d slides: %s", len(scripts), scripts)
773
+ add_stage_output("Scripts", json.dumps(scripts, indent=2))
774
  for i, script in enumerate(scripts):
775
  script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_script.txt")
776
  try:
777
  with open(script_file, "w", encoding="utf-8") as f:
778
  f.write(script)
779
  logger.info("Saved script to %s", script_file)
780
+ add_stage_output("Scripts", f"Saved script for slide {i+1}")
781
  except Exception as e:
782
  logger.error("Error saving script to %s: %s", script_file, str(e))
783
+ add_stage_output("Scripts", f"Failed to save script for slide {i+1}: {str(e)}")
784
  progress = 75
785
  label = "Script complete. Reviewing lecture materials..."
786
  yield (
 
805
  logger.info("Instructor Agent completed lecture review: %s", message.content)
806
  progress = 90
807
  label = "Lecture materials ready. Generating lecture speech..."
808
+ add_stage_output("Review", message.content)
809
  file_paths = [f for f in os.listdir(OUTPUT_DIR) if f.endswith(('.md', '.txt'))]
810
  file_paths.sort()
811
  file_paths = [os.path.join(OUTPUT_DIR, f) for f in file_paths]
 
900
 
901
  for i, script in enumerate(scripts):
902
  cleaned_script = clean_script_text(script)
903
+ audio_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_audio.mp3")
 
 
 
 
 
 
 
 
 
904
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
  for attempt in range(max_audio_retries + 1):
906
  try:
907
  current_text = cleaned_script
 
915
  if not success:
916
  raise RuntimeError("TTS generation failed")
917
 
918
+ logger.info("Generated speech for slide %d: %s", i + 1, audio_file)
919
  audio_files.append(audio_file)
920
+ audio_urls[i] = get_gradio_file_url(audio_file)
921
  progress = 90 + ((i + 1) / len(scripts)) * 10
922
  label = f"Generating lecture speech for slide {i + 1}/{len(scripts)}..."
923
+ add_stage_output("Speech", f"Generated speech for slide {i + 1}")
924
  file_paths.append(audio_file)
925
  yield (
926
  html_with_progress(label, progress),
 
937
  audio_urls[i] = None
938
  progress = 90 + ((i + 1) / len(scripts)) * 10
939
  label = f"Generating lecture speech for slide {i + 1}/{len(scripts)}..."
940
+ add_stage_output("Speech", f"Failed to generate audio for slide {i + 1}: {str(e)}")
941
  yield (
942
  html_with_progress(label, progress),
943
  file_paths,
 
2267
  return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(value=quiz, visible=True)
2268
 
2269
  # Helper to get fallback lecture context from form fields
 
2270
  def get_fallback_lecture_context(lecture_context, title_val, desc_val, style_val, audience_val):
2271
  # If slides/scripts missing, use form fields
2272
  if lecture_context and (lecture_context.get("slides") or lecture_context.get("scripts")):