Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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):
|