burtenshaw commited on
Commit
44176d4
·
1 Parent(s): 5118298

add status box

Browse files
Files changed (1) hide show
  1. 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
- gr.Info(f"Starting Step 1: Fetching content from {url}...")
 
 
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
- return (
 
339
  temp_dir,
340
  md_path,
341
  slides_data,
342
  gr.update(visible=True), # editor_column
343
  gr.update(
344
  visible=True
345
- ), # btn_generate_pdf (Enable PDF button next)
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
- return (
 
436
  temp_dir,
437
  md_path,
438
  slides_data,
439
  gr.update(visible=True), # editor_column
440
- gr.update(visible=True), # btn_generate_pdf (Enable PDF button next)
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
- raise gr.Error(f"Error during presentation setup: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- gr.Info("Starting Step 2: Building slides...")
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
- generated_pdf_path = generate_pdf_from_markdown(state_md_path, pdf_output_path)
488
- if not generated_pdf_path:
489
- raise gr.Error("PDF generation failed (check logs).")
 
 
 
 
 
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
- gr.Warning(
502
- f"PDF page count ({len(pdf_images)}) != slide count ({num_slides}). Images might mismatch."
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
- # Return tuple WITHOUT status textbox update
515
- return (
 
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 : 3 + MAX_SLIDES]: values from all_code_editors
531
- # args[3 + MAX_SLIDES :]: values from all_notes_textboxes
 
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
- code_editors_start_index = 3
544
- notes_textboxes_start_index = 3 + MAX_SLIDES
 
 
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
- # Continue with audio gen, but log warning
592
- gr.Warning(f"Could not save latest notes to markdown file: {e}")
 
 
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
- (i + 1) / num_slides * 0.8 + 0.1,
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 WITHOUT status textbox update
667
- return (
 
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
- logger.warning(
701
- f"No WAV files found in {state_audio_dir}. Video might lack audio."
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
- logger.warning(
716
- f"Mismatch: {len(pdf_images)} PDF pages vs {len(audio_files)} audio files. Video clips might have incorrect durations or missing audio."
717
- )
718
- # Pad the shorter list? For now, let create_video_clips handle it.
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
- # Return tuple WITHOUT status textbox update
749
- return (
 
 
 
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\n1. Enter URL & click 'Fetch & Generate'.\n2. Editor appears below tabs.\n3. Go to next tab."
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\n1. Edit content/notes below.\n2. Click 'Build Slides'. Images appear.\n3. Download PDF from sidebar.\n4. Go to next tab."
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\n1. Finalize notes below.\n2. Click 'Generate Audio'.\n3. Regenerate if needed.\n4. Go to next tab."
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\n1. Click 'Create Video'.\n2. Video appears below."
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("--- \n## Edit Slides & Notes")
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 = gr.Code(
923
- label="Script/Notes (for Audio)",
924
- lines=8,
925
- language="markdown",
926
- interactive=True,
927
- visible=False,
 
 
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
- upd
1074
- for i, slide in enumerate(s_data)
1075
- if i < MAX_SLIDES
1076
- for upd in [
1077
- gr.update(
1078
- label=f"Slide {i + 1}: {slide['content'][:25]}...",
1079
- visible=True,
1080
- open=(i == 0),
1081
- ), # Accordion
1082
- gr.update(value=slide["content"], visible=True), # Code Editor
1083
- gr.update(value=slide["content"], visible=True), # MD Preview
1084
- gr.update(value=slide["notes"], visible=True), # Notes Textbox
1085
- gr.update(value=None, visible=False), # Audio Player
1086
- gr.update(visible=False), # Regen Button
1087
- gr.update(value=None, visible=False), # Slide Image
 
 
 
 
 
1088
  ]
1089
- ]
1090
- + [
1091
- upd
1092
- for i in range(len(s_data), MAX_SLIDES)
1093
- for upd in [gr.update(visible=False)] * 7
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
- value=image_paths[i] if i < len(image_paths) else None,
1122
- visible=(i < len(image_paths)),
1123
- )
1124
- for i in range(MAX_SLIDES)
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
- [state_temp_dir, state_md_path, state_slides_data]
 
 
 
 
 
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(lambda: gr.update(selected=3), outputs=tabs_widget) # Switch to Tab 4
 
 
 
 
 
 
 
1152
 
1153
  # Step 4 Click Handler
1154
- step4_inputs = [state_temp_dir, state_audio_dir, state_pdf_path]
 
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
  ]