885 lines
39 KiB
Python
885 lines
39 KiB
Python
import logging
|
|
import base64
|
|
import io
|
|
from typing import Dict, Any, Optional, Tuple, List
|
|
from .rendererBaseTemplate import BaseRenderer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RendererPptx(BaseRenderer):
|
|
"""Renderer for PowerPoint (.pptx) files using python-pptx library."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.supported_formats = ["pptx", "ppt"]
|
|
self.output_mime_type = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
|
|
@classmethod
|
|
def get_supported_formats(cls) -> list:
|
|
"""Get list of supported output formats."""
|
|
return ["pptx", "ppt"]
|
|
|
|
async def render(self, extracted_content: Dict[str, Any], title: str, user_prompt: str = None, ai_service=None) -> Tuple[str, str]:
|
|
"""
|
|
Render content as PowerPoint presentation from JSON data.
|
|
|
|
Args:
|
|
extracted_content: JSON content to render as presentation
|
|
title: Title for the presentation
|
|
user_prompt: User prompt for AI styling
|
|
ai_service: AI service for styling
|
|
**kwargs: Additional rendering options
|
|
|
|
Returns:
|
|
Base64-encoded PowerPoint presentation as string
|
|
"""
|
|
try:
|
|
# Import python-pptx
|
|
from pptx import Presentation
|
|
from pptx.util import Inches, Pt
|
|
from pptx.enum.text import PP_ALIGN
|
|
from pptx.dml.color import RGBColor
|
|
import re
|
|
|
|
# Get AI-generated styling definitions first
|
|
styles = await self._get_pptx_styles(user_prompt, ai_service)
|
|
|
|
# Create new presentation
|
|
prs = Presentation()
|
|
|
|
# Set slide size based on user intent (default to 16:9)
|
|
slide_size = styles.get("slide_size", "16:9")
|
|
if slide_size == "4:3":
|
|
prs.slide_width = Inches(10)
|
|
prs.slide_height = Inches(7.5)
|
|
else: # Default to 16:9
|
|
prs.slide_width = Inches(13.33)
|
|
prs.slide_height = Inches(7.5)
|
|
|
|
# Generate slides from JSON content
|
|
slides_data = await self._parse_json_to_slides(extracted_content, title, styles)
|
|
logger.info(f"Parsed {len(slides_data)} slides from JSON content")
|
|
|
|
# Debug: Show first 200 chars of content
|
|
logger.info(f"JSON content preview: {str(extracted_content)[:200]}...")
|
|
|
|
for i, slide_data in enumerate(slides_data):
|
|
logger.info(f"Slide {i+1}: '{slide_data.get('title', 'No title')}' - {len(slide_data.get('content', ''))} chars")
|
|
# Debug: Show slide content preview
|
|
slide_content = slide_data.get('content', '')
|
|
if slide_content:
|
|
logger.info(f" Content preview: '{slide_content[:100]}...'")
|
|
else:
|
|
logger.warning(f" ⚠️ Slide {i+1} has NO content!")
|
|
|
|
# Create slide with appropriate layout based on content
|
|
slide_layout_index = self._get_slide_layout_index(slide_data, styles)
|
|
slide_layout = prs.slide_layouts[slide_layout_index]
|
|
slide = prs.slides.add_slide(slide_layout)
|
|
|
|
# Set title with AI-generated styling
|
|
title_shape = slide.shapes.title
|
|
title_shape.text = slide_data.get("title", "Slide")
|
|
|
|
# Apply title styling
|
|
title_style = styles.get("title", {})
|
|
if title_shape.text_frame.paragraphs[0].font:
|
|
title_shape.text_frame.paragraphs[0].font.size = Pt(title_style.get("font_size", 44))
|
|
title_shape.text_frame.paragraphs[0].font.bold = title_style.get("bold", True)
|
|
title_color = self._get_safe_color(title_style.get("color", (31, 78, 121)))
|
|
title_shape.text_frame.paragraphs[0].font.color.rgb = RGBColor(*title_color)
|
|
|
|
# Set content with AI-generated styling
|
|
content_shape = slide.placeholders[1]
|
|
content_text = slide_data.get("content", "")
|
|
|
|
# Format content text with AI styles
|
|
text_frame = content_shape.text_frame
|
|
text_frame.clear()
|
|
|
|
# Split content into paragraphs
|
|
paragraphs = content_text.split('\n\n')
|
|
|
|
for i, paragraph in enumerate(paragraphs):
|
|
if paragraph.strip():
|
|
if i == 0:
|
|
p = text_frame.paragraphs[0]
|
|
else:
|
|
p = text_frame.add_paragraph()
|
|
|
|
p.text = paragraph.strip()
|
|
|
|
# Apply AI-generated styling based on content type
|
|
if paragraph.startswith('#'):
|
|
# Header
|
|
p.text = paragraph.lstrip('#').strip()
|
|
heading_style = styles.get("heading", {})
|
|
p.font.size = Pt(heading_style.get("font_size", 32))
|
|
p.font.bold = heading_style.get("bold", True)
|
|
heading_color = self._get_safe_color(heading_style.get("color", (47, 47, 47)))
|
|
p.font.color.rgb = RGBColor(*heading_color)
|
|
elif paragraph.startswith('##'):
|
|
# Subheader
|
|
p.text = paragraph.lstrip('#').strip()
|
|
subheading_style = styles.get("subheading", {})
|
|
p.font.size = Pt(subheading_style.get("font_size", 24))
|
|
p.font.bold = subheading_style.get("bold", True)
|
|
subheading_color = self._get_safe_color(subheading_style.get("color", (79, 79, 79)))
|
|
p.font.color.rgb = RGBColor(*subheading_color)
|
|
elif paragraph.startswith('*') and paragraph.endswith('*'):
|
|
# Bold text
|
|
p.text = paragraph.strip('*')
|
|
paragraph_style = styles.get("paragraph", {})
|
|
p.font.size = Pt(paragraph_style.get("font_size", 18))
|
|
p.font.bold = True
|
|
paragraph_color = self._get_safe_color(paragraph_style.get("color", (47, 47, 47)))
|
|
p.font.color.rgb = RGBColor(*paragraph_color)
|
|
else:
|
|
# Regular text
|
|
paragraph_style = styles.get("paragraph", {})
|
|
p.font.size = Pt(paragraph_style.get("font_size", 18))
|
|
p.font.bold = paragraph_style.get("bold", False)
|
|
paragraph_color = self._get_safe_color(paragraph_style.get("color", (47, 47, 47)))
|
|
p.font.color.rgb = RGBColor(*paragraph_color)
|
|
|
|
# Apply alignment
|
|
align = paragraph_style.get("align", "left")
|
|
if align == "center":
|
|
p.alignment = PP_ALIGN.CENTER
|
|
elif align == "right":
|
|
p.alignment = PP_ALIGN.RIGHT
|
|
else:
|
|
p.alignment = PP_ALIGN.LEFT
|
|
|
|
# If no slides were created, create a default slide
|
|
if not slides_data:
|
|
slide_layout = prs.slide_layouts[0] # Title slide layout
|
|
slide = prs.slides.add_slide(slide_layout)
|
|
|
|
title_shape = slide.shapes.title
|
|
title_shape.text = title
|
|
|
|
# Apply title styling to default slide
|
|
title_style = styles.get("title", {})
|
|
if title_shape.text_frame.paragraphs[0].font:
|
|
title_shape.text_frame.paragraphs[0].font.size = Pt(title_style.get("font_size", 48))
|
|
title_shape.text_frame.paragraphs[0].font.bold = title_style.get("bold", True)
|
|
title_color = self._get_safe_color(title_style.get("color", (31, 78, 121)))
|
|
title_shape.text_frame.paragraphs[0].font.color.rgb = RGBColor(*title_color)
|
|
|
|
subtitle_shape = slide.placeholders[1]
|
|
subtitle_shape.text = "Generated by PowerOn AI System"
|
|
|
|
# Apply subtitle styling
|
|
paragraph_style = styles.get("paragraph", {})
|
|
if subtitle_shape.text_frame.paragraphs[0].font:
|
|
subtitle_shape.text_frame.paragraphs[0].font.size = Pt(paragraph_style.get("font_size", 20))
|
|
subtitle_shape.text_frame.paragraphs[0].font.bold = paragraph_style.get("bold", False)
|
|
paragraph_color = self._get_safe_color(paragraph_style.get("color", (47, 47, 47)))
|
|
subtitle_shape.text_frame.paragraphs[0].font.color.rgb = RGBColor(*paragraph_color)
|
|
|
|
# Save to buffer
|
|
buffer = io.BytesIO()
|
|
prs.save(buffer)
|
|
buffer.seek(0)
|
|
|
|
# Convert to base64
|
|
pptx_bytes = buffer.getvalue()
|
|
pptx_base64 = base64.b64encode(pptx_bytes).decode('utf-8')
|
|
|
|
logger.info(f"Successfully rendered PowerPoint presentation: {len(pptx_bytes)} bytes")
|
|
return pptx_base64, "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
|
|
except ImportError:
|
|
logger.error("python-pptx library not installed. Install with: pip install python-pptx")
|
|
return "python-pptx library not installed", "text/plain"
|
|
except Exception as e:
|
|
logger.error(f"Error rendering PowerPoint presentation: {str(e)}")
|
|
return f"Error rendering PowerPoint presentation: {str(e)}", "text/plain"
|
|
|
|
def _parse_content_to_slides(self, content: str, title: str) -> list:
|
|
"""
|
|
Parse content into slide data structure.
|
|
|
|
Args:
|
|
content: Content to parse
|
|
title: Presentation title
|
|
|
|
Returns:
|
|
List of slide data dictionaries
|
|
"""
|
|
slides = []
|
|
|
|
# Split content by slide markers or headers
|
|
slide_sections = self._split_content_into_slides(content)
|
|
|
|
for i, section in enumerate(slide_sections):
|
|
if section.strip():
|
|
slide_data = {
|
|
"title": f"Slide {i + 1}",
|
|
"content": section.strip()
|
|
}
|
|
|
|
# Extract title from content if it starts with #
|
|
lines = section.strip().split('\n')
|
|
if lines and lines[0].startswith('#'):
|
|
# Remove # symbols and clean up title
|
|
slide_title = lines[0].lstrip('#').strip()
|
|
slide_data["title"] = slide_title
|
|
slide_data["content"] = '\n'.join(lines[1:]).strip()
|
|
elif lines and lines[0].strip():
|
|
# Use first line as title if it looks like a title
|
|
first_line = lines[0].strip()
|
|
if len(first_line) < 100 and not first_line.endswith('.'):
|
|
slide_data["title"] = first_line
|
|
slide_data["content"] = '\n'.join(lines[1:]).strip()
|
|
|
|
slides.append(slide_data)
|
|
|
|
return slides
|
|
|
|
def _split_content_into_slides(self, content: str) -> list:
|
|
"""
|
|
Split content into individual slides based on headers and structure.
|
|
|
|
Args:
|
|
content: Content to split
|
|
|
|
Returns:
|
|
List of slide content strings
|
|
"""
|
|
import re
|
|
|
|
# First, try to split by major headers (# or ##)
|
|
# This is the most common case for AI-generated content
|
|
header_pattern = r'^(#{1,2})\s+(.+)$'
|
|
lines = content.split('\n')
|
|
slides = []
|
|
current_slide = []
|
|
|
|
for line in lines:
|
|
# Check if this line is a header
|
|
header_match = re.match(header_pattern, line.strip())
|
|
if header_match:
|
|
# If we have content in current slide, save it
|
|
if current_slide:
|
|
slide_content = '\n'.join(current_slide).strip()
|
|
if slide_content:
|
|
slides.append(slide_content)
|
|
current_slide = []
|
|
|
|
# Start new slide with this header
|
|
current_slide.append(line)
|
|
else:
|
|
# Add line to current slide
|
|
current_slide.append(line)
|
|
|
|
# Add the last slide
|
|
if current_slide:
|
|
slide_content = '\n'.join(current_slide).strip()
|
|
if slide_content:
|
|
slides.append(slide_content)
|
|
|
|
# If we found slides with headers, return them
|
|
if len(slides) > 1:
|
|
return slides
|
|
|
|
# Fallback: Split by double newlines
|
|
sections = content.split('\n\n\n')
|
|
if len(sections) > 1:
|
|
return [s.strip() for s in sections if s.strip()]
|
|
|
|
# Another fallback: Split by double newlines
|
|
sections = content.split('\n\n')
|
|
if len(sections) > 1:
|
|
return [s.strip() for s in sections if s.strip()]
|
|
|
|
# Last resort: return as single slide
|
|
return [content.strip()]
|
|
|
|
|
|
def get_output_mime_type(self) -> str:
|
|
"""Get MIME type for rendered output."""
|
|
return self.output_mime_type
|
|
|
|
async def _get_pptx_styles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]:
|
|
"""Get PowerPoint styling definitions using base template AI styling."""
|
|
style_schema = {
|
|
"title": {"font_size": 52, "color": "#1B365D", "bold": True, "align": "center"},
|
|
"heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"},
|
|
"subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"},
|
|
"paragraph": {"font_size": 20, "color": "#2F2F2F", "bold": False, "align": "left"},
|
|
"bullet_list": {"font_size": 20, "color": "#2F2F2F", "indent": 20},
|
|
"table_header": {"font_size": 18, "color": "#FFFFFF", "bold": True, "background": "#1B365D"},
|
|
"table_cell": {"font_size": 16, "color": "#2F2F2F", "bold": False, "background": "#F8F9FA"},
|
|
"slide_size": "16:9",
|
|
"content_per_slide": "concise",
|
|
"design_theme": "corporate",
|
|
"color_scheme": "professional",
|
|
"background_style": "clean",
|
|
"accent_colors": ["#1B365D", "#2C5F2D", "#4A90E2", "#6B7280"],
|
|
"professional_grade": True,
|
|
"executive_ready": True
|
|
}
|
|
|
|
style_template = self._create_professional_pptx_template(user_prompt, style_schema)
|
|
# Use our own _get_ai_styles_with_pptx_colors method to ensure proper color conversion
|
|
styles = await self._get_ai_styles_with_pptx_colors(ai_service, style_template, self._get_default_pptx_styles())
|
|
|
|
# Validate PowerPoint-specific requirements
|
|
return self._validate_pptx_styles_readability(styles)
|
|
|
|
def _create_professional_pptx_template(self, user_prompt: str, style_schema: Dict[str, Any]) -> str:
|
|
"""Create a professional PowerPoint-specific AI style template for corporate-quality slides."""
|
|
import json
|
|
schema_json = json.dumps(style_schema, indent=4)
|
|
|
|
return f"""Customize the JSON below for professional PowerPoint slides.
|
|
|
|
User Request: {user_prompt or "Create professional corporate slides"}
|
|
|
|
Rules:
|
|
- Use professional colors (blues, grays, deep greens)
|
|
- Large, readable font sizes
|
|
- High contrast
|
|
- Sophisticated color palettes
|
|
|
|
Return ONLY this JSON with your changes:
|
|
|
|
{schema_json}
|
|
|
|
JSON ONLY. NO OTHER TEXT."""
|
|
|
|
async def _get_ai_styles_with_pptx_colors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Get AI styles with proper PowerPoint color conversion."""
|
|
if not ai_service:
|
|
return default_styles
|
|
|
|
try:
|
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
|
|
|
|
request_options = AiCallOptions()
|
|
request_options.operationType = OperationTypeEnum.DATA_GENERATE
|
|
|
|
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
|
|
|
# Check if AI service is properly configured
|
|
if not hasattr(ai_service, 'aiObjects') or not ai_service.aiObjects:
|
|
self.logger.warning("AI service not properly configured, using defaults")
|
|
return default_styles
|
|
|
|
response = await ai_service.aiObjects.call(request)
|
|
|
|
# Check if response is valid
|
|
if not response:
|
|
self.logger.warning("AI service returned no response, using defaults")
|
|
return default_styles
|
|
|
|
import json
|
|
import re
|
|
|
|
# Clean and parse JSON
|
|
result = response.content.strip() if response and response.content else ""
|
|
|
|
# Check if result is empty
|
|
if not result:
|
|
self.logger.warning("AI styling returned empty response, using defaults")
|
|
return default_styles
|
|
|
|
# Log the raw response for debugging
|
|
self.logger.debug(f"AI styling raw response: {result[:200]}...")
|
|
|
|
# Extract JSON from various formats
|
|
json_match = re.search(r'```json\s*\n(.*?)\n```', result, re.DOTALL)
|
|
if json_match:
|
|
result = json_match.group(1).strip()
|
|
elif result.startswith('```json'):
|
|
result = re.sub(r'^```json\s*', '', result)
|
|
result = re.sub(r'\s*```$', '', result)
|
|
elif result.startswith('```'):
|
|
result = re.sub(r'^```\s*', '', result)
|
|
result = re.sub(r'\s*```$', '', result)
|
|
|
|
# Try to extract JSON from explanatory text
|
|
json_patterns = [
|
|
r'\{[^{}]*"title"[^{}]*\}', # Simple JSON object
|
|
r'\{.*?"title".*?\}', # JSON with title field
|
|
r'\{.*?"font_size".*?\}', # JSON with font_size field
|
|
]
|
|
|
|
for pattern in json_patterns:
|
|
json_match = re.search(pattern, result, re.DOTALL)
|
|
if json_match:
|
|
result = json_match.group(0)
|
|
break
|
|
|
|
# Additional cleanup - remove any leading/trailing whitespace and newlines
|
|
result = result.strip()
|
|
|
|
# Check if result is still empty after cleanup
|
|
if not result:
|
|
self.logger.warning("AI styling returned empty content after cleanup, using defaults")
|
|
return default_styles
|
|
|
|
# Try to parse JSON
|
|
try:
|
|
styles = json.loads(result)
|
|
self.logger.debug(f"Successfully parsed AI styles: {list(styles.keys())}")
|
|
except json.JSONDecodeError as json_error:
|
|
self.logger.warning(f"AI styling returned invalid JSON: {json_error}")
|
|
self.logger.warning(f"Raw content that failed to parse: {result[:100]}...")
|
|
# Try to extract just the JSON part if it's embedded in text
|
|
json_start = result.find('{')
|
|
json_end = result.rfind('}')
|
|
if json_start != -1 and json_end != -1 and json_end > json_start:
|
|
json_part = result[json_start:json_end+1]
|
|
try:
|
|
styles = json.loads(json_part)
|
|
self.logger.info("Successfully extracted JSON from explanatory text")
|
|
self.logger.debug(f"Extracted AI styles: {list(styles.keys())}")
|
|
except json.JSONDecodeError:
|
|
self.logger.warning("Could not extract valid JSON from response, using defaults")
|
|
return default_styles
|
|
else:
|
|
return default_styles
|
|
|
|
# Convert colors to PowerPoint RGB format
|
|
styles = self._convert_colors_format(styles)
|
|
|
|
return styles
|
|
|
|
except Exception as e:
|
|
self.logger.warning(f"AI styling failed: {str(e)}, using defaults")
|
|
return default_styles
|
|
|
|
def _convert_colors_format(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Convert hex colors to RGB format for PowerPoint compatibility."""
|
|
try:
|
|
for style_name, style_config in styles.items():
|
|
if isinstance(style_config, dict):
|
|
for prop, value in style_config.items():
|
|
if isinstance(value, str) and value.startswith('#'):
|
|
# Convert hex to RGB tuple for PowerPoint
|
|
hex_color = value.lstrip('#')
|
|
if len(hex_color) == 6:
|
|
r = int(hex_color[0:2], 16)
|
|
g = int(hex_color[2:4], 16)
|
|
b = int(hex_color[4:6], 16)
|
|
styles[style_name][prop] = (r, g, b)
|
|
elif len(hex_color) == 8: # aRGB format
|
|
r = int(hex_color[2:4], 16)
|
|
g = int(hex_color[4:6], 16)
|
|
b = int(hex_color[6:8], 16)
|
|
styles[style_name][prop] = (r, g, b)
|
|
return styles
|
|
except Exception as e:
|
|
self.logger.warning(f"Color conversion failed: {str(e)}")
|
|
return styles
|
|
|
|
def _get_safe_color(self, color_value, default=(0, 0, 0)) -> tuple:
|
|
"""Get a safe RGB color tuple for PowerPoint."""
|
|
if isinstance(color_value, tuple) and len(color_value) == 3:
|
|
return color_value
|
|
elif isinstance(color_value, str) and color_value.startswith('#'):
|
|
hex_color = color_value.lstrip('#')
|
|
if len(hex_color) == 6:
|
|
r = int(hex_color[0:2], 16)
|
|
g = int(hex_color[2:4], 16)
|
|
b = int(hex_color[4:6], 16)
|
|
return (r, g, b)
|
|
elif len(hex_color) == 8: # aRGB format
|
|
r = int(hex_color[2:4], 16)
|
|
g = int(hex_color[4:6], 16)
|
|
b = int(hex_color[6:8], 16)
|
|
return (r, g, b)
|
|
return default
|
|
|
|
def _validate_pptx_styles_readability(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Validate and fix readability issues in AI-generated styles."""
|
|
try:
|
|
# Ensure minimum font sizes for PowerPoint readability
|
|
min_font_sizes = {
|
|
"title": 36,
|
|
"heading": 24,
|
|
"subheading": 20,
|
|
"paragraph": 14,
|
|
"bullet_list": 14,
|
|
"table_header": 12,
|
|
"table_cell": 12
|
|
}
|
|
|
|
for style_name, min_size in min_font_sizes.items():
|
|
if style_name in styles:
|
|
current_size = styles[style_name].get("font_size", 12)
|
|
if current_size < min_size:
|
|
styles[style_name]["font_size"] = min_size
|
|
|
|
return styles
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Style validation failed: {str(e)}")
|
|
return self._get_default_pptx_styles()
|
|
|
|
def _get_default_pptx_styles(self) -> Dict[str, Any]:
|
|
"""Default PowerPoint styles with corporate professional color scheme."""
|
|
return {
|
|
"title": {"font_size": 52, "color": (27, 54, 93), "bold": True, "align": "center"},
|
|
"heading": {"font_size": 36, "color": (44, 95, 45), "bold": True, "align": "left"},
|
|
"subheading": {"font_size": 28, "color": (74, 144, 226), "bold": True, "align": "left"},
|
|
"paragraph": {"font_size": 20, "color": (47, 47, 47), "bold": False, "align": "left"},
|
|
"bullet_list": {"font_size": 20, "color": (47, 47, 47), "indent": 20},
|
|
"table_header": {"font_size": 18, "color": (255, 255, 255), "bold": True, "background": (27, 54, 93)},
|
|
"table_cell": {"font_size": 16, "color": (47, 47, 47), "bold": False, "background": (248, 249, 250)},
|
|
"slide_size": "16:9",
|
|
"content_per_slide": "concise",
|
|
"design_theme": "corporate",
|
|
"color_scheme": "professional",
|
|
"background_style": "clean",
|
|
"accent_colors": [(27, 54, 93), (44, 95, 45), (74, 144, 226), (107, 114, 128)],
|
|
"professional_grade": True,
|
|
"executive_ready": True
|
|
}
|
|
|
|
async def _parse_json_to_slides(self, json_content: Dict[str, Any], title: str, styles: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Parse JSON content into slide data structure.
|
|
|
|
Args:
|
|
json_content: JSON content to parse
|
|
title: Presentation title
|
|
styles: AI-generated styles
|
|
|
|
Returns:
|
|
List of slide data dictionaries
|
|
"""
|
|
slides = []
|
|
|
|
try:
|
|
# Validate JSON structure
|
|
if not isinstance(json_content, dict):
|
|
raise ValueError("JSON content must be a dictionary")
|
|
|
|
if "sections" not in json_content:
|
|
raise ValueError("JSON content must contain 'sections' field")
|
|
|
|
# Use title from JSON metadata if available, otherwise use provided title
|
|
document_title = json_content.get("metadata", {}).get("title", title)
|
|
|
|
# Create title slide
|
|
slides.append({
|
|
"title": document_title,
|
|
"content": "Generated by PowerOn AI System\n\n" + self._format_timestamp()
|
|
})
|
|
|
|
# Process sections into slides based on content and user intent
|
|
sections = json_content.get("sections", [])
|
|
slides.extend(self._create_slides_from_sections(sections, styles))
|
|
|
|
# If no content slides were created, create a default content slide
|
|
if len(slides) == 1: # Only title slide
|
|
slides.append({
|
|
"title": "Content Overview",
|
|
"content": "No structured content found in the source documents.\n\nPlease check the source documents and try again."
|
|
})
|
|
|
|
return slides
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing JSON to slides: {str(e)}")
|
|
# Return minimal fallback slides
|
|
return [
|
|
{
|
|
"title": title,
|
|
"content": "Error parsing content for presentation"
|
|
}
|
|
]
|
|
|
|
def _create_slide_from_section(self, section: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create a slide from a JSON section."""
|
|
try:
|
|
# Get section title from data or use default
|
|
section_title = "Untitled Section"
|
|
if section.get("content_type") == "heading":
|
|
# Extract text from elements array
|
|
for element in section.get("elements", []):
|
|
if isinstance(element, dict) and "text" in element:
|
|
section_title = element.get("text", "Untitled Section")
|
|
break
|
|
elif section.get("title"):
|
|
section_title = section.get("title")
|
|
|
|
content_type = section.get("content_type", "paragraph")
|
|
elements = section.get("elements", [])
|
|
|
|
# Build slide content based on section type
|
|
content_parts = []
|
|
|
|
if content_type == "table":
|
|
content_parts.append(self._format_table_for_slide(elements))
|
|
elif content_type == "list":
|
|
content_parts.append(self._format_list_for_slide(elements))
|
|
elif content_type == "heading":
|
|
content_parts.append(self._format_heading_for_slide(elements))
|
|
elif content_type == "paragraph":
|
|
content_parts.append(self._format_paragraph_for_slide(elements))
|
|
elif content_type == "code":
|
|
content_parts.append(self._format_code_for_slide(elements))
|
|
else:
|
|
content_parts.append(self._format_paragraph_for_slide(elements))
|
|
|
|
# Combine content parts
|
|
slide_content = "\n\n".join(filter(None, content_parts))
|
|
|
|
return {
|
|
"title": section_title,
|
|
"content": slide_content
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error creating slide from section: {str(e)}")
|
|
return None
|
|
|
|
def _format_table_for_slide(self, elements: List[Dict[str, Any]]) -> str:
|
|
"""Format table data for slide presentation."""
|
|
try:
|
|
# Extract table data from elements array
|
|
headers = []
|
|
rows = []
|
|
for element in elements:
|
|
if isinstance(element, dict) and "headers" in element and "rows" in element:
|
|
headers = element.get("headers", [])
|
|
rows = element.get("rows", [])
|
|
break
|
|
|
|
if not headers:
|
|
return ""
|
|
|
|
# Create table representation
|
|
table_lines = []
|
|
|
|
# Add headers
|
|
header_line = " | ".join(str(h) for h in headers)
|
|
table_lines.append(header_line)
|
|
|
|
# Add separator
|
|
separator = "-" * len(header_line)
|
|
table_lines.append(separator)
|
|
|
|
# Add data rows (limit based on content density)
|
|
max_rows = 5 # Default limit
|
|
for row in rows[:max_rows]:
|
|
row_line = " | ".join(str(cell) for cell in row)
|
|
table_lines.append(row_line)
|
|
|
|
if len(rows) > max_rows:
|
|
table_lines.append(f"... and {len(rows) - max_rows} more rows")
|
|
|
|
return "\n".join(table_lines)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error formatting table for slide: {str(e)}")
|
|
return ""
|
|
|
|
def _format_list_for_slide(self, list_data: Dict[str, Any]) -> str:
|
|
"""Format list data for slide presentation."""
|
|
try:
|
|
items = list_data.get("items", [])
|
|
|
|
if not items:
|
|
return ""
|
|
|
|
# Create list representation
|
|
list_lines = []
|
|
|
|
for item in items:
|
|
if isinstance(item, dict):
|
|
text = item.get("text", "")
|
|
list_lines.append(f"• {text}")
|
|
|
|
# Add subitems (limit to 3 for readability)
|
|
subitems = item.get("subitems", [])[:3]
|
|
for subitem in subitems:
|
|
if isinstance(subitem, dict):
|
|
list_lines.append(f" - {subitem.get('text', '')}")
|
|
else:
|
|
list_lines.append(f" - {subitem}")
|
|
else:
|
|
list_lines.append(f"• {str(item)}")
|
|
|
|
return "\n".join(list_lines)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error formatting list for slide: {str(e)}")
|
|
return ""
|
|
|
|
def _format_heading_for_slide(self, heading_data: Dict[str, Any]) -> str:
|
|
"""Format heading data for slide presentation."""
|
|
try:
|
|
text = heading_data.get("text", "")
|
|
level = heading_data.get("level", 1)
|
|
|
|
if text:
|
|
return f"{'#' * level} {text}"
|
|
|
|
return ""
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error formatting heading for slide: {str(e)}")
|
|
return ""
|
|
|
|
def _format_paragraph_for_slide(self, paragraph_data: Dict[str, Any]) -> str:
|
|
"""Format paragraph data for slide presentation."""
|
|
try:
|
|
text = paragraph_data.get("text", "")
|
|
|
|
if text:
|
|
# Limit paragraph length based on content density
|
|
max_length = 200 # Default limit
|
|
if len(text) > max_length:
|
|
text = text[:max_length] + "..."
|
|
|
|
return text
|
|
|
|
return ""
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error formatting paragraph for slide: {str(e)}")
|
|
return ""
|
|
|
|
def _format_code_for_slide(self, code_data: Dict[str, Any]) -> str:
|
|
"""Format code data for slide presentation."""
|
|
try:
|
|
code = code_data.get("code", "")
|
|
language = code_data.get("language", "")
|
|
|
|
if code:
|
|
# Limit code length based on content density
|
|
max_length = 100 # Default limit
|
|
if len(code) > max_length:
|
|
code = code[:max_length] + "..."
|
|
|
|
if language:
|
|
return f"Code ({language}):\n{code}"
|
|
else:
|
|
return f"Code:\n{code}"
|
|
|
|
return ""
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error formatting code for slide: {str(e)}")
|
|
return ""
|
|
|
|
def _get_slide_layout_index(self, slide_data: Dict[str, Any], styles: Dict[str, Any]) -> int:
|
|
"""Determine the best professional slide layout based on content."""
|
|
try:
|
|
content = slide_data.get("content", "")
|
|
title = slide_data.get("title", "")
|
|
|
|
# Check if it's a title slide (first slide)
|
|
if not content or "Generated by PowerOn AI System" in content:
|
|
return 0 # Title slide layout
|
|
|
|
# Professional layout selection based on content
|
|
if "|" in content and "-" in content:
|
|
# Has both tables and lists - use content with caption for professional look
|
|
return 2
|
|
elif "|" in content:
|
|
# Has tables - use content layout for clean table presentation
|
|
return 1
|
|
elif content.count("•") > 2:
|
|
# Has many bullet points - use content layout for better readability
|
|
return 1
|
|
elif len(content) > 200:
|
|
# Long content - use content layout for better text flow
|
|
return 1
|
|
elif title and len(title) > 20:
|
|
# Long title - use title and content layout
|
|
return 1
|
|
else:
|
|
# Default to title and content layout for professional appearance
|
|
return 1
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error determining slide layout: {str(e)}")
|
|
return 1 # Default to title and content layout
|
|
|
|
def _create_slides_from_sections(self, sections: List[Dict[str, Any]], styles: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""Create slides from sections based on content density and user intent."""
|
|
try:
|
|
slides = []
|
|
content_per_slide = styles.get("content_per_slide", "concise")
|
|
|
|
# Group sections by type and create slides
|
|
current_slide_content = []
|
|
current_slide_title = "Content Overview"
|
|
|
|
for section in sections:
|
|
section_type = section.get("content_type", "paragraph")
|
|
elements = section.get("elements", [])
|
|
|
|
if section_type == "heading":
|
|
# If we have accumulated content, create a slide
|
|
if current_slide_content:
|
|
slides.append({
|
|
"title": current_slide_title,
|
|
"content": "\n\n".join(current_slide_content)
|
|
})
|
|
current_slide_content = []
|
|
|
|
# Start new slide with heading as title
|
|
for element in elements:
|
|
if isinstance(element, dict) and "text" in element:
|
|
current_slide_title = element.get("text", "Untitled Section")
|
|
break
|
|
else:
|
|
# Add content to current slide
|
|
formatted_content = self._format_section_content(section)
|
|
if formatted_content:
|
|
current_slide_content.append(formatted_content)
|
|
|
|
# Add final slide if there's content
|
|
if current_slide_content:
|
|
slides.append({
|
|
"title": current_slide_title,
|
|
"content": "\n\n".join(current_slide_content)
|
|
})
|
|
|
|
return slides
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error creating slides from sections: {str(e)}")
|
|
return []
|
|
|
|
def _format_section_content(self, section: Dict[str, Any]) -> str:
|
|
"""Format section content for slide presentation."""
|
|
try:
|
|
content_type = section.get("content_type", "paragraph")
|
|
elements = section.get("elements", [])
|
|
|
|
# Process each element in the section
|
|
content_parts = []
|
|
for element in elements:
|
|
if content_type == "table":
|
|
content_parts.append(self._format_table_for_slide([element]))
|
|
elif content_type == "list":
|
|
content_parts.append(self._format_list_for_slide([element]))
|
|
elif content_type == "heading":
|
|
content_parts.append(self._format_heading_for_slide([element]))
|
|
elif content_type == "paragraph":
|
|
content_parts.append(self._format_paragraph_for_slide([element]))
|
|
elif content_type == "code":
|
|
content_parts.append(self._format_code_for_slide([element]))
|
|
else:
|
|
content_parts.append(self._format_paragraph_for_slide([element]))
|
|
|
|
return "\n\n".join(filter(None, content_parts))
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error formatting section content: {str(e)}")
|
|
return ""
|
|
|
|
def _format_timestamp(self) -> str:
|
|
"""Format current timestamp for presentation generation."""
|
|
from datetime import datetime, UTC
|
|
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
|