Spaces:
Running
Running
burtenshaw
commited on
Commit
·
44176d4
1
Parent(s):
5118298
add status box
Browse files- app/app.py +177 -170
app/app.py
CHANGED
@@ -283,7 +283,9 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
283 |
if not url:
|
284 |
raise gr.Error("Please enter a URL.")
|
285 |
logger.info(f"Step 1: Fetching & Generating for {url}")
|
286 |
-
|
|
|
|
|
287 |
|
288 |
# --- Cache Check ---
|
289 |
try:
|
@@ -333,20 +335,26 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
333 |
)
|
334 |
|
335 |
progress(0.9, desc="Preparing editor from cache...")
|
|
|
|
|
|
|
|
|
336 |
logger.info(f"Using cached data for {len(slides_data)} slides.")
|
337 |
# Return updates for the UI state and controls
|
338 |
-
|
|
|
339 |
temp_dir,
|
340 |
md_path,
|
341 |
slides_data,
|
342 |
gr.update(visible=True), # editor_column
|
343 |
gr.update(
|
344 |
visible=True
|
345 |
-
), #
|
346 |
gr.update(
|
347 |
interactive=False
|
348 |
), # btn_fetch_generate (disable)
|
349 |
)
|
|
|
350 |
except Exception as e:
|
351 |
logger.error(f"Error writing cached markdown: {e}")
|
352 |
if os.path.exists(temp_dir):
|
@@ -359,11 +367,15 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
359 |
if not hf_client:
|
360 |
raise gr.Error("LLM Client not initialized. Check API Key.")
|
361 |
|
|
|
|
|
362 |
web_content = fetch_webpage_content(url)
|
363 |
if not web_content:
|
364 |
raise gr.Error("Failed to fetch or parse content from the URL.")
|
365 |
|
366 |
progress(0.3, desc="Generating presentation with LLM...")
|
|
|
|
|
367 |
try:
|
368 |
presentation_md = generate_presentation_with_llm(
|
369 |
hf_client, LLM_MODEL, PRESENTATION_PROMPT, web_content, url
|
@@ -387,6 +399,8 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
387 |
)
|
388 |
|
389 |
progress(0.7, desc="Parsing presentation slides...")
|
|
|
|
|
390 |
slides_data = parse_presentation_markdown(presentation_md)
|
391 |
if not slides_data:
|
392 |
logger.error("Parsing markdown resulted in zero slides.")
|
@@ -429,21 +443,37 @@ def step1_fetch_and_generate_presentation(url, progress=gr.Progress(track_tqdm=T
|
|
429 |
logger.error(f"Failed to write to cache for URL {url}: {e}")
|
430 |
|
431 |
progress(0.9, desc="Preparing editor...")
|
|
|
|
|
432 |
logger.info(f"Prepared data for {len(slides_data)} slides.")
|
433 |
|
434 |
# Return updates for the UI state and controls
|
435 |
-
|
|
|
436 |
temp_dir,
|
437 |
md_path,
|
438 |
slides_data,
|
439 |
gr.update(visible=True), # editor_column
|
440 |
-
gr.update(visible=True), #
|
441 |
gr.update(interactive=False), # btn_fetch_generate (disable)
|
442 |
)
|
443 |
|
444 |
except Exception as e:
|
445 |
logger.error(f"Error in step 1 (fetch/generate): {e}", exc_info=True)
|
446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
447 |
|
448 |
|
449 |
def step2_build_slides(
|
@@ -457,7 +487,7 @@ def step2_build_slides(
|
|
457 |
if not all([state_temp_dir, state_md_path, state_slides_data]):
|
458 |
raise gr.Error("Session state missing.")
|
459 |
logger.info("Step 2: Building Slides (PDF + Images)")
|
460 |
-
|
461 |
num_slides = len(state_slides_data)
|
462 |
MAX_SLIDES = 20
|
463 |
all_editors = list(editors)
|
@@ -484,9 +514,14 @@ def step2_build_slides(
|
|
484 |
|
485 |
progress(0.3, desc="Generating PDF...")
|
486 |
pdf_output_path = os.path.join(state_temp_dir, "presentation.pdf")
|
487 |
-
|
488 |
-
|
489 |
-
|
|
|
|
|
|
|
|
|
|
|
490 |
|
491 |
progress(0.7, desc="Converting PDF to images...")
|
492 |
pdf_images = []
|
@@ -498,26 +533,24 @@ def step2_build_slides(
|
|
498 |
raise gr.Error("PDF to image conversion failed.")
|
499 |
logger.info(f"Converted PDF to {len(pdf_images)} images.")
|
500 |
if len(pdf_images) != num_slides:
|
501 |
-
|
502 |
-
|
503 |
-
)
|
504 |
# Pad or truncate? For now, just return what we have, UI update logic handles MAX_SLIDES
|
505 |
except Exception as e:
|
506 |
logger.error(f"Error converting PDF to images: {e}", exc_info=True)
|
507 |
-
# Proceed without images? Or raise error? Let's raise.
|
508 |
raise gr.Error(f"Failed to convert PDF to images: {e}")
|
509 |
|
510 |
info_msg = f"Built {len(pdf_images)} slide images. Ready for Step 3."
|
511 |
logger.info(info_msg)
|
512 |
-
gr.Info(info_msg)
|
513 |
progress(1.0, desc="Slide build complete.")
|
514 |
-
|
515 |
-
|
|
|
516 |
generated_pdf_path,
|
517 |
pdf_images, # Return the list of image paths
|
518 |
-
gr.update(visible=True),
|
519 |
-
gr.update(visible=False),
|
520 |
-
gr.update(value=generated_pdf_path, visible=True),
|
521 |
)
|
522 |
|
523 |
|
@@ -527,21 +560,24 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
527 |
# args[0]: state_temp_dir
|
528 |
# args[1]: state_md_path
|
529 |
# args[2]: original_slides_data (list of dicts, used to get count)
|
530 |
-
# args[3 :
|
531 |
-
# args[
|
|
|
532 |
|
533 |
state_temp_dir = args[0]
|
534 |
state_md_path = args[1]
|
535 |
original_slides_data = args[2]
|
536 |
-
editors = args[3:]
|
537 |
num_slides = len(original_slides_data)
|
538 |
if num_slides == 0:
|
539 |
logger.error("Step 3 (Audio) called with zero slides data.")
|
540 |
raise gr.Error("No slide data available. Please start over.")
|
541 |
|
542 |
MAX_SLIDES = 20 # Ensure this matches UI definition
|
543 |
-
|
544 |
-
|
|
|
|
|
545 |
|
546 |
# Slice the *actual* edited values based on num_slides
|
547 |
edited_contents = args[
|
@@ -588,8 +624,10 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
588 |
logger.info(f"Updated presentation markdown before audio gen: {state_md_path}")
|
589 |
except IOError as e:
|
590 |
logger.error(f"Failed to save updated markdown before audio gen: {e}")
|
591 |
-
|
592 |
-
gr.Warning(
|
|
|
|
|
593 |
|
594 |
generated_audio_paths = ["" for _ in range(num_slides)]
|
595 |
audio_generation_failed = False
|
@@ -598,8 +636,9 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
598 |
for i in range(num_slides):
|
599 |
note_text = edited_notes_list[i]
|
600 |
slide_num = i + 1
|
|
|
601 |
progress(
|
602 |
-
|
603 |
desc=f"Audio slide {slide_num}/{num_slides}",
|
604 |
)
|
605 |
output_file_path = Path(audio_dir) / f"{slide_num}.wav"
|
@@ -659,12 +698,13 @@ def step3_generate_audio(*args, progress=gr.Progress(track_tqdm=True)):
|
|
659 |
gr.Warning(info_msg)
|
660 |
else:
|
661 |
info_msg += "Ready for Step 4."
|
662 |
-
gr.Info(info_msg)
|
663 |
logger.info(info_msg)
|
664 |
progress(1.0, desc="Audio generation complete.")
|
|
|
665 |
|
666 |
-
# Return tuple
|
667 |
-
|
|
|
668 |
audio_dir,
|
669 |
gr.update(visible=True), # btn_generate_video
|
670 |
gr.update(visible=False), # btn_generate_audio
|
@@ -690,16 +730,16 @@ def step4_generate_video(
|
|
690 |
)
|
691 |
|
692 |
video_output_path = os.path.join(state_temp_dir, "final_presentation.mp4")
|
693 |
-
|
694 |
progress(0.1, desc="Preparing video components...")
|
695 |
pdf_images = [] # Initialize to ensure cleanup happens
|
696 |
try:
|
697 |
# Find audio files (natsorted)
|
698 |
audio_files = find_audio_files(state_audio_dir, "*.wav")
|
699 |
if not audio_files:
|
700 |
-
|
701 |
-
|
702 |
-
|
|
|
703 |
# Decide whether to proceed with silent video or error out
|
704 |
# raise gr.Error(f"No audio files found in {state_audio_dir}")
|
705 |
|
@@ -712,10 +752,10 @@ def step4_generate_video(
|
|
712 |
# Allow video generation even if audio is missing or count mismatch
|
713 |
# The create_video_clips function should handle missing audio gracefully (e.g., use image duration)
|
714 |
if len(pdf_images) != len(audio_files):
|
715 |
-
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
|
720 |
progress(0.5, desc="Creating individual video clips...")
|
721 |
buffer_seconds = 1.0
|
@@ -739,14 +779,22 @@ def step4_generate_video(
|
|
739 |
if pdf_images:
|
740 |
cleanup_temp_files(pdf_images)
|
741 |
logger.error(f"Video generation failed: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
742 |
raise gr.Error(f"Video generation failed: {e}")
|
743 |
|
744 |
info_msg = f"Video generated: {os.path.basename(video_output_path)}"
|
745 |
logger.info(info_msg)
|
746 |
-
gr.Info(info_msg)
|
747 |
progress(1.0, desc="Video Complete.")
|
748 |
-
|
749 |
-
|
|
|
|
|
|
|
750 |
gr.update(value=video_output_path, visible=True), # video_output
|
751 |
gr.update(visible=False), # btn_generate_video
|
752 |
)
|
@@ -857,7 +905,7 @@ with gr.Blocks(
|
|
857 |
)
|
858 |
with gr.Column(scale=4):
|
859 |
gr.Markdown(
|
860 |
-
"### Instructions
|
861 |
)
|
862 |
|
863 |
# Tab 2: Build Slides
|
@@ -873,7 +921,7 @@ with gr.Blocks(
|
|
873 |
)
|
874 |
with gr.Column(scale=4):
|
875 |
gr.Markdown(
|
876 |
-
"### Instructions
|
877 |
)
|
878 |
|
879 |
# Tab 3: Generate Audio
|
@@ -886,7 +934,7 @@ with gr.Blocks(
|
|
886 |
)
|
887 |
with gr.Column(scale=4):
|
888 |
gr.Markdown(
|
889 |
-
"### Instructions
|
890 |
)
|
891 |
|
892 |
# Tab 4: Generate Video
|
@@ -899,14 +947,14 @@ with gr.Blocks(
|
|
899 |
)
|
900 |
with gr.Column(scale=4):
|
901 |
gr.Markdown(
|
902 |
-
"### Instructions
|
903 |
)
|
904 |
video_output = gr.Video(label="Final Video", visible=False)
|
905 |
|
906 |
# Define the shared editor structure once, AFTER tabs
|
907 |
slide_editors_group = []
|
908 |
with gr.Column(visible=False) as editor_column: # Initially hidden
|
909 |
-
gr.Markdown("---
|
910 |
gr.Markdown("_(PDF uses content & notes, Audio uses notes only)_")
|
911 |
for i in range(MAX_SLIDES):
|
912 |
with gr.Accordion(f"Slide {i + 1}", open=(i == 0), visible=False) as acc:
|
@@ -919,19 +967,21 @@ with gr.Blocks(
|
|
919 |
interactive=True,
|
920 |
visible=False,
|
921 |
)
|
922 |
-
notes_textbox =
|
923 |
-
|
924 |
-
|
925 |
-
|
926 |
-
|
927 |
-
|
|
|
|
|
928 |
)
|
929 |
with gr.Column(scale=1):
|
930 |
slide_image = gr.Image(
|
931 |
label="Slide Image",
|
932 |
visible=False,
|
933 |
interactive=False,
|
934 |
-
height=300,
|
935 |
)
|
936 |
md_preview = gr.Markdown(visible=False)
|
937 |
with gr.Row(): # Row for audio controls
|
@@ -962,6 +1012,16 @@ with gr.Blocks(
|
|
962 |
show_progress="hidden",
|
963 |
)
|
964 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
965 |
# --- Component Lists for Updates ---
|
966 |
all_editor_components = [comp for group in slide_editors_group for comp in group]
|
967 |
all_code_editors = [group[1] for group in slide_editors_group]
|
@@ -970,92 +1030,11 @@ with gr.Blocks(
|
|
970 |
all_regen_buttons = [group[5] for group in slide_editors_group]
|
971 |
all_slide_images = [group[6] for group in slide_editors_group]
|
972 |
|
973 |
-
# --- Function to regenerate audio --- (Assumed correct)
|
974 |
-
# ... (regenerate_single_audio implementation as fixed before)...
|
975 |
-
def regenerate_single_audio(
|
976 |
-
slide_idx, note_text, temp_dir, progress=gr.Progress(track_tqdm=True)
|
977 |
-
):
|
978 |
-
# ...(Implementation as fixed before)...
|
979 |
-
if (
|
980 |
-
not temp_dir
|
981 |
-
or not isinstance(temp_dir, str)
|
982 |
-
or not os.path.exists(temp_dir)
|
983 |
-
):
|
984 |
-
logger.error(f"Regen audio failed: Invalid temp_dir '{temp_dir}'")
|
985 |
-
return gr.update(value=None, visible=False)
|
986 |
-
slide_num = slide_idx + 1
|
987 |
-
audio_dir = os.path.join(temp_dir, "audio")
|
988 |
-
os.makedirs(audio_dir, exist_ok=True)
|
989 |
-
output_file = Path(audio_dir) / f"{slide_num}.wav"
|
990 |
-
logger.info(f"Regenerating audio for slide {slide_num} -> {output_file}")
|
991 |
-
progress(0.1, desc=f"Regen audio slide {slide_num}...")
|
992 |
-
if not note_text or not note_text.strip():
|
993 |
-
logger.warning(f"Note for slide {slide_num} empty. Generating silence.")
|
994 |
-
try:
|
995 |
-
subprocess.run(
|
996 |
-
[
|
997 |
-
"ffmpeg",
|
998 |
-
"-y",
|
999 |
-
"-f",
|
1000 |
-
"lavfi",
|
1001 |
-
"-i",
|
1002 |
-
"anullsrc=r=44100:cl=mono",
|
1003 |
-
"-t",
|
1004 |
-
"0.1",
|
1005 |
-
"-q:a",
|
1006 |
-
"9",
|
1007 |
-
str(output_file),
|
1008 |
-
],
|
1009 |
-
check=True,
|
1010 |
-
capture_output=True,
|
1011 |
-
text=True,
|
1012 |
-
)
|
1013 |
-
logger.info(f"Created silent placeholder: {output_file}")
|
1014 |
-
progress(1.0, desc=f"Generated silence slide {slide_num}.")
|
1015 |
-
return gr.update(value=str(output_file), visible=True)
|
1016 |
-
except Exception as e:
|
1017 |
-
logger.error(f"Failed silent gen slide {slide_num}: {e}")
|
1018 |
-
return gr.update(value=None, visible=False)
|
1019 |
-
else:
|
1020 |
-
try:
|
1021 |
-
success = text_to_speech(
|
1022 |
-
note_text, output_file, voice=VOICE_ID, cache_dir=CACHE_DIR
|
1023 |
-
)
|
1024 |
-
if success:
|
1025 |
-
logger.info(f"Regen OK slide {slide_num}")
|
1026 |
-
progress(1.0, desc=f"Audio regen OK slide {slide_num}.")
|
1027 |
-
return gr.update(value=str(output_file), visible=True)
|
1028 |
-
else:
|
1029 |
-
logger.error(f"Regen TTS failed slide {slide_num}")
|
1030 |
-
return gr.update(value=None, visible=False)
|
1031 |
-
except Exception as e:
|
1032 |
-
logger.error(
|
1033 |
-
f"Regen TTS exception slide {slide_num}: {e}", exc_info=True
|
1034 |
-
)
|
1035 |
-
return gr.update(value=None, visible=False)
|
1036 |
-
|
1037 |
-
# --- Connect the individual Re-generate buttons ---
|
1038 |
-
# Update unpacking to include slide_image (7 items)
|
1039 |
-
for i, (
|
1040 |
-
acc,
|
1041 |
-
code_edit,
|
1042 |
-
md_preview,
|
1043 |
-
notes_tb,
|
1044 |
-
audio_pl,
|
1045 |
-
regen_btn,
|
1046 |
-
slide_image,
|
1047 |
-
) in enumerate(slide_editors_group):
|
1048 |
-
regen_btn.click(
|
1049 |
-
fn=regenerate_single_audio,
|
1050 |
-
inputs=[gr.State(i), notes_tb, state_temp_dir],
|
1051 |
-
outputs=[audio_pl],
|
1052 |
-
show_progress="minimal",
|
1053 |
-
)
|
1054 |
-
|
1055 |
# --- Main Button Click Handlers --- (Outputs use locally defined component vars)
|
1056 |
|
1057 |
# Step 1 Click Handler
|
1058 |
step1_outputs = [
|
|
|
1059 |
state_temp_dir,
|
1060 |
state_md_path,
|
1061 |
state_slides_data,
|
@@ -1069,31 +1048,36 @@ with gr.Blocks(
|
|
1069 |
outputs=step1_outputs,
|
1070 |
show_progress="full",
|
1071 |
).then(
|
1072 |
-
fn=lambda s_data:
|
1073 |
-
|
1074 |
-
|
1075 |
-
|
1076 |
-
|
1077 |
-
|
1078 |
-
|
1079 |
-
|
1080 |
-
|
1081 |
-
|
1082 |
-
|
1083 |
-
|
1084 |
-
|
1085 |
-
|
1086 |
-
|
1087 |
-
|
|
|
|
|
|
|
|
|
|
|
1088 |
]
|
1089 |
-
|
1090 |
-
|
1091 |
-
|
1092 |
-
|
1093 |
-
|
1094 |
-
|
1095 |
inputs=[state_slides_data],
|
1096 |
-
outputs=all_editor_components,
|
1097 |
show_progress="hidden",
|
1098 |
).then(lambda: gr.update(selected=1), outputs=tabs_widget) # Switch to Tab 2
|
1099 |
|
@@ -1104,6 +1088,7 @@ with gr.Blocks(
|
|
1104 |
+ all_notes_textboxes
|
1105 |
)
|
1106 |
step2_outputs = [
|
|
|
1107 |
state_pdf_path,
|
1108 |
state_pdf_image_paths,
|
1109 |
btn_generate_audio, # Enable button in Tab 3
|
@@ -1116,26 +1101,39 @@ with gr.Blocks(
|
|
1116 |
outputs=step2_outputs,
|
1117 |
show_progress="full",
|
1118 |
).then(
|
1119 |
-
fn=lambda image_paths:
|
1120 |
-
gr.update(
|
1121 |
-
|
1122 |
-
|
1123 |
-
|
1124 |
-
|
1125 |
-
|
|
|
|
|
|
|
|
|
|
|
1126 |
inputs=[state_pdf_image_paths],
|
1127 |
-
outputs=all_slide_images,
|
1128 |
show_progress="hidden",
|
1129 |
).then(lambda: gr.update(selected=2), outputs=tabs_widget) # Switch to Tab 3
|
1130 |
|
1131 |
# Step 3 Click Handler
|
|
|
1132 |
step3_inputs = (
|
1133 |
-
[
|
|
|
|
|
|
|
|
|
|
|
1134 |
+ all_code_editors
|
1135 |
+ all_notes_textboxes
|
1136 |
)
|
|
|
1137 |
step3_outputs = (
|
1138 |
[
|
|
|
1139 |
state_audio_dir,
|
1140 |
btn_generate_video, # Enable button in Tab 4
|
1141 |
btn_generate_audio, # Disable self
|
@@ -1148,11 +1146,20 @@ with gr.Blocks(
|
|
1148 |
inputs=step3_inputs,
|
1149 |
outputs=step3_outputs,
|
1150 |
show_progress="full",
|
1151 |
-
).then(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1152 |
|
1153 |
# Step 4 Click Handler
|
1154 |
-
|
|
|
1155 |
step4_outputs = [
|
|
|
1156 |
video_output, # Update video output in Tab 4
|
1157 |
btn_generate_video, # Disable self
|
1158 |
]
|
|
|
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:
|
|
|
335 |
)
|
336 |
|
337 |
progress(0.9, desc="Preparing editor from cache...")
|
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, # Keep status
|
346 |
temp_dir,
|
347 |
md_path,
|
348 |
slides_data,
|
349 |
gr.update(visible=True), # editor_column
|
350 |
gr.update(
|
351 |
visible=True
|
352 |
+
), # btn_build_slides (Enable PDF button next)
|
353 |
gr.update(
|
354 |
interactive=False
|
355 |
), # btn_fetch_generate (disable)
|
356 |
)
|
357 |
+
return # End generator here for cache hit
|
358 |
except Exception as e:
|
359 |
logger.error(f"Error writing cached markdown: {e}")
|
360 |
if os.path.exists(temp_dir):
|
|
|
367 |
if not hf_client:
|
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
|
|
|
399 |
)
|
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.")
|
|
|
443 |
logger.error(f"Failed to write to cache for URL {url}: {e}")
|
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, # Keep status
|
453 |
temp_dir,
|
454 |
md_path,
|
455 |
slides_data,
|
456 |
gr.update(visible=True), # editor_column
|
457 |
+
gr.update(visible=True), # btn_build_slides (Enable PDF button next)
|
458 |
gr.update(interactive=False), # btn_fetch_generate (disable)
|
459 |
)
|
460 |
|
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 # Yield error status
|
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 |
+
[],
|
471 |
+
gr.update(),
|
472 |
+
gr.update(),
|
473 |
+
gr.update(interactive=True),
|
474 |
+
)
|
475 |
+
# Optionally re-raise or handle differently
|
476 |
+
# raise gr.Error(f"Error during presentation setup: {e}")
|
477 |
|
478 |
|
479 |
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
|
493 |
all_editors = list(editors)
|
|
|
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 |
+
raise e # Re-raise
|
523 |
+
except Exception as e:
|
524 |
+
raise gr.Error(f"Unexpected PDF Error: {e}")
|
525 |
|
526 |
progress(0.7, desc="Converting PDF to images...")
|
527 |
pdf_images = []
|
|
|
533 |
raise gr.Error("PDF to image conversion failed.")
|
534 |
logger.info(f"Converted PDF to {len(pdf_images)} images.")
|
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."
|
544 |
logger.info(info_msg)
|
|
|
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
|
552 |
+
gr.update(visible=False), # btn_build_slides
|
553 |
+
gr.update(value=generated_pdf_path, visible=True), # pdf_download_link
|
554 |
)
|
555 |
|
556 |
|
|
|
560 |
# args[0]: state_temp_dir
|
561 |
# args[1]: state_md_path
|
562 |
# args[2]: original_slides_data (list of dicts, used to get count)
|
563 |
+
# args[3] : status_textbox (This index needs adjustment if adding more inputs)
|
564 |
+
# args[4 : 4 + MAX_SLIDES]: values from all_code_editors
|
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 |
+
# editors = args[3:] # noqa F841 - Keeping for potential future use or clarity of arg structure # Old slicing
|
571 |
num_slides = len(original_slides_data)
|
572 |
if num_slides == 0:
|
573 |
logger.error("Step 3 (Audio) called with zero slides data.")
|
574 |
raise gr.Error("No slide data available. Please start over.")
|
575 |
|
576 |
MAX_SLIDES = 20 # Ensure this matches UI definition
|
577 |
+
# --- Adjust indices based on adding status_textbox input ---
|
578 |
+
# Assuming status_textbox is now at index 3
|
579 |
+
code_editors_start_index = 4 # Was 3
|
580 |
+
notes_textboxes_start_index = 4 + MAX_SLIDES # Was 3 + MAX_SLIDES
|
581 |
|
582 |
# Slice the *actual* edited values based on num_slides
|
583 |
edited_contents = args[
|
|
|
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 status_update # Yield status with warning
|
631 |
|
632 |
generated_audio_paths = ["" for _ in range(num_slides)]
|
633 |
audio_generation_failed = False
|
|
|
636 |
for i in range(num_slides):
|
637 |
note_text = edited_notes_list[i]
|
638 |
slide_num = i + 1
|
639 |
+
progress_val = (i + 1) / num_slides * 0.8 + 0.1
|
640 |
progress(
|
641 |
+
progress_val,
|
642 |
desc=f"Audio slide {slide_num}/{num_slides}",
|
643 |
)
|
644 |
output_file_path = Path(audio_dir) / f"{slide_num}.wav"
|
|
|
698 |
gr.Warning(info_msg)
|
699 |
else:
|
700 |
info_msg += "Ready for Step 4."
|
|
|
701 |
logger.info(info_msg)
|
702 |
progress(1.0, desc="Audio generation complete.")
|
703 |
+
status_update = f"Step 3 Complete: {info_msg}"
|
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
|
|
|
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:
|
736 |
# Find audio files (natsorted)
|
737 |
audio_files = find_audio_files(state_audio_dir, "*.wav")
|
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 = f"Warning: {warning_msg}"
|
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 |
|
|
|
752 |
# Allow video generation even if audio is missing or count mismatch
|
753 |
# The create_video_clips function should handle missing audio gracefully (e.g., use image duration)
|
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 = f"Warning: {warning_msg}"
|
758 |
+
yield status_update
|
759 |
|
760 |
progress(0.5, desc="Creating individual video clips...")
|
761 |
buffer_seconds = 1.0
|
|
|
779 |
if pdf_images:
|
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 |
+
status_update,
|
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)}"
|
791 |
logger.info(info_msg)
|
|
|
792 |
progress(1.0, desc="Video Complete.")
|
793 |
+
status_update = f"Step 4 Complete: {info_msg}"
|
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 |
)
|
|
|
905 |
)
|
906 |
with gr.Column(scale=4):
|
907 |
gr.Markdown(
|
908 |
+
"### Instructions\\n1. Enter URL & click 'Fetch & Generate'.\\n2. Editor appears below.\\n3. Go to next tab."
|
909 |
)
|
910 |
|
911 |
# Tab 2: Build Slides
|
|
|
921 |
)
|
922 |
with gr.Column(scale=4):
|
923 |
gr.Markdown(
|
924 |
+
"### Instructions\\n1. Edit content/notes below.\\n2. Click 'Build Slides'. Images appear.\\n3. Download PDF from sidebar.\\n4. Go to next tab."
|
925 |
)
|
926 |
|
927 |
# Tab 3: Generate Audio
|
|
|
934 |
)
|
935 |
with gr.Column(scale=4):
|
936 |
gr.Markdown(
|
937 |
+
"### Instructions\\n1. Finalize notes below.\\n2. Click 'Generate Audio'.\\n3. Regenerate if needed.\\n4. Go to next tab."
|
938 |
)
|
939 |
|
940 |
# Tab 4: Generate Video
|
|
|
947 |
)
|
948 |
with gr.Column(scale=4):
|
949 |
gr.Markdown(
|
950 |
+
"### Instructions\\n1. Click 'Create Video'.\\n2. Video appears below."
|
951 |
)
|
952 |
video_output = gr.Video(label="Final Video", visible=False)
|
953 |
|
954 |
# Define the shared editor structure once, AFTER tabs
|
955 |
slide_editors_group = []
|
956 |
with gr.Column(visible=False) as editor_column: # Initially hidden
|
957 |
+
gr.Markdown("--- \\n## Edit Slides & Notes")
|
958 |
gr.Markdown("_(PDF uses content & notes, Audio uses notes only)_")
|
959 |
for i in range(MAX_SLIDES):
|
960 |
with gr.Accordion(f"Slide {i + 1}", open=(i == 0), visible=False) as acc:
|
|
|
967 |
interactive=True,
|
968 |
visible=False,
|
969 |
)
|
970 |
+
notes_textbox = (
|
971 |
+
gr.Textbox( # Changed from gr.Code to gr.Textbox
|
972 |
+
label="Script/Notes (for Audio)",
|
973 |
+
lines=8,
|
974 |
+
# language="markdown", # Removed language parameter
|
975 |
+
interactive=True,
|
976 |
+
visible=False,
|
977 |
+
)
|
978 |
)
|
979 |
with gr.Column(scale=1):
|
980 |
slide_image = gr.Image(
|
981 |
label="Slide Image",
|
982 |
visible=False,
|
983 |
interactive=False,
|
984 |
+
# height=300, # Removed fixed height
|
985 |
)
|
986 |
md_preview = gr.Markdown(visible=False)
|
987 |
with gr.Row(): # Row for audio controls
|
|
|
1012 |
show_progress="hidden",
|
1013 |
)
|
1014 |
|
1015 |
+
# --- Status Textbox (Added) ---
|
1016 |
+
with gr.Row():
|
1017 |
+
status_textbox = gr.Textbox(
|
1018 |
+
label="Status",
|
1019 |
+
value="Enter a URL and click 'Fetch & Generate' to start.",
|
1020 |
+
interactive=False,
|
1021 |
+
lines=1,
|
1022 |
+
max_lines=1,
|
1023 |
+
)
|
1024 |
+
|
1025 |
# --- Component Lists for Updates ---
|
1026 |
all_editor_components = [comp for group in slide_editors_group for comp in group]
|
1027 |
all_code_editors = [group[1] for group in slide_editors_group]
|
|
|
1030 |
all_regen_buttons = [group[5] for group in slide_editors_group]
|
1031 |
all_slide_images = [group[6] for group in slide_editors_group]
|
1032 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1033 |
# --- Main Button Click Handlers --- (Outputs use locally defined component vars)
|
1034 |
|
1035 |
# Step 1 Click Handler
|
1036 |
step1_outputs = [
|
1037 |
+
status_textbox, # Added status output
|
1038 |
state_temp_dir,
|
1039 |
state_md_path,
|
1040 |
state_slides_data,
|
|
|
1048 |
outputs=step1_outputs,
|
1049 |
show_progress="full",
|
1050 |
).then(
|
1051 |
+
fn=lambda s_data: (
|
1052 |
+
gr.update(value="Editor populated. Proceed to Step 2.") # Update status
|
1053 |
+
)
|
1054 |
+
+ tuple(
|
1055 |
+
[
|
1056 |
+
upd
|
1057 |
+
for i, slide in enumerate(s_data)
|
1058 |
+
if i < MAX_SLIDES
|
1059 |
+
for upd in [
|
1060 |
+
gr.update(
|
1061 |
+
label=f"Slide {i + 1}: {slide['content'][:25]}...",
|
1062 |
+
visible=True,
|
1063 |
+
open=(i == 0),
|
1064 |
+
), # Accordion
|
1065 |
+
gr.update(value=slide["content"], visible=True), # Code Editor
|
1066 |
+
gr.update(value=slide["content"], visible=True), # MD Preview
|
1067 |
+
gr.update(value=slide["notes"], visible=True), # Notes Textbox
|
1068 |
+
gr.update(value=None, visible=False), # Audio Player
|
1069 |
+
gr.update(visible=False), # Regen Button
|
1070 |
+
gr.update(value=None, visible=False), # Slide Image
|
1071 |
+
]
|
1072 |
]
|
1073 |
+
+ [
|
1074 |
+
upd
|
1075 |
+
for i in range(len(s_data), MAX_SLIDES)
|
1076 |
+
for upd in [gr.update(visible=False)] * 7
|
1077 |
+
]
|
1078 |
+
), # Keep editor updates
|
1079 |
inputs=[state_slides_data],
|
1080 |
+
outputs=[status_textbox] + all_editor_components, # Add status_textbox output
|
1081 |
show_progress="hidden",
|
1082 |
).then(lambda: gr.update(selected=1), outputs=tabs_widget) # Switch to Tab 2
|
1083 |
|
|
|
1088 |
+ all_notes_textboxes
|
1089 |
)
|
1090 |
step2_outputs = [
|
1091 |
+
status_textbox, # Added status output
|
1092 |
state_pdf_path,
|
1093 |
state_pdf_image_paths,
|
1094 |
btn_generate_audio, # Enable button in Tab 3
|
|
|
1101 |
outputs=step2_outputs,
|
1102 |
show_progress="full",
|
1103 |
).then(
|
1104 |
+
fn=lambda image_paths: (
|
1105 |
+
gr.update(value="Slide images updated. Proceed to Step 3.") # Update status
|
1106 |
+
)
|
1107 |
+
+ tuple(
|
1108 |
+
[
|
1109 |
+
gr.update(
|
1110 |
+
value=image_paths[i] if i < len(image_paths) else None,
|
1111 |
+
visible=(i < len(image_paths)),
|
1112 |
+
)
|
1113 |
+
for i in range(MAX_SLIDES)
|
1114 |
+
]
|
1115 |
+
), # Keep image updates
|
1116 |
inputs=[state_pdf_image_paths],
|
1117 |
+
outputs=[status_textbox] + all_slide_images, # Add status_textbox output
|
1118 |
show_progress="hidden",
|
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
|
1137 |
state_audio_dir,
|
1138 |
btn_generate_video, # Enable button in Tab 4
|
1139 |
btn_generate_audio, # Disable self
|
|
|
1146 |
inputs=step3_inputs,
|
1147 |
outputs=step3_outputs,
|
1148 |
show_progress="full",
|
1149 |
+
).then(
|
1150 |
+
lambda: (
|
1151 |
+
gr.update(value="Audio generated. Proceed to Step 4."),
|
1152 |
+
gr.update(selected=3),
|
1153 |
+
), # Update status and switch tab
|
1154 |
+
outputs=[status_textbox, tabs_widget], # Add status_textbox output
|
1155 |
+
show_progress="hidden", # Hide progress for simple status update + tab switch
|
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
|
1163 |
video_output, # Update video output in Tab 4
|
1164 |
btn_generate_video, # Disable self
|
1165 |
]
|