Spaces:
Running
Running
visualize stage logs
Browse files
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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
895 |
audio_files.append(audio_file)
|
896 |
-
audio_urls[i] =
|
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")):
|