Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -16,7 +16,6 @@ from autogen_agentchat.messages import TextMessage, HandoffMessage, StructuredMe
|
|
16 |
from autogen_ext.models.anthropic import AnthropicChatCompletionClient
|
17 |
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
18 |
from autogen_ext.models.ollama import OllamaChatCompletionClient
|
19 |
-
from markdown_pdf import MarkdownPdf, Section
|
20 |
import traceback
|
21 |
import soundfile as sf
|
22 |
import tempfile
|
@@ -35,7 +34,6 @@ logging.basicConfig(
|
|
35 |
logger = logging.getLogger(__name__)
|
36 |
|
37 |
# Set up environment
|
38 |
-
# For Huggingface Spaces, use /tmp for temporary storage
|
39 |
if os.path.exists("/tmp"):
|
40 |
OUTPUT_DIR = "/tmp/outputs" # Use /tmp for Huggingface Spaces
|
41 |
else:
|
@@ -117,25 +115,21 @@ def clean_script_text(script):
|
|
117 |
logger.error("Invalid script input: %s", script)
|
118 |
return None
|
119 |
|
120 |
-
|
121 |
-
script = re.sub(r"
|
122 |
-
script = re.sub(r"
|
123 |
-
script = re.sub(r"Title:.*?\n|Content:.*?\n", "", script) # Remove metadata
|
124 |
script = script.replace("humanlike", "human-like").replace("problemsolving", "problem-solving")
|
125 |
-
script = re.sub(r"\s+", " ", script).strip()
|
126 |
|
127 |
-
# Convert bullet points to spoken cues
|
128 |
script = re.sub(r"^\s*-\s*", "So, ", script, flags=re.MULTILINE)
|
129 |
|
130 |
-
# Add non-verbal words randomly (e.g., "um," "you know," "like")
|
131 |
non_verbal = ["um, ", "you know, ", "like, "]
|
132 |
words = script.split()
|
133 |
for i in range(len(words) - 1, -1, -1):
|
134 |
-
if random.random() < 0.1:
|
135 |
words.insert(i, random.choice(non_verbal))
|
136 |
script = " ".join(words)
|
137 |
|
138 |
-
# Basic validation
|
139 |
if len(script) < 10:
|
140 |
logger.error("Cleaned script too short (%d characters): %s", len(script), script)
|
141 |
return None
|
@@ -143,7 +137,7 @@ def clean_script_text(script):
|
|
143 |
logger.info("Cleaned and naturalized script: %s", script)
|
144 |
return script
|
145 |
|
146 |
-
# Helper function to validate and convert speaker audio
|
147 |
async def validate_and_convert_speaker_audio(speaker_audio):
|
148 |
if not speaker_audio or not os.path.exists(speaker_audio):
|
149 |
logger.warning("Speaker audio file does not exist: %s. Using default voice.", speaker_audio)
|
@@ -155,12 +149,10 @@ async def validate_and_convert_speaker_audio(speaker_audio):
|
|
155 |
return None
|
156 |
|
157 |
try:
|
158 |
-
# Check file extension
|
159 |
ext = os.path.splitext(speaker_audio)[1].lower()
|
160 |
if ext == ".mp3":
|
161 |
logger.info("Converting MP3 to WAV: %s", speaker_audio)
|
162 |
audio = AudioSegment.from_mp3(speaker_audio)
|
163 |
-
# Convert to mono, 22050 Hz
|
164 |
audio = audio.set_channels(1).set_frame_rate(22050)
|
165 |
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False, dir=OUTPUT_DIR) as temp_file:
|
166 |
audio.export(temp_file.name, format="wav")
|
@@ -171,7 +163,6 @@ async def validate_and_convert_speaker_audio(speaker_audio):
|
|
171 |
logger.error("Unsupported audio format: %s", ext)
|
172 |
return None
|
173 |
|
174 |
-
# Validate WAV file
|
175 |
data, samplerate = sf.read(speaker_wav)
|
176 |
if samplerate < 16000 or samplerate > 48000:
|
177 |
logger.error("Invalid sample rate for %s: %d Hz", speaker_wav, samplerate)
|
@@ -215,7 +206,6 @@ def extract_json_from_message(message):
|
|
215 |
logger.warning("TextMessage content is not a string: %s", content)
|
216 |
return None
|
217 |
|
218 |
-
# Try standard JSON block with triple backticks
|
219 |
pattern = r"```json\s*(.*?)\s*```"
|
220 |
match = re.search(pattern, content, re.DOTALL)
|
221 |
if match:
|
@@ -226,10 +216,9 @@ def extract_json_from_message(message):
|
|
226 |
except json.JSONDecodeError as e:
|
227 |
logger.error("Failed to parse JSON from code block: %s", e)
|
228 |
|
229 |
-
# Try to find arrays or objects
|
230 |
json_patterns = [
|
231 |
-
r"\[\s*\{.*?\}\s*\]",
|
232 |
-
r"\{\s*\".*?\"\s*:.*?\}",
|
233 |
]
|
234 |
|
235 |
for pattern in json_patterns:
|
@@ -242,7 +231,6 @@ def extract_json_from_message(message):
|
|
242 |
except json.JSONDecodeError as e:
|
243 |
logger.error("Failed to parse JSON with pattern %s: %s", pattern, e)
|
244 |
|
245 |
-
# Try to find JSON anywhere in the content
|
246 |
try:
|
247 |
for i in range(len(content)):
|
248 |
for j in range(len(content), i, -1):
|
@@ -290,8 +278,8 @@ def extract_json_from_message(message):
|
|
290 |
logger.error("Failed to parse JSON from HandoffMessage: %s", e)
|
291 |
|
292 |
json_patterns = [
|
293 |
-
r"\[\s*\{.*?\}\s*\]",
|
294 |
-
r"\{\s*\".*?\"\s*:.*?\}",
|
295 |
]
|
296 |
|
297 |
for pattern in json_patterns:
|
@@ -310,26 +298,36 @@ def extract_json_from_message(message):
|
|
310 |
logger.warning("Unsupported message type for JSON extraction: %s", type(message))
|
311 |
return None
|
312 |
|
313 |
-
# Function to generate
|
314 |
-
def
|
315 |
try:
|
316 |
-
|
317 |
-
|
318 |
for i, slide in enumerate(slides):
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
|
|
|
|
|
|
|
|
326 |
"""
|
327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
|
329 |
-
logger.info(f"Generated
|
330 |
-
return
|
331 |
except Exception as e:
|
332 |
-
logger.error(f"Failed to generate
|
333 |
logger.error(traceback.format_exc())
|
334 |
return None
|
335 |
|
@@ -351,7 +349,6 @@ async def on_generate(api_service, api_key, serpapi_key, title, topic, instructi
|
|
351 |
"""
|
352 |
return
|
353 |
|
354 |
-
# Initialize TTS model
|
355 |
tts = None
|
356 |
try:
|
357 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
@@ -535,7 +532,6 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
535 |
)
|
536 |
task_result.messages.append(retry_message)
|
537 |
continue
|
538 |
-
# Save slide content to individual files
|
539 |
for i, slide in enumerate(slides):
|
540 |
content_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_content.txt")
|
541 |
try:
|
@@ -567,7 +563,6 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
567 |
if extracted_json:
|
568 |
scripts = extracted_json
|
569 |
logger.info("Script Agent generated scripts for %d slides", len(scripts))
|
570 |
-
# Save raw scripts to individual files
|
571 |
for i, script in enumerate(scripts):
|
572 |
script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_raw_script.txt")
|
573 |
try:
|
@@ -648,10 +643,9 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
648 |
"""
|
649 |
return
|
650 |
|
651 |
-
|
652 |
-
|
653 |
-
|
654 |
-
logger.error("Failed to generate HTML slides")
|
655 |
yield f"""
|
656 |
<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;">
|
657 |
<h2 style="color: #d9534f;">Failed to generate slides</h2>
|
@@ -672,13 +666,11 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
672 |
"""
|
673 |
return
|
674 |
|
675 |
-
# Process audio generation sequentially with retries
|
676 |
for i, script in enumerate(scripts):
|
677 |
cleaned_script = clean_script_text(script)
|
678 |
-
audio_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}.
|
679 |
script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_script.txt")
|
680 |
|
681 |
-
# Save cleaned script
|
682 |
try:
|
683 |
with open(script_file, "w", encoding="utf-8") as f:
|
684 |
f.write(cleaned_script or "")
|
@@ -727,36 +719,51 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
727 |
await asyncio.sleep(0.1)
|
728 |
break
|
729 |
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
734 |
|
735 |
html_output = f"""
|
736 |
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
737 |
-
<div id="slide-content" style="flex: 1; overflow: auto;">
|
738 |
-
|
739 |
</div>
|
740 |
<div style="padding: 20px;">
|
741 |
-
<div
|
742 |
-
|
743 |
</div>
|
744 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
745 |
<button onclick="prevSlide()" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
746 |
-
<button onclick="
|
747 |
<button onclick="nextSlide()" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
|
|
748 |
</div>
|
749 |
-
|
|
|
|
|
|
|
750 |
</div>
|
751 |
</div>
|
752 |
<script>
|
753 |
const lectureData = {slides_info};
|
754 |
let currentSlide = 0;
|
755 |
const totalSlides = lectureData.slides.length;
|
756 |
-
const slideCounter = document.getElementById('slide-counter');
|
757 |
-
const progressFill = document.getElementById('progress-fill');
|
758 |
let audioElements = [];
|
759 |
-
let
|
760 |
|
761 |
for (let i = 0; i < totalSlides; i++) {{
|
762 |
if (lectureData.audioFiles && lectureData.audioFiles[i]) {{
|
@@ -767,24 +774,19 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
767 |
}}
|
768 |
}}
|
769 |
|
770 |
-
function
|
771 |
-
|
772 |
-
|
773 |
-
|
774 |
-
slideCounter.textContent = `Slide ${{currentSlide + 1}} of ${{totalSlides}}`;
|
775 |
-
progressFill.style.width = `${{(currentSlide + 1) / totalSlides * 100}}%`;
|
776 |
-
|
777 |
-
if (currentAudio) {{
|
778 |
-
currentAudio.pause();
|
779 |
-
currentAudio.currentTime = 0;
|
780 |
-
}}
|
781 |
|
782 |
-
|
783 |
-
|
784 |
-
|
785 |
-
|
786 |
-
|
787 |
-
|
|
|
|
|
788 |
}}
|
789 |
|
790 |
function prevSlide() {{
|
@@ -801,27 +803,52 @@ Example: 'Received {total_slides} slides and {total_slides} scripts. Lecture is
|
|
801 |
}}
|
802 |
}}
|
803 |
|
804 |
-
function
|
805 |
-
if (
|
806 |
-
|
807 |
-
|
808 |
-
|
809 |
-
|
|
|
810 |
}}
|
|
|
|
|
|
|
|
|
|
|
811 |
}}
|
812 |
|
813 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
814 |
if (audio) {{
|
815 |
-
audio.
|
816 |
-
|
817 |
-
|
818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
819 |
}});
|
|
|
|
|
|
|
820 |
}}
|
821 |
-
}}
|
822 |
|
823 |
// Initialize first slide
|
824 |
-
|
825 |
</script>
|
826 |
"""
|
827 |
logger.info("Lecture generation completed successfully")
|
|
|
16 |
from autogen_ext.models.anthropic import AnthropicChatCompletionClient
|
17 |
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
18 |
from autogen_ext.models.ollama import OllamaChatCompletionClient
|
|
|
19 |
import traceback
|
20 |
import soundfile as sf
|
21 |
import tempfile
|
|
|
34 |
logger = logging.getLogger(__name__)
|
35 |
|
36 |
# Set up environment
|
|
|
37 |
if os.path.exists("/tmp"):
|
38 |
OUTPUT_DIR = "/tmp/outputs" # Use /tmp for Huggingface Spaces
|
39 |
else:
|
|
|
115 |
logger.error("Invalid script input: %s", script)
|
116 |
return None
|
117 |
|
118 |
+
script = re.sub(r"\*\*Slide \d+:.*?\*\*", "", script)
|
119 |
+
script = re.sub(r"\[.*?\]", "", script)
|
120 |
+
script = re.sub(r"Title:.*?\n|Content:.*?\n", "", script)
|
|
|
121 |
script = script.replace("humanlike", "human-like").replace("problemsolving", "problem-solving")
|
122 |
+
script = re.sub(r"\s+", " ", script).strip()
|
123 |
|
|
|
124 |
script = re.sub(r"^\s*-\s*", "So, ", script, flags=re.MULTILINE)
|
125 |
|
|
|
126 |
non_verbal = ["um, ", "you know, ", "like, "]
|
127 |
words = script.split()
|
128 |
for i in range(len(words) - 1, -1, -1):
|
129 |
+
if random.random() < 0.1:
|
130 |
words.insert(i, random.choice(non_verbal))
|
131 |
script = " ".join(words)
|
132 |
|
|
|
133 |
if len(script) < 10:
|
134 |
logger.error("Cleaned script too short (%d characters): %s", len(script), script)
|
135 |
return None
|
|
|
137 |
logger.info("Cleaned and naturalized script: %s", script)
|
138 |
return script
|
139 |
|
140 |
+
# Helper function to validate and convert speaker audio
|
141 |
async def validate_and_convert_speaker_audio(speaker_audio):
|
142 |
if not speaker_audio or not os.path.exists(speaker_audio):
|
143 |
logger.warning("Speaker audio file does not exist: %s. Using default voice.", speaker_audio)
|
|
|
149 |
return None
|
150 |
|
151 |
try:
|
|
|
152 |
ext = os.path.splitext(speaker_audio)[1].lower()
|
153 |
if ext == ".mp3":
|
154 |
logger.info("Converting MP3 to WAV: %s", speaker_audio)
|
155 |
audio = AudioSegment.from_mp3(speaker_audio)
|
|
|
156 |
audio = audio.set_channels(1).set_frame_rate(22050)
|
157 |
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False, dir=OUTPUT_DIR) as temp_file:
|
158 |
audio.export(temp_file.name, format="wav")
|
|
|
163 |
logger.error("Unsupported audio format: %s", ext)
|
164 |
return None
|
165 |
|
|
|
166 |
data, samplerate = sf.read(speaker_wav)
|
167 |
if samplerate < 16000 or samplerate > 48000:
|
168 |
logger.error("Invalid sample rate for %s: %d Hz", speaker_wav, samplerate)
|
|
|
206 |
logger.warning("TextMessage content is not a string: %s", content)
|
207 |
return None
|
208 |
|
|
|
209 |
pattern = r"```json\s*(.*?)\s*```"
|
210 |
match = re.search(pattern, content, re.DOTALL)
|
211 |
if match:
|
|
|
216 |
except json.JSONDecodeError as e:
|
217 |
logger.error("Failed to parse JSON from code block: %s", e)
|
218 |
|
|
|
219 |
json_patterns = [
|
220 |
+
r"\[\s*\{.*?\}\s*\]",
|
221 |
+
r"\{\s*\".*?\"\s*:.*?\}",
|
222 |
]
|
223 |
|
224 |
for pattern in json_patterns:
|
|
|
231 |
except json.JSONDecodeError as e:
|
232 |
logger.error("Failed to parse JSON with pattern %s: %s", pattern, e)
|
233 |
|
|
|
234 |
try:
|
235 |
for i in range(len(content)):
|
236 |
for j in range(len(content), i, -1):
|
|
|
278 |
logger.error("Failed to parse JSON from HandoffMessage: %s", e)
|
279 |
|
280 |
json_patterns = [
|
281 |
+
r"\[\s*\{.*?\}\s*\]",
|
282 |
+
r"\{\s*\".*?\"\s*:.*?\}",
|
283 |
]
|
284 |
|
285 |
for pattern in json_patterns:
|
|
|
298 |
logger.warning("Unsupported message type for JSON extraction: %s", type(message))
|
299 |
return None
|
300 |
|
301 |
+
# Function to generate Markdown slides
|
302 |
+
def generate_markdown_slides(slides, title, speaker="Prof. AI Feynman", date="April 26th, 2025"):
|
303 |
try:
|
304 |
+
markdown_slides = []
|
|
|
305 |
for i, slide in enumerate(slides):
|
306 |
+
slide_number = i + 1
|
307 |
+
content = slide['content']
|
308 |
+
|
309 |
+
# First and last slides have no header/footer
|
310 |
+
if i == 0 or i == len(slides) - 1:
|
311 |
+
slide_md = f"""
|
312 |
+
# {slide['title']}
|
313 |
+
{content}
|
314 |
+
|
315 |
+
**{speaker}**
|
316 |
+
*{date}*
|
317 |
"""
|
318 |
+
else:
|
319 |
+
slide_md = f"""
|
320 |
+
##### Slide {slide_number}, {slide['title']}
|
321 |
+
{content}
|
322 |
+
|
323 |
+
, {title} {speaker}, {date}
|
324 |
+
"""
|
325 |
+
markdown_slides.append(slide_md)
|
326 |
|
327 |
+
logger.info(f"Generated Markdown slides for: {title}")
|
328 |
+
return markdown_slides
|
329 |
except Exception as e:
|
330 |
+
logger.error(f"Failed to generate Markdown slides: {str(e)}")
|
331 |
logger.error(traceback.format_exc())
|
332 |
return None
|
333 |
|
|
|
349 |
"""
|
350 |
return
|
351 |
|
|
|
352 |
tts = None
|
353 |
try:
|
354 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
532 |
)
|
533 |
task_result.messages.append(retry_message)
|
534 |
continue
|
|
|
535 |
for i, slide in enumerate(slides):
|
536 |
content_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_content.txt")
|
537 |
try:
|
|
|
563 |
if extracted_json:
|
564 |
scripts = extracted_json
|
565 |
logger.info("Script Agent generated scripts for %d slides", len(scripts))
|
|
|
566 |
for i, script in enumerate(scripts):
|
567 |
script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_raw_script.txt")
|
568 |
try:
|
|
|
643 |
"""
|
644 |
return
|
645 |
|
646 |
+
markdown_slides = generate_markdown_slides(slides, title)
|
647 |
+
if not markdown_slides:
|
648 |
+
logger.error("Failed to generate Markdown slides")
|
|
|
649 |
yield f"""
|
650 |
<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;">
|
651 |
<h2 style="color: #d9534f;">Failed to generate slides</h2>
|
|
|
666 |
"""
|
667 |
return
|
668 |
|
|
|
669 |
for i, script in enumerate(scripts):
|
670 |
cleaned_script = clean_script_text(script)
|
671 |
+
audio_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}.mp3")
|
672 |
script_file = os.path.join(OUTPUT_DIR, f"slide_{i+1}_script.txt")
|
673 |
|
|
|
674 |
try:
|
675 |
with open(script_file, "w", encoding="utf-8") as f:
|
676 |
f.write(cleaned_script or "")
|
|
|
719 |
await asyncio.sleep(0.1)
|
720 |
break
|
721 |
|
722 |
+
# Collect .txt files for download
|
723 |
+
txt_files = [f for f in os.listdir(OUTPUT_DIR) if f.endswith('.txt')]
|
724 |
+
txt_files.sort() # Sort for consistent display
|
725 |
+
txt_links = ""
|
726 |
+
for txt_file in txt_files:
|
727 |
+
file_path = os.path.join(OUTPUT_DIR, txt_file)
|
728 |
+
txt_links += f'<a href="file/{file_path}" download>{txt_file}</a> '
|
729 |
+
|
730 |
+
# Generate audio timeline
|
731 |
+
audio_timeline = ""
|
732 |
+
for i, audio_file in enumerate(audio_files):
|
733 |
+
if audio_file:
|
734 |
+
audio_timeline += f'<span id="audio-{i+1}">{os.path.basename(audio_file)}</span> '
|
735 |
+
else:
|
736 |
+
audio_timeline += f'<span id="audio-{i+1}">slide_{i+1}.mp3</span> '
|
737 |
+
|
738 |
+
slides_info = json.dumps({"slides": markdown_slides, "audioFiles": audio_files})
|
739 |
|
740 |
html_output = f"""
|
741 |
<div id="lecture-container" style="height: 700px; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between;">
|
742 |
+
<div id="slide-content" style="flex: 1; overflow: auto; padding: 20px; text-align: center;">
|
743 |
+
<!-- Slides will be rendered here -->
|
744 |
</div>
|
745 |
<div style="padding: 20px;">
|
746 |
+
<div style="text-align: center; margin-bottom: 10px;">
|
747 |
+
{audio_timeline}
|
748 |
</div>
|
749 |
<div style="display: flex; justify-content: center; margin-bottom: 10px;">
|
750 |
<button onclick="prevSlide()" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏮</button>
|
751 |
+
<button onclick="playAll()" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏯</button>
|
752 |
<button onclick="nextSlide()" style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">⏭</button>
|
753 |
+
<button style="border-radius: 50%; width: 40px; height: 40px; margin: 0 5px; font-size: 1.2em; cursor: pointer;">☐</button>
|
754 |
</div>
|
755 |
+
</div>
|
756 |
+
<div style="padding: 10px; text-align: center;">
|
757 |
+
<h4>Download Generated Files:</h4>
|
758 |
+
{txt_links}
|
759 |
</div>
|
760 |
</div>
|
761 |
<script>
|
762 |
const lectureData = {slides_info};
|
763 |
let currentSlide = 0;
|
764 |
const totalSlides = lectureData.slides.length;
|
|
|
|
|
765 |
let audioElements = [];
|
766 |
+
let isPlayingAll = false;
|
767 |
|
768 |
for (let i = 0; i < totalSlides; i++) {{
|
769 |
if (lectureData.audioFiles && lectureData.audioFiles[i]) {{
|
|
|
774 |
}}
|
775 |
}}
|
776 |
|
777 |
+
function renderSlide() {{
|
778 |
+
const slideContent = document.getElementById('slide-content');
|
779 |
+
slideContent.innerHTML = lectureData.slides[currentSlide];
|
780 |
+
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
781 |
|
782 |
+
function updateSlide() {{
|
783 |
+
renderSlide();
|
784 |
+
audioElements.forEach(audio => {{
|
785 |
+
if (audio) {{
|
786 |
+
audio.pause();
|
787 |
+
audio.currentTime = 0;
|
788 |
+
}}
|
789 |
+
}});
|
790 |
}}
|
791 |
|
792 |
function prevSlide() {{
|
|
|
803 |
}}
|
804 |
}}
|
805 |
|
806 |
+
function playAll() {{
|
807 |
+
if (isPlayingAll) {{
|
808 |
+
audioElements.forEach(audio => {{
|
809 |
+
if (audio) audio.pause();
|
810 |
+
}});
|
811 |
+
isPlayingAll = false;
|
812 |
+
return;
|
813 |
}}
|
814 |
+
|
815 |
+
isPlayingAll = true;
|
816 |
+
currentSlide = 0;
|
817 |
+
updateSlide();
|
818 |
+
playCurrentSlide();
|
819 |
}}
|
820 |
|
821 |
+
function playCurrentSlide() {{
|
822 |
+
if (!isPlayingAll || currentSlide >= totalSlides) {{
|
823 |
+
isPlayingAll = false;
|
824 |
+
return;
|
825 |
+
}}
|
826 |
+
|
827 |
+
const audio = audioElements[currentSlide];
|
828 |
if (audio) {{
|
829 |
+
audio.play().then(() => {{
|
830 |
+
audio.addEventListener('ended', () => {{
|
831 |
+
currentSlide++;
|
832 |
+
if (currentSlide < totalSlides) {{
|
833 |
+
updateSlide();
|
834 |
+
playCurrentSlide();
|
835 |
+
}} else {{
|
836 |
+
isPlayingAll = false;
|
837 |
+
}}
|
838 |
+
}}, {{ once: true }});
|
839 |
+
}}).catch(e => {{
|
840 |
+
console.error('Audio play failed:', e);
|
841 |
+
currentSlide++;
|
842 |
+
playCurrentSlide();
|
843 |
}});
|
844 |
+
}} else {{
|
845 |
+
currentSlide++;
|
846 |
+
playCurrentSlide();
|
847 |
}}
|
848 |
+
}}
|
849 |
|
850 |
// Initialize first slide
|
851 |
+
renderSlide();
|
852 |
</script>
|
853 |
"""
|
854 |
logger.info("Lecture generation completed successfully")
|