Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| from pptx import Presentation | |
| from io import BytesIO | |
| import json | |
| import anthropic | |
| import os | |
| from multi_llm_provider import get_ai_manager | |
| # Define available templates | |
| TEMPLATES = { | |
| "professional": { | |
| "name": "Professional", | |
| "description": "Clean, corporate style with blue and gray color scheme", | |
| "colors": { | |
| "primary": "#0F52BA", # Blue | |
| "secondary": "#E6E6E6", # Light gray | |
| "accent": "#D4AF37", # Gold | |
| "text": "#333333" # Dark gray | |
| }, | |
| "fonts": { | |
| "title": "Arial, sans-serif", | |
| "body": "Calibri, sans-serif" | |
| } | |
| }, | |
| "creative": { | |
| "name": "Creative", | |
| "description": "Vibrant design with modern elements and bold colors", | |
| "colors": { | |
| "primary": "#FF5757", # Coral red | |
| "secondary": "#FFDE59", # Yellow | |
| "accent": "#5CE1E6", # Turquoise | |
| "text": "#333333" # Dark gray | |
| }, | |
| "fonts": { | |
| "title": "Montserrat, sans-serif", | |
| "body": "Roboto, sans-serif" | |
| } | |
| }, | |
| "minimalist": { | |
| "name": "Minimalist", | |
| "description": "Simple, elegant design with lots of whitespace", | |
| "colors": { | |
| "primary": "#FFFFFF", # White | |
| "secondary": "#F5F5F5", # Off-white | |
| "accent": "#000000", # Black | |
| "text": "#333333" # Dark gray | |
| }, | |
| "fonts": { | |
| "title": "Helvetica, Arial, sans-serif", | |
| "body": "Helvetica, Arial, sans-serif" | |
| } | |
| }, | |
| "custom": { | |
| "name": "Custom", | |
| "description": "Your custom uploaded template", | |
| "colors": { | |
| "primary": "#0073E6", # Blue | |
| "secondary": "#FFFFFF", # White | |
| "accent": "#FF6D00", # Orange | |
| "text": "#333333" # Dark gray | |
| }, | |
| "fonts": { | |
| "title": "Arial, sans-serif", | |
| "body": "Calibri, sans-serif" | |
| } | |
| } | |
| } | |
| def generate_storyboard(title, purpose, audience, model=None): | |
| """Generate a presentation storyboard using the specified AI model""" | |
| # Get AI manager | |
| ai_manager = get_ai_manager() | |
| # Use default model if none specified | |
| if not model: | |
| model = st.session_state.get("default_model", "claude-3-sonnet-20250219") | |
| # Check for web research data | |
| web_research = "" | |
| if hasattr(st.session_state, 'web_research') and st.session_state.web_research: | |
| research_data = st.session_state.web_research | |
| web_research = "Recent information from web research:\n\n" | |
| for i, item in enumerate(research_data[:3]): # Limit to 3 results | |
| title = item.get("title", "No title") | |
| snippet = item.get("snippet", "No snippet") | |
| url = item.get("url", "No URL") | |
| web_research += f"{i+1}. {title}\n" | |
| web_research += f" {snippet}\n" | |
| web_research += f" Source: {url}\n\n" | |
| # Create the prompt | |
| prompt = f""" | |
| You are an expert presentation designer with deep expertise in creating compelling PowerPoint presentations. | |
| I need you to create a storyboard for a presentation with the following details: | |
| - Title: {title} | |
| - Purpose: {purpose} | |
| - Target Audience: {audience} | |
| {web_research if web_research else ""} | |
| For each slide, include: | |
| 1. Slide title | |
| 2. Slide purpose | |
| 3. Key content points | |
| 4. Suggested visual elements | |
| Think deeply about the narrative flow, ensuring each slide builds on the previous one. | |
| Format your response as a JSON list where each item is a slide with the above elements. | |
| Start with an engaging title slide and end with a clear call-to-action or summary slide. | |
| The response should be a valid JSON array like this: | |
| [ | |
| {{ | |
| "title": "Slide Title", | |
| "purpose": "Purpose of this slide", | |
| "key_points": ["Key point 1", "Key point 2"], | |
| "visual_elements": ["Visual element 1", "Visual element 2"] | |
| }}, | |
| ... | |
| ] | |
| """ | |
| system_prompt = "You are an expert presentation designer specializing in creating logical, compelling PowerPoint storyboards. Always output valid JSON without explanations or extra text." | |
| with st.sidebar: | |
| progress_text = st.empty() | |
| progress_text.write(f"Generating storyboard using {model}...") | |
| try: | |
| # Generate storyboard using the selected model | |
| response_text = ai_manager.generate_text( | |
| prompt=prompt, | |
| model=model, | |
| system_prompt=system_prompt, | |
| temperature=st.session_state.get("ai_temperature", 0.7), | |
| max_tokens=4000 | |
| ) | |
| # For debugging, show raw response | |
| st.sidebar.write("Raw response: " + response_text[:200] + "...") | |
| # Extract the JSON from the response | |
| json_start = response_text.find("[") | |
| json_end = response_text.rfind("]") + 1 | |
| if json_start >= 0 and json_end > 0: | |
| json_str = response_text[json_start:json_end] | |
| parsed_json = json.loads(json_str) | |
| progress_text.write(f"Generated storyboard with {len(parsed_json)} slides") | |
| # Add AI model used to each slide for future reference | |
| for slide in parsed_json: | |
| if "ai_settings" not in slide: | |
| slide["ai_settings"] = {} | |
| slide["ai_settings"]["model"] = model | |
| return parsed_json | |
| else: | |
| progress_text.error("JSON not found in response") | |
| st.sidebar.error(f"Full response: {response_text}") | |
| # Create a default storyboard as fallback | |
| return default_storyboard(title) | |
| except Exception as e: | |
| with st.sidebar: | |
| st.error(f"Error generating storyboard: {str(e)}") | |
| import traceback | |
| st.error(traceback.format_exc()) | |
| return default_storyboard(title) | |
| def default_storyboard(title): | |
| """Create a default storyboard when API calls fail""" | |
| return [ | |
| { | |
| "title": title, | |
| "purpose": "Title slide", | |
| "key_points": ["Introduction"], | |
| "visual_elements": ["Company logo", "Presenter name"] | |
| }, | |
| { | |
| "title": "Agenda", | |
| "purpose": "Outline the presentation contents", | |
| "key_points": ["Overview of key topics", "Agenda items", "Presentation goals"], | |
| "visual_elements": ["Bullet list", "Simple timeline graphic"] | |
| }, | |
| { | |
| "title": "Key Points", | |
| "purpose": "Present main content", | |
| "key_points": ["Main point 1", "Main point 2", "Main point 3"], | |
| "visual_elements": ["Relevant icons", "Supporting image"] | |
| }, | |
| { | |
| "title": "Conclusion", | |
| "purpose": "Summarize key points and next steps", | |
| "key_points": ["Summary of main points", "Call to action", "Next steps"], | |
| "visual_elements": ["Summary graphic", "Contact information"] | |
| } | |
| ] | |
| def generate_slide_content(slide_info, template_name, model=None): | |
| """Generate detailed content for a slide using the specified AI model""" | |
| # Get AI manager | |
| ai_manager = get_ai_manager() | |
| # Use default model if none specified | |
| if not model: | |
| model = slide_info.get("ai_settings", {}).get("model", st.session_state.get("default_model", "claude-3-sonnet-20250219")) | |
| prompt = f""" | |
| Create detailed content for a PowerPoint slide with the following information: | |
| Slide Title: {slide_info.get('title', 'Untitled')} | |
| Slide Purpose: {slide_info.get('purpose', 'No purpose provided')} | |
| Key Points: {slide_info.get('key_points', [])} | |
| Visual Elements: {slide_info.get('visual_elements', [])} | |
| The presentation uses the {template_name} template style. | |
| Provide the following in your response as JSON: | |
| 1. Final slide title (refined if needed) | |
| 2. Main content text (bullet points or paragraphs as appropriate) | |
| 3. Notes for the presenter | |
| 4. Specific visual instructions (charts, images, etc.) | |
| Your response must be a valid JSON object like this: | |
| {{ | |
| "title": "Refined Title", | |
| "content": ["Point 1", "Point 2", "Point 3"], | |
| "notes": "Notes for the presenter", | |
| "visual_instructions": "Details about visuals" | |
| }} | |
| Focus on clarity, impact, and alignment with the slide's purpose. | |
| """ | |
| system_prompt = "You are an expert presentation content creator. Always output valid JSON without explanations or extra text." | |
| try: | |
| # Generate content using the selected model | |
| response_text = ai_manager.generate_text( | |
| prompt=prompt, | |
| model=model, | |
| system_prompt=system_prompt, | |
| temperature=st.session_state.get("ai_temperature", 0.5), | |
| max_tokens=2000 | |
| ) | |
| # Extract the JSON from the response | |
| json_start = response_text.find("{") | |
| json_end = response_text.rfind("}") + 1 | |
| if json_start >= 0 and json_end > 0: | |
| json_str = response_text[json_start:json_end] | |
| content = json.loads(json_str) | |
| # Store AI model used | |
| if "ai_settings" not in content: | |
| content["ai_settings"] = {} | |
| content["ai_settings"]["model"] = model | |
| return content | |
| else: | |
| # Fallback: create a basic slide | |
| st.sidebar.error(f"JSON not found in response: {response_text}") | |
| return default_slide_content(slide_info) | |
| except Exception as e: | |
| st.sidebar.error(f"Error generating slide content: {str(e)}") | |
| import traceback | |
| st.sidebar.error(traceback.format_exc()) | |
| # Return a default structure | |
| return default_slide_content(slide_info) | |
| def default_slide_content(slide_info): | |
| """Create default slide content when API calls fail""" | |
| return { | |
| "title": slide_info.get('title', 'Untitled'), | |
| "content": slide_info.get('key_points', ["Content to be added"]), | |
| "notes": f"This slide covers: {slide_info.get('purpose', 'No purpose specified')}", | |
| "visual_instructions": "Suggested visuals will appear here" | |
| } | |
| def create_slide_preview(slide, template_name): | |
| """Create an HTML preview of a slide""" | |
| # Get template info | |
| template = TEMPLATES.get(template_name, TEMPLATES["professional"]) | |
| colors = template["colors"] | |
| fonts = template["fonts"] | |
| # Apply custom colors if available | |
| if "custom_colors" in st.session_state and template_name in st.session_state.custom_colors: | |
| custom_colors = st.session_state.custom_colors[template_name] | |
| colors["primary"] = custom_colors.get("primary", colors["primary"]) | |
| colors["accent"] = custom_colors.get("accent", colors["accent"]) | |
| # Prepare content | |
| title = slide.get('title', 'Untitled Slide') | |
| if isinstance(slide.get('content', []), list): | |
| content = slide.get('content', []) | |
| else: | |
| content_text = slide.get('content', '') | |
| content = content_text.split('\n') if content_text else [] | |
| # Determine layout based on slide design or content | |
| layout_type = "standard" | |
| if "design" in slide and "layout" in slide["design"]: | |
| layout_type = slide["design"]["layout"].lower() | |
| # Generate HTML based on layout type | |
| if "two column" in layout_type or "comparison" in layout_type: | |
| # Two-column layout | |
| mid_point = len(content) // 2 | |
| left_content = content[:mid_point] | |
| right_content = content[mid_point:] | |
| content_html = f""" | |
| <div class="row" style="display: flex; flex-direction: row;"> | |
| <div class="col" style="flex: 1; padding-right: 10px;"> | |
| <ul> | |
| {"".join(f'<li style="margin-bottom: 8px;">{item}</li>' for item in left_content)} | |
| </ul> | |
| </div> | |
| <div class="col" style="flex: 1; padding-left: 10px;"> | |
| <ul> | |
| {"".join(f'<li style="margin-bottom: 8px;">{item}</li>' for item in right_content)} | |
| </ul> | |
| </div> | |
| </div> | |
| """ | |
| elif "quote" in layout_type: | |
| # Quote layout | |
| if content: | |
| quote_text = content[0] if isinstance(content, list) and len(content) > 0 else "Quote text" | |
| author = content[1] if isinstance(content, list) and len(content) > 1 else "" | |
| content_html = f""" | |
| <div class="quote-container" style="text-align: center; padding: 20px;"> | |
| <blockquote style="font-size: 24px; font-style: italic; color: {colors['accent']};"> | |
| "{quote_text}" | |
| </blockquote> | |
| {f'<div style="text-align: right; font-size: 18px;">— {author}</div>' if author else ''} | |
| </div> | |
| """ | |
| else: | |
| content_html = '<div class="quote-container" style="text-align: center;"><p>No quote content</p></div>' | |
| else: | |
| # Standard layout with bullet points | |
| content_html = f""" | |
| <ul> | |
| {"".join(f'<li style="margin-bottom: 8px;">{item}</li>' for item in content)} | |
| </ul> | |
| """ | |
| # Create the full HTML preview | |
| html = f""" | |
| <div class="slide-preview" style="background-color: {colors['secondary']}; border: 1px solid #dee2e6; border-radius: 4px; padding: 20px; height: 300px; overflow: hidden; font-family: {fonts['body']};"> | |
| <div class="slide-title" style="color: {colors['text']}; font-family: {fonts['title']}; font-size: 24px; margin-bottom: 20px; border-bottom: 2px solid {colors['primary']}; padding-bottom: 10px;"> | |
| {title} | |
| </div> | |
| <div class="slide-content" style="color: {colors['text']}; font-family: {fonts['body']}; font-size: 16px;"> | |
| {content_html} | |
| </div> | |
| </div> | |
| """ | |
| return html | |
| def add_custom_template(template_file): | |
| """Add a custom template from an uploaded file""" | |
| try: | |
| # Validate it's a proper PowerPoint file | |
| file_content = template_file.getvalue() | |
| prs = Presentation(BytesIO(file_content)) | |
| # Store in session state | |
| st.session_state.custom_template = file_content | |
| st.session_state.custom_template_name = template_file.name | |
| # Extract template information if possible | |
| try: | |
| # Try to extract color scheme from the template | |
| background_color = "#FFFFFF" # Default | |
| accent_color = "#0073E6" # Default | |
| # Look for theme colors in the first slide if available | |
| if len(prs.slides) > 0: | |
| slide = prs.slides[0] | |
| # Look for background color | |
| for shape in slide.shapes: | |
| if hasattr(shape, 'fill') and shape.fill.type != 0: # 0 is None | |
| if hasattr(shape.fill, 'fore_color') and hasattr(shape.fill.fore_color, 'rgb'): | |
| rgb = shape.fill.fore_color.rgb | |
| if rgb: | |
| background_color = f"#{rgb.r:02x}{rgb.g:02x}{rgb.b:02x}" | |
| break | |
| # Update custom template with extracted colors | |
| TEMPLATES["custom"]["colors"]["primary"] = accent_color | |
| TEMPLATES["custom"]["colors"]["secondary"] = background_color | |
| # Get the number of slide layouts available | |
| num_layouts = len(prs.slide_layouts) | |
| st.session_state.custom_template_layouts = num_layouts | |
| except Exception as extract_error: | |
| st.warning(f"Could not extract all template details: {str(extract_error)}") | |
| return True, f"Custom template '{template_file.name}' uploaded successfully with {len(prs.slides)} master layouts!" | |
| except Exception as e: | |
| return False, f"Error with template file: {str(e)}" |