Spaces:
Running
Running
burtenshaw
commited on
Commit
·
de93716
1
Parent(s):
44176d4
add progress updates
Browse files- app/app.py +122 -47
app/app.py
CHANGED
@@ -278,14 +278,16 @@ def load_css(css_filename="style.scss"):
|
|
278 |
# --- Gradio Workflow Functions ---
|
279 |
|
280 |
|
281 |
-
def step1_fetch_and_generate_presentation(
|
|
|
|
|
282 |
"""Fetches content, generates presentation markdown, prepares editor, and copies template. Uses caching based on URL."""
|
283 |
if not url:
|
284 |
raise gr.Error("Please enter a URL.")
|
285 |
logger.info(f"Step 1: Fetching & Generating for {url}")
|
286 |
|
287 |
status_update = f"Starting Step 1: Fetching content from {url}..."
|
288 |
-
yield status_update
|
289 |
|
290 |
# --- Cache Check ---
|
291 |
try:
|
@@ -338,11 +340,11 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
338 |
status_update = (
|
339 |
"Loaded presentation from cache. Preparing editor..."
|
340 |
)
|
341 |
-
yield status_update
|
342 |
logger.info(f"Using cached data for {len(slides_data)} slides.")
|
343 |
# Return updates for the UI state and controls
|
344 |
yield (
|
345 |
-
status_update,
|
346 |
temp_dir,
|
347 |
md_path,
|
348 |
slides_data,
|
@@ -368,14 +370,14 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
368 |
raise gr.Error("LLM Client not initialized. Check API Key.")
|
369 |
|
370 |
status_update = "Fetching webpage content..."
|
371 |
-
yield status_update
|
372 |
web_content = fetch_webpage_content(url)
|
373 |
if not web_content:
|
374 |
raise gr.Error("Failed to fetch or parse content from the URL.")
|
375 |
|
376 |
progress(0.3, desc="Generating presentation with LLM...")
|
377 |
status_update = "Generating presentation with LLM..."
|
378 |
-
yield status_update
|
379 |
try:
|
380 |
presentation_md = generate_presentation_with_llm(
|
381 |
hf_client, LLM_MODEL, PRESENTATION_PROMPT, web_content, url
|
@@ -400,7 +402,7 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
400 |
|
401 |
progress(0.7, desc="Parsing presentation slides...")
|
402 |
status_update = "Parsing presentation slides..."
|
403 |
-
yield status_update
|
404 |
slides_data = parse_presentation_markdown(presentation_md)
|
405 |
if not slides_data:
|
406 |
logger.error("Parsing markdown resulted in zero slides.")
|
@@ -444,12 +446,12 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
444 |
|
445 |
progress(0.9, desc="Preparing editor...")
|
446 |
status_update = "Generated presentation. Preparing editor..."
|
447 |
-
yield status_update
|
448 |
logger.info(f"Prepared data for {len(slides_data)} slides.")
|
449 |
|
450 |
# Return updates for the UI state and controls
|
451 |
yield (
|
452 |
-
status_update,
|
453 |
temp_dir,
|
454 |
md_path,
|
455 |
slides_data,
|
@@ -461,10 +463,10 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
461 |
except Exception as e:
|
462 |
logger.error(f"Error in step 1 (fetch/generate): {e}", exc_info=True)
|
463 |
status_update = f"Error during presentation setup: {e}"
|
464 |
-
yield status_update
|
465 |
# Need to yield the correct number of outputs even on error to avoid issues
|
466 |
yield (
|
467 |
-
status_update,
|
468 |
None,
|
469 |
None,
|
470 |
[],
|
@@ -480,6 +482,7 @@ def step2_build_slides(
|
|
480 |
state_temp_dir,
|
481 |
state_md_path,
|
482 |
state_slides_data,
|
|
|
483 |
*editors,
|
484 |
progress=gr.Progress(track_tqdm=True),
|
485 |
):
|
@@ -487,6 +490,8 @@ def step2_build_slides(
|
|
487 |
if not all([state_temp_dir, state_md_path, state_slides_data]):
|
488 |
raise gr.Error("Session state missing.")
|
489 |
logger.info("Step 2: Building Slides (PDF + Images)")
|
|
|
|
|
490 |
|
491 |
num_slides = len(state_slides_data)
|
492 |
MAX_SLIDES = 20
|
@@ -499,6 +504,8 @@ def step2_build_slides(
|
|
499 |
raise gr.Error("Editor input mismatch.")
|
500 |
|
501 |
progress(0.1, desc="Saving edited markdown...")
|
|
|
|
|
502 |
updated_slides = []
|
503 |
for i in range(num_slides):
|
504 |
updated_slides.append(
|
@@ -510,20 +517,54 @@ def step2_build_slides(
|
|
510 |
f.write(updated_md)
|
511 |
logger.info(f"Saved edited markdown: {state_md_path}")
|
512 |
except IOError as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
513 |
raise gr.Error(f"Failed to save markdown: {e}")
|
514 |
|
515 |
progress(0.3, desc="Generating PDF...")
|
|
|
|
|
516 |
pdf_output_path = os.path.join(state_temp_dir, "presentation.pdf")
|
517 |
try:
|
518 |
generated_pdf_path = generate_pdf_from_markdown(state_md_path, pdf_output_path)
|
519 |
if not generated_pdf_path:
|
520 |
raise gr.Error("PDF generation failed (check logs).")
|
521 |
except gr.Error as e:
|
522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
523 |
except Exception as e:
|
524 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
525 |
|
526 |
progress(0.7, desc="Converting PDF to images...")
|
|
|
|
|
527 |
pdf_images = []
|
528 |
try:
|
529 |
pdf_images = convert_pdf_to_images(
|
@@ -535,9 +576,21 @@ def step2_build_slides(
|
|
535 |
if len(pdf_images) != num_slides:
|
536 |
warning_msg = f"Warning: PDF page count ({len(pdf_images)}) != slide count ({num_slides}). Images might mismatch."
|
537 |
gr.Warning(warning_msg)
|
|
|
538 |
# Pad or truncate? For now, just return what we have, UI update logic handles MAX_SLIDES
|
539 |
except Exception as e:
|
540 |
logger.error(f"Error converting PDF to images: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
541 |
raise gr.Error(f"Failed to convert PDF to images: {e}")
|
542 |
|
543 |
info_msg = f"Built {len(pdf_images)} slide images. Ready for Step 3."
|
@@ -545,7 +598,7 @@ def step2_build_slides(
|
|
545 |
progress(1.0, desc="Slide build complete.")
|
546 |
status_update = f"Step 2 Complete: {info_msg}"
|
547 |
yield (
|
548 |
-
status_update,
|
549 |
generated_pdf_path,
|
550 |
pdf_images, # Return the list of image paths
|
551 |
gr.update(visible=True), # btn_generate_audio
|
@@ -556,18 +609,19 @@ def step2_build_slides(
|
|
556 |
|
557 |
def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
558 |
"""Generates audio files for the speaker notes using edited content."""
|
559 |
-
# Args structure:
|
560 |
# args[0]: state_temp_dir
|
561 |
# args[1]: state_md_path
|
562 |
-
# args[2]: original_slides_data
|
563 |
-
# args[3]
|
564 |
-
# args[4
|
565 |
-
# args[4 + MAX_SLIDES :]: values from all_notes_textboxes
|
566 |
|
567 |
state_temp_dir = args[0]
|
568 |
state_md_path = args[1]
|
569 |
original_slides_data = args[2]
|
570 |
-
|
|
|
|
|
571 |
num_slides = len(original_slides_data)
|
572 |
if num_slides == 0:
|
573 |
logger.error("Step 3 (Audio) called with zero slides data.")
|
@@ -575,9 +629,9 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
575 |
|
576 |
MAX_SLIDES = 20 # Ensure this matches UI definition
|
577 |
# --- Adjust indices based on adding status_textbox input ---
|
578 |
-
#
|
579 |
-
code_editors_start_index = 4
|
580 |
-
notes_textboxes_start_index = 4 + MAX_SLIDES
|
581 |
|
582 |
# Slice the *actual* edited values based on num_slides
|
583 |
edited_contents = args[
|
@@ -600,6 +654,9 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
600 |
)
|
601 |
|
602 |
logger.info(f"Processing {num_slides} slides for audio generation.")
|
|
|
|
|
|
|
603 |
audio_dir = os.path.join(state_temp_dir, "audio")
|
604 |
os.makedirs(audio_dir, exist_ok=True)
|
605 |
|
@@ -607,6 +664,8 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
607 |
# This might be redundant if users don't edit notes between PDF and Audio steps,
|
608 |
# but ensures the audio matches the *latest* notes displayed.
|
609 |
progress(0.1, desc="Saving latest notes...")
|
|
|
|
|
610 |
updated_slides_data = []
|
611 |
for i in range(num_slides):
|
612 |
updated_slides_data.append(
|
@@ -623,11 +682,20 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
623 |
f.write(updated_markdown)
|
624 |
logger.info(f"Updated presentation markdown before audio gen: {state_md_path}")
|
625 |
except IOError as e:
|
626 |
-
logger.error(f"Failed to save updated markdown before audio gen: {e}")
|
627 |
warning_msg = f"Warning: Could not save latest notes to markdown file: {e}"
|
628 |
gr.Warning(warning_msg)
|
|
|
|
|
629 |
status_update = f"Warning: {warning_msg}"
|
630 |
-
yield
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
631 |
|
632 |
generated_audio_paths = ["" for _ in range(num_slides)]
|
633 |
audio_generation_failed = False
|
@@ -641,6 +709,9 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
641 |
progress_val,
|
642 |
desc=f"Audio slide {slide_num}/{num_slides}",
|
643 |
)
|
|
|
|
|
|
|
644 |
output_file_path = Path(audio_dir) / f"{slide_num}.wav"
|
645 |
if not note_text or not note_text.strip():
|
646 |
try: # Generate silence
|
@@ -704,7 +775,7 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
704 |
|
705 |
# Return tuple including status update + original outputs
|
706 |
yield (
|
707 |
-
status_update,
|
708 |
audio_dir,
|
709 |
gr.update(visible=True), # btn_generate_video
|
710 |
gr.update(visible=False), # btn_generate_audio
|
@@ -717,6 +788,7 @@ def step4_generate_video(
|
|
717 |
state_temp_dir,
|
718 |
state_audio_dir,
|
719 |
state_pdf_path, # Use PDF path from state
|
|
|
720 |
progress=gr.Progress(track_tqdm=True),
|
721 |
):
|
722 |
"""Generates the final video using PDF images and audio files."""
|
@@ -730,6 +802,9 @@ def step4_generate_video(
|
|
730 |
)
|
731 |
|
732 |
video_output_path = os.path.join(state_temp_dir, "final_presentation.mp4")
|
|
|
|
|
|
|
733 |
progress(0.1, desc="Preparing video components...")
|
734 |
pdf_images = [] # Initialize to ensure cleanup happens
|
735 |
try:
|
@@ -738,13 +813,14 @@ def step4_generate_video(
|
|
738 |
if not audio_files:
|
739 |
warning_msg = f"Warning: No WAV files found in {state_audio_dir}. Video might lack audio."
|
740 |
logger.warning(warning_msg)
|
741 |
-
status_update
|
742 |
-
yield status_update
|
743 |
# Decide whether to proceed with silent video or error out
|
744 |
# raise gr.Error(f"No audio files found in {state_audio_dir}")
|
745 |
|
746 |
# Convert PDF to images
|
747 |
progress(0.2, desc="Converting PDF to images...")
|
|
|
|
|
748 |
pdf_images = convert_pdf_to_images(state_pdf_path, dpi=150)
|
749 |
if not pdf_images:
|
750 |
raise gr.Error(f"Failed to convert PDF ({state_pdf_path}) to images.")
|
@@ -754,10 +830,14 @@ def step4_generate_video(
|
|
754 |
if len(pdf_images) != len(audio_files):
|
755 |
warning_msg = f"Warning: Mismatch: {len(pdf_images)} PDF pages vs {len(audio_files)} audio files. Video clips might have incorrect durations or missing audio."
|
756 |
logger.warning(warning_msg)
|
757 |
-
status_update
|
758 |
-
yield status_update
|
|
|
759 |
|
760 |
progress(0.5, desc="Creating individual video clips...")
|
|
|
|
|
|
|
761 |
buffer_seconds = 1.0
|
762 |
output_fps = 10
|
763 |
video_clips = create_video_clips(
|
@@ -768,11 +848,17 @@ def step4_generate_video(
|
|
768 |
raise gr.Error("Failed to create any video clips.")
|
769 |
|
770 |
progress(0.8, desc="Concatenating clips...")
|
|
|
|
|
|
|
771 |
concatenate_clips(video_clips, video_output_path, output_fps)
|
772 |
|
773 |
logger.info(f"Video concatenation complete: {video_output_path}")
|
774 |
|
775 |
progress(0.95, desc="Cleaning up temp images...")
|
|
|
|
|
|
|
776 |
cleanup_temp_files(pdf_images) # Pass the list of image paths
|
777 |
|
778 |
except Exception as e:
|
@@ -780,11 +866,8 @@ def step4_generate_video(
|
|
780 |
cleanup_temp_files(pdf_images)
|
781 |
logger.error(f"Video generation failed: {e}", exc_info=True)
|
782 |
status_update = f"Video generation failed: {e}"
|
783 |
-
yield (
|
784 |
-
|
785 |
-
gr.update(),
|
786 |
-
gr.update(visible=True),
|
787 |
-
) # Keep button visible
|
788 |
raise gr.Error(f"Video generation failed: {e}")
|
789 |
|
790 |
info_msg = f"Video generated: {os.path.basename(video_output_path)}"
|
@@ -794,7 +877,7 @@ def step4_generate_video(
|
|
794 |
|
795 |
# Return tuple including status update
|
796 |
yield (
|
797 |
-
status_update,
|
798 |
gr.update(value=video_output_path, visible=True), # video_output
|
799 |
gr.update(visible=False), # btn_generate_video
|
800 |
)
|
@@ -1044,7 +1127,7 @@ with gr.Blocks(
|
|
1044 |
]
|
1045 |
btn_fetch_generate.click(
|
1046 |
fn=step1_fetch_and_generate_presentation,
|
1047 |
-
inputs=[input_url],
|
1048 |
outputs=step1_outputs,
|
1049 |
show_progress="full",
|
1050 |
).then(
|
@@ -1083,7 +1166,7 @@ with gr.Blocks(
|
|
1083 |
|
1084 |
# Step 2 Click Handler
|
1085 |
step2_inputs = (
|
1086 |
-
[state_temp_dir, state_md_path, state_slides_data]
|
1087 |
+ all_code_editors
|
1088 |
+ all_notes_textboxes
|
1089 |
)
|
@@ -1119,18 +1202,11 @@ with gr.Blocks(
|
|
1119 |
).then(lambda: gr.update(selected=2), outputs=tabs_widget) # Switch to Tab 3
|
1120 |
|
1121 |
# Step 3 Click Handler
|
1122 |
-
# Need to add status_textbox to inputs for step 3
|
1123 |
step3_inputs = (
|
1124 |
-
[
|
1125 |
-
state_temp_dir,
|
1126 |
-
state_md_path,
|
1127 |
-
state_slides_data,
|
1128 |
-
status_textbox,
|
1129 |
-
] # Added status_textbox
|
1130 |
+ all_code_editors
|
1131 |
+ all_notes_textboxes
|
1132 |
)
|
1133 |
-
# Need to add status_textbox to outputs for step 3
|
1134 |
step3_outputs = (
|
1135 |
[
|
1136 |
status_textbox, # Added status output
|
@@ -1156,7 +1232,6 @@ with gr.Blocks(
|
|
1156 |
)
|
1157 |
|
1158 |
# Step 4 Click Handler
|
1159 |
-
# Add status_textbox to inputs and outputs
|
1160 |
step4_inputs = [state_temp_dir, state_audio_dir, state_pdf_path, status_textbox]
|
1161 |
step4_outputs = [
|
1162 |
status_textbox, # Added status output
|
|
|
278 |
# --- Gradio Workflow Functions ---
|
279 |
|
280 |
|
281 |
+
def step1_fetch_and_generate_presentation(
|
282 |
+
url, status_textbox, progress=gr.Progress(track_tqdm=True)
|
283 |
+
):
|
284 |
"""Fetches content, generates presentation markdown, prepares editor, and copies template. Uses caching based on URL."""
|
285 |
if not url:
|
286 |
raise gr.Error("Please enter a URL.")
|
287 |
logger.info(f"Step 1: Fetching & Generating for {url}")
|
288 |
|
289 |
status_update = f"Starting Step 1: Fetching content from {url}..."
|
290 |
+
yield {status_textbox: gr.update(value=status_update)}
|
291 |
|
292 |
# --- Cache Check ---
|
293 |
try:
|
|
|
340 |
status_update = (
|
341 |
"Loaded presentation from cache. Preparing editor..."
|
342 |
)
|
343 |
+
yield {status_textbox: gr.update(value=status_update)}
|
344 |
logger.info(f"Using cached data for {len(slides_data)} slides.")
|
345 |
# Return updates for the UI state and controls
|
346 |
yield (
|
347 |
+
gr.update(value=status_update),
|
348 |
temp_dir,
|
349 |
md_path,
|
350 |
slides_data,
|
|
|
370 |
raise gr.Error("LLM Client not initialized. Check API Key.")
|
371 |
|
372 |
status_update = "Fetching webpage content..."
|
373 |
+
yield {status_textbox: gr.update(value=status_update)}
|
374 |
web_content = fetch_webpage_content(url)
|
375 |
if not web_content:
|
376 |
raise gr.Error("Failed to fetch or parse content from the URL.")
|
377 |
|
378 |
progress(0.3, desc="Generating presentation with LLM...")
|
379 |
status_update = "Generating presentation with LLM..."
|
380 |
+
yield {status_textbox: gr.update(value=status_update)}
|
381 |
try:
|
382 |
presentation_md = generate_presentation_with_llm(
|
383 |
hf_client, LLM_MODEL, PRESENTATION_PROMPT, web_content, url
|
|
|
402 |
|
403 |
progress(0.7, desc="Parsing presentation slides...")
|
404 |
status_update = "Parsing presentation slides..."
|
405 |
+
yield {status_textbox: gr.update(value=status_update)}
|
406 |
slides_data = parse_presentation_markdown(presentation_md)
|
407 |
if not slides_data:
|
408 |
logger.error("Parsing markdown resulted in zero slides.")
|
|
|
446 |
|
447 |
progress(0.9, desc="Preparing editor...")
|
448 |
status_update = "Generated presentation. Preparing editor..."
|
449 |
+
yield {status_textbox: gr.update(value=status_update)}
|
450 |
logger.info(f"Prepared data for {len(slides_data)} slides.")
|
451 |
|
452 |
# Return updates for the UI state and controls
|
453 |
yield (
|
454 |
+
gr.update(value=status_update),
|
455 |
temp_dir,
|
456 |
md_path,
|
457 |
slides_data,
|
|
|
463 |
except Exception as e:
|
464 |
logger.error(f"Error in step 1 (fetch/generate): {e}", exc_info=True)
|
465 |
status_update = f"Error during presentation setup: {e}"
|
466 |
+
yield {status_textbox: gr.update(value=status_update)}
|
467 |
# Need to yield the correct number of outputs even on error to avoid issues
|
468 |
yield (
|
469 |
+
gr.update(value=status_update),
|
470 |
None,
|
471 |
None,
|
472 |
[],
|
|
|
482 |
state_temp_dir,
|
483 |
state_md_path,
|
484 |
state_slides_data,
|
485 |
+
status_textbox,
|
486 |
*editors,
|
487 |
progress=gr.Progress(track_tqdm=True),
|
488 |
):
|
|
|
490 |
if not all([state_temp_dir, state_md_path, state_slides_data]):
|
491 |
raise gr.Error("Session state missing.")
|
492 |
logger.info("Step 2: Building Slides (PDF + Images)")
|
493 |
+
status_update = "Starting Step 2: Building slides..."
|
494 |
+
yield {status_textbox: gr.update(value=status_update)}
|
495 |
|
496 |
num_slides = len(state_slides_data)
|
497 |
MAX_SLIDES = 20
|
|
|
504 |
raise gr.Error("Editor input mismatch.")
|
505 |
|
506 |
progress(0.1, desc="Saving edited markdown...")
|
507 |
+
status_update = "Saving edited markdown..."
|
508 |
+
yield {status_textbox: gr.update(value=status_update)}
|
509 |
updated_slides = []
|
510 |
for i in range(num_slides):
|
511 |
updated_slides.append(
|
|
|
517 |
f.write(updated_md)
|
518 |
logger.info(f"Saved edited markdown: {state_md_path}")
|
519 |
except IOError as e:
|
520 |
+
status_update = f"Failed to save markdown: {e}"
|
521 |
+
yield {status_textbox: gr.update(value=status_update)}
|
522 |
+
yield (
|
523 |
+
gr.update(value=status_update),
|
524 |
+
None,
|
525 |
+
[],
|
526 |
+
gr.update(),
|
527 |
+
gr.update(),
|
528 |
+
gr.update(),
|
529 |
+
)
|
530 |
raise gr.Error(f"Failed to save markdown: {e}")
|
531 |
|
532 |
progress(0.3, desc="Generating PDF...")
|
533 |
+
status_update = "Generating PDF..."
|
534 |
+
yield {status_textbox: gr.update(value=status_update)}
|
535 |
pdf_output_path = os.path.join(state_temp_dir, "presentation.pdf")
|
536 |
try:
|
537 |
generated_pdf_path = generate_pdf_from_markdown(state_md_path, pdf_output_path)
|
538 |
if not generated_pdf_path:
|
539 |
raise gr.Error("PDF generation failed (check logs).")
|
540 |
except gr.Error as e:
|
541 |
+
status_update = f"PDF Generation Error: {e}"
|
542 |
+
yield {status_textbox: gr.update(value=status_update)}
|
543 |
+
yield (
|
544 |
+
gr.update(value=status_update),
|
545 |
+
None,
|
546 |
+
[],
|
547 |
+
gr.update(),
|
548 |
+
gr.update(visible=True),
|
549 |
+
gr.update(),
|
550 |
+
)
|
551 |
+
raise e
|
552 |
except Exception as e:
|
553 |
+
status_update = f"Unexpected PDF Error: {e}"
|
554 |
+
yield {status_textbox: gr.update(value=status_update)}
|
555 |
+
yield (
|
556 |
+
gr.update(value=status_update),
|
557 |
+
None,
|
558 |
+
[],
|
559 |
+
gr.update(),
|
560 |
+
gr.update(visible=True),
|
561 |
+
gr.update(),
|
562 |
+
)
|
563 |
+
raise gr.Error(f"Unexpected error generating PDF: {e}")
|
564 |
|
565 |
progress(0.7, desc="Converting PDF to images...")
|
566 |
+
status_update = "Converting PDF to images..."
|
567 |
+
yield {status_textbox: gr.update(value=status_update)}
|
568 |
pdf_images = []
|
569 |
try:
|
570 |
pdf_images = convert_pdf_to_images(
|
|
|
576 |
if len(pdf_images) != num_slides:
|
577 |
warning_msg = f"Warning: PDF page count ({len(pdf_images)}) != slide count ({num_slides}). Images might mismatch."
|
578 |
gr.Warning(warning_msg)
|
579 |
+
status_update += f" ({warning_msg})"
|
580 |
# Pad or truncate? For now, just return what we have, UI update logic handles MAX_SLIDES
|
581 |
except Exception as e:
|
582 |
logger.error(f"Error converting PDF to images: {e}", exc_info=True)
|
583 |
+
status_update = f"Failed to convert PDF to images: {e}"
|
584 |
+
yield {status_textbox: gr.update(value=status_update)}
|
585 |
+
yield (
|
586 |
+
gr.update(value=status_update),
|
587 |
+
generated_pdf_path,
|
588 |
+
[],
|
589 |
+
gr.update(),
|
590 |
+
gr.update(visible=True),
|
591 |
+
gr.update(value=generated_pdf_path, visible=True),
|
592 |
+
)
|
593 |
+
# Proceed without images? Or raise error? Let's raise.
|
594 |
raise gr.Error(f"Failed to convert PDF to images: {e}")
|
595 |
|
596 |
info_msg = f"Built {len(pdf_images)} slide images. Ready for Step 3."
|
|
|
598 |
progress(1.0, desc="Slide build complete.")
|
599 |
status_update = f"Step 2 Complete: {info_msg}"
|
600 |
yield (
|
601 |
+
gr.update(value=status_update),
|
602 |
generated_pdf_path,
|
603 |
pdf_images, # Return the list of image paths
|
604 |
gr.update(visible=True), # btn_generate_audio
|
|
|
609 |
|
610 |
def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
611 |
"""Generates audio files for the speaker notes using edited content."""
|
612 |
+
# Args structure adjustment:
|
613 |
# args[0]: state_temp_dir
|
614 |
# args[1]: state_md_path
|
615 |
+
# args[2]: original_slides_data
|
616 |
+
# args[3]: status_textbox <- Now passed explicitly
|
617 |
+
# args[4...]: editors
|
|
|
618 |
|
619 |
state_temp_dir = args[0]
|
620 |
state_md_path = args[1]
|
621 |
original_slides_data = args[2]
|
622 |
+
status_textbox = args[3]
|
623 |
+
|
624 |
+
# editors = args[3:] # Old slicing
|
625 |
num_slides = len(original_slides_data)
|
626 |
if num_slides == 0:
|
627 |
logger.error("Step 3 (Audio) called with zero slides data.")
|
|
|
629 |
|
630 |
MAX_SLIDES = 20 # Ensure this matches UI definition
|
631 |
# --- Adjust indices based on adding status_textbox input ---
|
632 |
+
# Start editors from index 4 now
|
633 |
+
code_editors_start_index = 4
|
634 |
+
notes_textboxes_start_index = 4 + MAX_SLIDES
|
635 |
|
636 |
# Slice the *actual* edited values based on num_slides
|
637 |
edited_contents = args[
|
|
|
654 |
)
|
655 |
|
656 |
logger.info(f"Processing {num_slides} slides for audio generation.")
|
657 |
+
status_update = "Starting Step 3: Generating audio..."
|
658 |
+
yield {status_textbox: gr.update(value=status_update)}
|
659 |
+
|
660 |
audio_dir = os.path.join(state_temp_dir, "audio")
|
661 |
os.makedirs(audio_dir, exist_ok=True)
|
662 |
|
|
|
664 |
# This might be redundant if users don't edit notes between PDF and Audio steps,
|
665 |
# but ensures the audio matches the *latest* notes displayed.
|
666 |
progress(0.1, desc="Saving latest notes...")
|
667 |
+
status_update = "Saving latest notes..."
|
668 |
+
yield {status_textbox: gr.update(value=status_update)}
|
669 |
updated_slides_data = []
|
670 |
for i in range(num_slides):
|
671 |
updated_slides_data.append(
|
|
|
682 |
f.write(updated_markdown)
|
683 |
logger.info(f"Updated presentation markdown before audio gen: {state_md_path}")
|
684 |
except IOError as e:
|
|
|
685 |
warning_msg = f"Warning: Could not save latest notes to markdown file: {e}"
|
686 |
gr.Warning(warning_msg)
|
687 |
+
status_update += f" ({warning_msg})"
|
688 |
+
yield {status_textbox: gr.update(value=status_update)}
|
689 |
status_update = f"Warning: {warning_msg}"
|
690 |
+
yield (
|
691 |
+
gr.update(value=status_update),
|
692 |
+
None,
|
693 |
+
[],
|
694 |
+
gr.update(),
|
695 |
+
gr.update(visible=True),
|
696 |
+
gr.update(),
|
697 |
+
)
|
698 |
+
raise gr.Error(f"Failed to save updated markdown before audio gen: {e}")
|
699 |
|
700 |
generated_audio_paths = ["" for _ in range(num_slides)]
|
701 |
audio_generation_failed = False
|
|
|
709 |
progress_val,
|
710 |
desc=f"Audio slide {slide_num}/{num_slides}",
|
711 |
)
|
712 |
+
status_update = f"Generating audio for slide {slide_num}/{num_slides}..."
|
713 |
+
yield {status_textbox: gr.update(value=status_update)}
|
714 |
+
|
715 |
output_file_path = Path(audio_dir) / f"{slide_num}.wav"
|
716 |
if not note_text or not note_text.strip():
|
717 |
try: # Generate silence
|
|
|
775 |
|
776 |
# Return tuple including status update + original outputs
|
777 |
yield (
|
778 |
+
gr.update(value=status_update),
|
779 |
audio_dir,
|
780 |
gr.update(visible=True), # btn_generate_video
|
781 |
gr.update(visible=False), # btn_generate_audio
|
|
|
788 |
state_temp_dir,
|
789 |
state_audio_dir,
|
790 |
state_pdf_path, # Use PDF path from state
|
791 |
+
status_textbox,
|
792 |
progress=gr.Progress(track_tqdm=True),
|
793 |
):
|
794 |
"""Generates the final video using PDF images and audio files."""
|
|
|
802 |
)
|
803 |
|
804 |
video_output_path = os.path.join(state_temp_dir, "final_presentation.mp4")
|
805 |
+
status_update = "Starting Step 4: Generating video..."
|
806 |
+
yield {status_textbox: gr.update(value=status_update)}
|
807 |
+
|
808 |
progress(0.1, desc="Preparing video components...")
|
809 |
pdf_images = [] # Initialize to ensure cleanup happens
|
810 |
try:
|
|
|
813 |
if not audio_files:
|
814 |
warning_msg = f"Warning: No WAV files found in {state_audio_dir}. Video might lack audio."
|
815 |
logger.warning(warning_msg)
|
816 |
+
status_update += f" ({warning_msg})"
|
|
|
817 |
# Decide whether to proceed with silent video or error out
|
818 |
# raise gr.Error(f"No audio files found in {state_audio_dir}")
|
819 |
|
820 |
# Convert PDF to images
|
821 |
progress(0.2, desc="Converting PDF to images...")
|
822 |
+
status_update = "Converting PDF back to images for video..."
|
823 |
+
yield {status_textbox: gr.update(value=status_update)}
|
824 |
pdf_images = convert_pdf_to_images(state_pdf_path, dpi=150)
|
825 |
if not pdf_images:
|
826 |
raise gr.Error(f"Failed to convert PDF ({state_pdf_path}) to images.")
|
|
|
830 |
if len(pdf_images) != len(audio_files):
|
831 |
warning_msg = f"Warning: Mismatch: {len(pdf_images)} PDF pages vs {len(audio_files)} audio files. Video clips might have incorrect durations or missing audio."
|
832 |
logger.warning(warning_msg)
|
833 |
+
status_update += f" ({warning_msg})"
|
834 |
+
# yield status_update # Old yield
|
835 |
+
yield {status_textbox: gr.update(value=status_update)}
|
836 |
|
837 |
progress(0.5, desc="Creating individual video clips...")
|
838 |
+
status_update = "Creating individual video clips..."
|
839 |
+
# yield status_update # Old yield
|
840 |
+
yield {status_textbox: gr.update(value=status_update)}
|
841 |
buffer_seconds = 1.0
|
842 |
output_fps = 10
|
843 |
video_clips = create_video_clips(
|
|
|
848 |
raise gr.Error("Failed to create any video clips.")
|
849 |
|
850 |
progress(0.8, desc="Concatenating clips...")
|
851 |
+
status_update = "Concatenating clips into final video..."
|
852 |
+
# yield status_update # Old yield
|
853 |
+
yield {status_textbox: gr.update(value=status_update)}
|
854 |
concatenate_clips(video_clips, video_output_path, output_fps)
|
855 |
|
856 |
logger.info(f"Video concatenation complete: {video_output_path}")
|
857 |
|
858 |
progress(0.95, desc="Cleaning up temp images...")
|
859 |
+
status_update = "Cleaning up temporary image files..."
|
860 |
+
# yield status_update # Old yield
|
861 |
+
yield {status_textbox: gr.update(value=status_update)}
|
862 |
cleanup_temp_files(pdf_images) # Pass the list of image paths
|
863 |
|
864 |
except Exception as e:
|
|
|
866 |
cleanup_temp_files(pdf_images)
|
867 |
logger.error(f"Video generation failed: {e}", exc_info=True)
|
868 |
status_update = f"Video generation failed: {e}"
|
869 |
+
yield {status_textbox: gr.update(value=status_update)}
|
870 |
+
yield (gr.update(value=status_update), gr.update(), gr.update(visible=True))
|
|
|
|
|
|
|
871 |
raise gr.Error(f"Video generation failed: {e}")
|
872 |
|
873 |
info_msg = f"Video generated: {os.path.basename(video_output_path)}"
|
|
|
877 |
|
878 |
# Return tuple including status update
|
879 |
yield (
|
880 |
+
gr.update(value=status_update),
|
881 |
gr.update(value=video_output_path, visible=True), # video_output
|
882 |
gr.update(visible=False), # btn_generate_video
|
883 |
)
|
|
|
1127 |
]
|
1128 |
btn_fetch_generate.click(
|
1129 |
fn=step1_fetch_and_generate_presentation,
|
1130 |
+
inputs=[input_url, status_textbox],
|
1131 |
outputs=step1_outputs,
|
1132 |
show_progress="full",
|
1133 |
).then(
|
|
|
1166 |
|
1167 |
# Step 2 Click Handler
|
1168 |
step2_inputs = (
|
1169 |
+
[state_temp_dir, state_md_path, state_slides_data, status_textbox]
|
1170 |
+ all_code_editors
|
1171 |
+ all_notes_textboxes
|
1172 |
)
|
|
|
1202 |
).then(lambda: gr.update(selected=2), outputs=tabs_widget) # Switch to Tab 3
|
1203 |
|
1204 |
# Step 3 Click Handler
|
|
|
1205 |
step3_inputs = (
|
1206 |
+
[state_temp_dir, state_md_path, state_slides_data, status_textbox]
|
|
|
|
|
|
|
|
|
|
|
1207 |
+ all_code_editors
|
1208 |
+ all_notes_textboxes
|
1209 |
)
|
|
|
1210 |
step3_outputs = (
|
1211 |
[
|
1212 |
status_textbox, # Added status output
|
|
|
1232 |
)
|
1233 |
|
1234 |
# Step 4 Click Handler
|
|
|
1235 |
step4_inputs = [state_temp_dir, state_audio_dir, state_pdf_path, status_textbox]
|
1236 |
step4_outputs = [
|
1237 |
status_textbox, # Added status output
|