rairo commited on
Commit
0096675
Β·
verified Β·
1 Parent(s): c07a5e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +284 -0
app.py CHANGED
@@ -596,6 +596,290 @@ def animate_chart_fallback(spec: ChartSpecification, df: pd.DataFrame, dur: floa
596
  except Exception:
597
  return str(out) # Return path even if failed
598
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  # ─── ENHANCED MAIN FUNCTIONS (DROP-IN REPLACEMENTS) ────────────────────────────
600
 
601
  def generate_report_bundle(buf: bytes, name: str, ctx: str, key: str):
 
596
  except Exception:
597
  return str(out) # Return path even if failed
598
 
599
+ def safe_chart(desc: str, df: pd.DataFrame, dur: float, out: Path) -> str:
600
+ """
601
+ Enhanced safe chart generation with animation for video pipeline.
602
+
603
+ This function integrates with the existing ChartGenerator system to create
604
+ animated charts that are suitable for video scenes. It provides multiple
605
+ fallback layers to ensure reliable chart generation.
606
+
607
+ Args:
608
+ desc (str): Chart description/specification
609
+ df (pd.DataFrame): Source data
610
+ dur (float): Duration in seconds for animation
611
+ out (Path): Output video file path
612
+
613
+ Returns:
614
+ str: Path to generated video file
615
+ """
616
+ try:
617
+ # Initialize the enhanced chart generator
618
+ llm = ChatGoogleGenerativeAI(
619
+ model="gemini-2.0-flash",
620
+ google_api_key=API_KEY,
621
+ temperature=0.1
622
+ )
623
+ chart_generator = create_chart_generator(llm, df)
624
+
625
+ # Generate AI-driven chart specification
626
+ with st.spinner(f"Analyzing chart requirements: {desc}..."):
627
+ chart_spec = chart_generator.generate_chart_spec(desc)
628
+
629
+ # Attempt enhanced animation with specification
630
+ try:
631
+ return animate_chart_with_spec(chart_spec, df, dur, out, fps=FPS)
632
+ except Exception as anim_error:
633
+ print(f"Enhanced animation failed: {anim_error}")
634
+
635
+ # Fallback 1: Static chart with fade animation
636
+ try:
637
+ temp_png = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
638
+ if execute_chart_spec(chart_spec, df, temp_png):
639
+ img = cv2.imread(str(temp_png))
640
+ if img is not None:
641
+ img = cv2.resize(img, (WIDTH, HEIGHT))
642
+ return animate_image_fade(img, dur, out, fps=FPS)
643
+ else:
644
+ raise RuntimeError("Failed to load generated chart image")
645
+ else:
646
+ raise RuntimeError("Chart specification execution failed")
647
+
648
+ except Exception as static_error:
649
+ print(f"Static chart generation failed: {static_error}")
650
+
651
+ # Fallback 2: Quick chart generation
652
+ try:
653
+ temp_png = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
654
+ quick_chart(desc, df, temp_png)
655
+
656
+ if temp_png.exists():
657
+ img = cv2.imread(str(temp_png))
658
+ if img is not None:
659
+ img = cv2.resize(img, (WIDTH, HEIGHT))
660
+ return animate_image_fade(img, dur, out, fps=FPS)
661
+ else:
662
+ raise RuntimeError("Failed to load quick chart image")
663
+ else:
664
+ raise RuntimeError("Quick chart generation failed")
665
+
666
+ except Exception as quick_error:
667
+ print(f"Quick chart generation failed: {quick_error}")
668
+
669
+ # Fallback 3: AI-generated image
670
+ try:
671
+ # Generate descriptive prompt for AI image generation
672
+ img_prompt = f"Professional business chart showing {desc}. Clean, modern design with clear data visualization."
673
+ img = generate_image_from_prompt(img_prompt)
674
+
675
+ # Convert PIL to OpenCV format
676
+ img_cv = cv2.cvtColor(
677
+ np.array(img.resize((WIDTH, HEIGHT))),
678
+ cv2.COLOR_RGB2BGR
679
+ )
680
+ return animate_image_fade(img_cv, dur, out, fps=FPS)
681
+
682
+ except Exception as ai_error:
683
+ print(f"AI image generation failed: {ai_error}")
684
+
685
+ # Fallback 4: Placeholder with text
686
+ return create_placeholder_chart_video(desc, dur, out)
687
+
688
+ except Exception as e:
689
+ print(f"Safe chart generation completely failed: {e}")
690
+ # Ultimate fallback
691
+ return create_placeholder_chart_video(desc, dur, out)
692
+
693
+
694
+ def create_placeholder_chart_video(desc: str, dur: float, out: Path) -> str:
695
+ """
696
+ Create a placeholder video with descriptive text when all chart generation fails.
697
+
698
+ Args:
699
+ desc (str): Chart description
700
+ dur (float): Duration in seconds
701
+ out (Path): Output path
702
+
703
+ Returns:
704
+ str: Path to generated placeholder video
705
+ """
706
+ try:
707
+ # Create a professional-looking placeholder
708
+ fig, ax = plt.subplots(figsize=(16, 9), dpi=100)
709
+ fig.patch.set_facecolor('#f8f9fa')
710
+ ax.set_facecolor('#ffffff')
711
+
712
+ # Add title and description
713
+ ax.text(0.5, 0.65, "Data Visualization",
714
+ ha='center', va='center', fontsize=24, fontweight='bold',
715
+ color='#2c3e50', transform=ax.transAxes)
716
+
717
+ ax.text(0.5, 0.45, desc,
718
+ ha='center', va='center', fontsize=16,
719
+ color='#34495e', transform=ax.transAxes,
720
+ wrap=True, bbox=dict(boxstyle="round,pad=0.3", facecolor='#ecf0f1', alpha=0.8))
721
+
722
+ ax.text(0.5, 0.25, "Chart generation in progress...",
723
+ ha='center', va='center', fontsize=12,
724
+ color='#7f8c8d', transform=ax.transAxes)
725
+
726
+ # Add some decorative elements
727
+ ax.add_patch(plt.Rectangle((0.1, 0.1), 0.8, 0.8,
728
+ fill=False, edgecolor='#3498db', linewidth=3,
729
+ transform=ax.transAxes))
730
+
731
+ ax.set_xlim(0, 1)
732
+ ax.set_ylim(0, 1)
733
+ ax.axis('off')
734
+
735
+ # Save as temporary image
736
+ temp_png = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
737
+ plt.savefig(temp_png, dpi=150, bbox_inches='tight',
738
+ facecolor='#f8f9fa', edgecolor='none')
739
+ plt.close()
740
+
741
+ # Convert to video
742
+ img = cv2.imread(str(temp_png))
743
+ if img is not None:
744
+ img = cv2.resize(img, (WIDTH, HEIGHT))
745
+ return animate_image_fade(img, dur, out, fps=FPS)
746
+ else:
747
+ # Last resort: create solid color video
748
+ return create_solid_color_video(dur, out)
749
+
750
+ except Exception as e:
751
+ print(f"Placeholder creation failed: {e}")
752
+ return create_solid_color_video(dur, out)
753
+
754
+
755
+ def create_solid_color_video(dur: float, out: Path) -> str:
756
+ """
757
+ Create a simple solid color video as the ultimate fallback.
758
+
759
+ Args:
760
+ dur (float): Duration in seconds
761
+ out (Path): Output path
762
+
763
+ Returns:
764
+ str: Path to generated video
765
+ """
766
+ try:
767
+ # Create a simple colored frame
768
+ frame = np.full((HEIGHT, WIDTH, 3), [240, 240, 240], dtype=np.uint8)
769
+
770
+ # Add simple text
771
+ cv2.putText(frame, "Data Visualization",
772
+ (WIDTH//2 - 200, HEIGHT//2 - 50),
773
+ cv2.FONT_HERSHEY_SIMPLEX, 2, (100, 100, 100), 3)
774
+
775
+ cv2.putText(frame, "Loading...",
776
+ (WIDTH//2 - 80, HEIGHT//2 + 50),
777
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (150, 150, 150), 2)
778
+
779
+ # Write video
780
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
781
+ video_writer = cv2.VideoWriter(str(out), fourcc, FPS, (WIDTH, HEIGHT))
782
+
783
+ total_frames = int(dur * FPS)
784
+ for i in range(total_frames):
785
+ video_writer.write(frame)
786
+
787
+ video_writer.release()
788
+ return str(out)
789
+
790
+ except Exception as e:
791
+ print(f"Solid color video creation failed: {e}")
792
+ # If even this fails, just return the output path
793
+ return str(out)
794
+
795
+
796
+ def animate_image_fade(img: np.ndarray, dur: float, out: Path, fps: int = 24) -> str:
797
+ """
798
+ Create a fade-in animation for static images.
799
+
800
+ Args:
801
+ img (np.ndarray): Input image in BGR format
802
+ dur (float): Duration in seconds
803
+ out (Path): Output video path
804
+ fps (int): Frames per second
805
+
806
+ Returns:
807
+ str: Path to generated video
808
+ """
809
+ try:
810
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
811
+ video_writer = cv2.VideoWriter(str(out), fourcc, fps, (WIDTH, HEIGHT))
812
+
813
+ total_frames = int(dur * fps)
814
+ fade_frames = min(int(fps * 0.5), total_frames // 3) # 0.5 second fade or 1/3 of total
815
+
816
+ for frame_idx in range(total_frames):
817
+ if frame_idx < fade_frames:
818
+ # Fade in
819
+ alpha = frame_idx / fade_frames
820
+ faded_img = cv2.addWeighted(img, alpha, np.zeros_like(img), 1 - alpha, 0)
821
+ else:
822
+ # Full opacity
823
+ faded_img = img
824
+
825
+ video_writer.write(faded_img)
826
+
827
+ video_writer.release()
828
+ return str(out)
829
+
830
+ except Exception as e:
831
+ print(f"Image fade animation failed: {e}")
832
+ return str(out)
833
+
834
+
835
+ def concat_media(file_paths: List[str], output_path: Path, media_type: str):
836
+ """
837
+ Concatenate multiple media files using FFmpeg.
838
+
839
+ Args:
840
+ file_paths (List[str]): List of input file paths
841
+ output_path (Path): Output file path
842
+ media_type (str): Either 'video' or 'audio'
843
+ """
844
+ if not file_paths:
845
+ return
846
+
847
+ try:
848
+ # Create temporary file list for FFmpeg
849
+ list_file = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.txt"
850
+
851
+ with open(list_file, 'w') as f:
852
+ for path in file_paths:
853
+ # Escape path for FFmpeg
854
+ escaped_path = str(path).replace('\\', '\\\\').replace("'", "\\'")
855
+ f.write(f"file '{escaped_path}'\n")
856
+
857
+ # Build FFmpeg command
858
+ cmd = [
859
+ "ffmpeg", "-y", "-f", "concat", "-safe", "0",
860
+ "-i", str(list_file)
861
+ ]
862
+
863
+ if media_type == "video":
864
+ cmd.extend(["-c", "copy"])
865
+ else: # audio
866
+ cmd.extend(["-c:a", "aac"])
867
+
868
+ cmd.append(str(output_path))
869
+
870
+ # Execute FFmpeg command
871
+ subprocess.run(cmd, check=True, capture_output=True)
872
+
873
+ # Clean up temporary file
874
+ list_file.unlink(missing_ok=True)
875
+
876
+ except Exception as e:
877
+ print(f"Media concatenation failed: {e}")
878
+ # Create a fallback if concatenation fails
879
+ if file_paths:
880
+ # Just copy the first file as a fallback
881
+ subprocess.run(["cp", file_paths[0], str(output_path)], check=False)
882
+
883
  # ─── ENHANCED MAIN FUNCTIONS (DROP-IN REPLACEMENTS) ────────────────────────────
884
 
885
  def generate_report_bundle(buf: bytes, name: str, ctx: str, key: str):