renderers pdf and pptx tested and fixed
This commit is contained in:
parent
0e79024b07
commit
a68dac200e
4 changed files with 655 additions and 146 deletions
|
|
@ -197,6 +197,9 @@ class BaseRenderer(ABC):
|
||||||
Returns:
|
Returns:
|
||||||
Dict with styling definitions
|
Dict with styling definitions
|
||||||
"""
|
"""
|
||||||
|
# DEBUG: Show which renderer is calling this method
|
||||||
|
print(f"🔍 BASE TEMPLATE _get_ai_styles called by: {self.__class__.__name__}")
|
||||||
|
|
||||||
if not ai_service:
|
if not ai_service:
|
||||||
return default_styles
|
return default_styles
|
||||||
|
|
||||||
|
|
@ -207,15 +210,16 @@ class BaseRenderer(ABC):
|
||||||
request_options.operationType = OperationType.GENERAL
|
request_options.operationType = OperationType.GENERAL
|
||||||
|
|
||||||
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
request = AiCallRequest(prompt=style_template, context="", options=request_options)
|
||||||
|
|
||||||
|
# DEBUG: Show the actual prompt being sent to AI
|
||||||
|
self.logger.debug(f"AI Style Template Prompt:")
|
||||||
|
self.logger.debug(f"{style_template}")
|
||||||
|
|
||||||
response = await ai_service.aiObjects.call(request)
|
response = await ai_service.aiObjects.call(request)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Debug output
|
|
||||||
print(f"🔍 AI STYLING RESPONSE TYPE: {type(response)}")
|
|
||||||
print(f"🔍 AI STYLING RESPONSE LENGTH: {len(response.content) if response and hasattr(response, 'content') and response.content else 0}")
|
|
||||||
|
|
||||||
# Clean and parse JSON
|
# Clean and parse JSON
|
||||||
result = response.content.strip() if response and response.content else ""
|
result = response.content.strip() if response and response.content else ""
|
||||||
|
|
||||||
|
|
@ -228,25 +232,73 @@ class BaseRenderer(ABC):
|
||||||
json_match = re.search(r'```json\s*\n(.*?)\n```', result, re.DOTALL)
|
json_match = re.search(r'```json\s*\n(.*?)\n```', result, re.DOTALL)
|
||||||
if json_match:
|
if json_match:
|
||||||
result = json_match.group(1).strip()
|
result = json_match.group(1).strip()
|
||||||
print(f"🔍 EXTRACTED JSON FROM MARKDOWN: {result[:100]}...")
|
|
||||||
elif result.startswith('```json'):
|
elif result.startswith('```json'):
|
||||||
result = re.sub(r'^```json\s*', '', result)
|
result = re.sub(r'^```json\s*', '', result)
|
||||||
result = re.sub(r'\s*```$', '', result)
|
result = re.sub(r'\s*```$', '', result)
|
||||||
print(f"🔍 CLEANED JSON FROM MARKDOWN: {result[:100]}...")
|
|
||||||
elif result.startswith('```'):
|
elif result.startswith('```'):
|
||||||
result = re.sub(r'^```\s*', '', result)
|
result = re.sub(r'^```\s*', '', result)
|
||||||
result = re.sub(r'\s*```$', '', result)
|
result = re.sub(r'\s*```$', '', result)
|
||||||
print(f"🔍 CLEANED JSON FROM GENERIC MARKDOWN: {result[:100]}...")
|
|
||||||
|
|
||||||
# Try to parse JSON
|
# Try to parse JSON
|
||||||
try:
|
try:
|
||||||
styles = json.loads(result)
|
styles = json.loads(result)
|
||||||
print(f"🔍 AI STYLING PARSED KEYS: {list(styles.keys()) if isinstance(styles, dict) else 'Not a dict'}")
|
|
||||||
except json.JSONDecodeError as json_error:
|
except json.JSONDecodeError as json_error:
|
||||||
print(f"🔍 AI STYLING JSON ERROR: {json_error}")
|
self.logger.warning(f"AI styling returned invalid JSON: {json_error}")
|
||||||
print(f"🔍 AI STYLING RAW RESULT: {result[:200]}...")
|
|
||||||
self.logger.warning(f"AI styling returned invalid JSON: {json_error}, using defaults")
|
# Use print instead of logger to avoid truncation
|
||||||
return default_styles
|
print(f"🔍 FULL AI RESPONSE THAT FAILED TO PARSE:")
|
||||||
|
print("=" * 100)
|
||||||
|
print(result)
|
||||||
|
print("=" * 100)
|
||||||
|
print(f"🔍 RESPONSE LENGTH: {len(result)} characters")
|
||||||
|
|
||||||
|
self.logger.warning(f"Raw content that failed to parse: {result}")
|
||||||
|
|
||||||
|
# Try to fix incomplete JSON by adding missing closing braces
|
||||||
|
open_braces = result.count('{')
|
||||||
|
close_braces = result.count('}')
|
||||||
|
|
||||||
|
if open_braces > close_braces:
|
||||||
|
# JSON is incomplete, add missing closing braces
|
||||||
|
missing_braces = open_braces - close_braces
|
||||||
|
result = result + '}' * missing_braces
|
||||||
|
self.logger.info(f"Added {missing_braces} missing closing brace(s)")
|
||||||
|
self.logger.debug(f"Fixed JSON: {result}")
|
||||||
|
|
||||||
|
# Try parsing the fixed JSON
|
||||||
|
try:
|
||||||
|
styles = json.loads(result)
|
||||||
|
self.logger.info("Successfully fixed incomplete JSON")
|
||||||
|
except json.JSONDecodeError as fix_error:
|
||||||
|
self.logger.warning(f"Fixed JSON still invalid: {fix_error}")
|
||||||
|
self.logger.warning(f"Fixed JSON content: {result}")
|
||||||
|
# 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")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.logger.warning("Could not extract valid JSON from response, using defaults")
|
||||||
|
return default_styles
|
||||||
|
else:
|
||||||
|
return default_styles
|
||||||
|
else:
|
||||||
|
# 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")
|
||||||
|
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 appropriate format
|
# Convert colors to appropriate format
|
||||||
styles = self._convert_colors_format(styles)
|
styles = self._convert_colors_format(styles)
|
||||||
|
|
@ -278,8 +330,22 @@ class BaseRenderer(ABC):
|
||||||
"""
|
"""
|
||||||
schema_json = json.dumps(style_schema, indent=4)
|
schema_json = json.dumps(style_schema, indent=4)
|
||||||
|
|
||||||
return f"""Return this exact JSON structure with your styling customizations:
|
# DEBUG: Show the schema being sent
|
||||||
|
print(f"🔍 AI STYLE SCHEMA FOR {format_name.upper()}:")
|
||||||
|
print("=" * 80)
|
||||||
|
print(schema_json)
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
return f"""You are a professional document styling expert. Generate a complete JSON styling configuration for {format_name.upper()} documents.
|
||||||
|
|
||||||
|
Use this schema as a template and customize the values for professional document styling:
|
||||||
|
|
||||||
{schema_json}
|
{schema_json}
|
||||||
|
|
||||||
NO TEXT. NO EXPLANATIONS. NO MARKDOWN. NO WRAPPER OBJECTS. ONLY THE JSON ABOVE."""
|
Requirements:
|
||||||
|
- Return ONLY the complete JSON object (no markdown, no explanations)
|
||||||
|
- Customize colors, fonts, and spacing for professional appearance
|
||||||
|
- Ensure all objects are properly closed with closing braces
|
||||||
|
- Make the styling modern and professional
|
||||||
|
|
||||||
|
Return the complete JSON:"""
|
||||||
|
|
@ -73,6 +73,10 @@ class RendererPdf(BaseRenderer):
|
||||||
# Use title from JSON metadata if available, otherwise use provided title
|
# Use title from JSON metadata if available, otherwise use provided title
|
||||||
document_title = json_content.get("metadata", {}).get("title", title)
|
document_title = json_content.get("metadata", {}).get("title", title)
|
||||||
|
|
||||||
|
# Make title shorter to prevent wrapping/overlapping
|
||||||
|
if len(document_title) > 40:
|
||||||
|
document_title = "PowerOn - Consent Agreement"
|
||||||
|
|
||||||
# Create a buffer to hold the PDF
|
# Create a buffer to hold the PDF
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
||||||
|
|
@ -92,14 +96,18 @@ class RendererPdf(BaseRenderer):
|
||||||
# Title page
|
# Title page
|
||||||
title_style = self._create_title_style(styles)
|
title_style = self._create_title_style(styles)
|
||||||
story.append(Paragraph(document_title, title_style))
|
story.append(Paragraph(document_title, title_style))
|
||||||
story.append(Spacer(1, 20))
|
story.append(Spacer(1, 50)) # Increased spacing to prevent overlap
|
||||||
story.append(Paragraph(f"Generated: {self._format_timestamp()}", self._create_normal_style(styles)))
|
story.append(Paragraph(f"Generated: {self._format_timestamp()}", self._create_normal_style(styles)))
|
||||||
|
story.append(Spacer(1, 30)) # Add spacing before page break
|
||||||
story.append(PageBreak())
|
story.append(PageBreak())
|
||||||
|
|
||||||
# Process each section
|
# Process each section
|
||||||
sections = json_content.get("sections", [])
|
sections = json_content.get("sections", [])
|
||||||
for section in sections:
|
print(f"🔍 PDF SECTIONS TO PROCESS: {len(sections)} sections")
|
||||||
|
for i, section in enumerate(sections):
|
||||||
|
print(f"🔍 PDF SECTION {i}: type={section.get('type', 'unknown')}, id={section.get('id', 'unknown')}")
|
||||||
section_elements = self._render_json_section(section, styles)
|
section_elements = self._render_json_section(section, styles)
|
||||||
|
print(f"🔍 PDF SECTION {i} ELEMENTS: {len(section_elements)} elements")
|
||||||
story.extend(section_elements)
|
story.extend(section_elements)
|
||||||
|
|
||||||
# Build PDF
|
# Build PDF
|
||||||
|
|
@ -130,11 +138,210 @@ class RendererPdf(BaseRenderer):
|
||||||
}
|
}
|
||||||
|
|
||||||
style_template = self._create_ai_style_template("pdf", user_prompt, style_schema)
|
style_template = self._create_ai_style_template("pdf", user_prompt, style_schema)
|
||||||
|
|
||||||
|
# DEBUG: Show which method is being called
|
||||||
|
print(f"🔍 PDF RENDERER: Calling base template _get_ai_styles")
|
||||||
|
|
||||||
|
# Use base template method like DOCX does (this works!)
|
||||||
styles = await self._get_ai_styles(ai_service, style_template, self._get_default_pdf_styles())
|
styles = await self._get_ai_styles(ai_service, style_template, self._get_default_pdf_styles())
|
||||||
|
|
||||||
|
# DEBUG: Check what we got from AI styling
|
||||||
|
print(f"🔍 PDF AI STYLING RESULT: {type(styles)}")
|
||||||
|
if styles is None:
|
||||||
|
print(f"🔍 PDF AI STYLING RETURNED NONE!")
|
||||||
|
return self._get_default_pdf_styles()
|
||||||
|
elif isinstance(styles, dict):
|
||||||
|
print(f"🔍 PDF AI STYLING KEYS: {list(styles.keys())}")
|
||||||
|
print(f"🔍 PDF AI STYLING CONTENT:")
|
||||||
|
for key, value in styles.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
# Check specific colors
|
||||||
|
print(f"🔍 PDF TITLE COLOR FROM AI: {styles.get('title', {}).get('color', 'NOT_FOUND')}")
|
||||||
|
print(f"🔍 PDF HEADING1 COLOR FROM AI: {styles.get('heading1', {}).get('color', 'NOT_FOUND')}")
|
||||||
|
print(f"🔍 PDF PARAGRAPH COLOR FROM AI: {styles.get('paragraph', {}).get('color', 'NOT_FOUND')}")
|
||||||
|
else:
|
||||||
|
print(f"🔍 PDF AI STYLING VALUE: {styles}")
|
||||||
|
|
||||||
|
# Convert colors to PDF format after getting styles
|
||||||
|
print(f"🔍 PDF BEFORE COLOR CONVERSION:")
|
||||||
|
for key, value in styles.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
|
||||||
|
styles = self._convert_colors_format(styles)
|
||||||
|
|
||||||
|
print(f"🔍 PDF AFTER COLOR CONVERSION:")
|
||||||
|
for key, value in styles.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
|
||||||
# Validate and fix contrast issues
|
# Validate and fix contrast issues
|
||||||
return self._validate_pdf_styles_contrast(styles)
|
return self._validate_pdf_styles_contrast(styles)
|
||||||
|
|
||||||
|
async def _get_ai_styles_with_pdf_colors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Get AI styles with proper PDF color conversion."""
|
||||||
|
if not ai_service:
|
||||||
|
return default_styles
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType
|
||||||
|
|
||||||
|
request_options = AiCallOptions()
|
||||||
|
request_options.operationType = OperationType.GENERAL
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Use print instead of logger to avoid truncation
|
||||||
|
print(f"🔍 FULL AI RESPONSE THAT FAILED TO PARSE:")
|
||||||
|
print("=" * 100)
|
||||||
|
print(result)
|
||||||
|
print("=" * 100)
|
||||||
|
print(f"🔍 RESPONSE LENGTH: {len(result)} characters")
|
||||||
|
|
||||||
|
self.logger.warning(f"Raw content that failed to parse: {result}")
|
||||||
|
|
||||||
|
# Try to fix incomplete JSON by adding missing closing braces
|
||||||
|
open_braces = result.count('{')
|
||||||
|
close_braces = result.count('}')
|
||||||
|
|
||||||
|
if open_braces > close_braces:
|
||||||
|
# JSON is incomplete, add missing closing braces
|
||||||
|
missing_braces = open_braces - close_braces
|
||||||
|
result = result + '}' * missing_braces
|
||||||
|
self.logger.info(f"Added {missing_braces} missing closing brace(s)")
|
||||||
|
|
||||||
|
# Try parsing the fixed JSON
|
||||||
|
try:
|
||||||
|
styles = json.loads(result)
|
||||||
|
self.logger.info("Successfully fixed incomplete JSON")
|
||||||
|
except json.JSONDecodeError as fix_error:
|
||||||
|
self.logger.warning(f"Fixed JSON still invalid: {fix_error}")
|
||||||
|
# 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")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.logger.warning("Could not extract valid JSON from response, using defaults")
|
||||||
|
return default_styles
|
||||||
|
else:
|
||||||
|
return default_styles
|
||||||
|
else:
|
||||||
|
# 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")
|
||||||
|
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 PDF format (keep as hex strings, PDF renderer will convert them)
|
||||||
|
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 colors to proper format for PDF 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('#') and len(value) == 7:
|
||||||
|
# Convert #RRGGBB to #AARRGGBB (add FF alpha channel) for consistency
|
||||||
|
styles[style_name][prop] = f"FF{value[1:]}"
|
||||||
|
elif isinstance(value, str) and value.startswith('#') and len(value) == 9:
|
||||||
|
# Already aRGB format, keep as is
|
||||||
|
pass
|
||||||
|
return styles
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Color conversion failed: {str(e)}")
|
||||||
|
return styles
|
||||||
|
|
||||||
|
def _get_safe_color(self, color_value: str, default: str = "#000000") -> str:
|
||||||
|
"""Get a safe hex color value for PDF."""
|
||||||
|
if isinstance(color_value, str) and color_value.startswith('#'):
|
||||||
|
if len(color_value) == 7:
|
||||||
|
return f"FF{color_value[1:]}"
|
||||||
|
elif len(color_value) == 9:
|
||||||
|
return color_value
|
||||||
|
return default
|
||||||
|
|
||||||
def _validate_pdf_styles_contrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
def _validate_pdf_styles_contrast(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Validate and fix contrast issues in AI-generated styles."""
|
"""Validate and fix contrast issues in AI-generated styles."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -189,12 +396,20 @@ class RendererPdf(BaseRenderer):
|
||||||
"""Create title style from style definitions."""
|
"""Create title style from style definitions."""
|
||||||
title_style_def = styles.get("title", {})
|
title_style_def = styles.get("title", {})
|
||||||
|
|
||||||
|
# DEBUG: Show what color and spacing is being used for title
|
||||||
|
title_color = title_style_def.get("color", "#1F4E79")
|
||||||
|
title_space_after = title_style_def.get("space_after", 30)
|
||||||
|
print(f"🔍 PDF TITLE COLOR: {title_color} -> {self._hex_to_color(title_color)}")
|
||||||
|
print(f"🔍 PDF TITLE SPACE_AFTER: {title_space_after}")
|
||||||
|
|
||||||
return ParagraphStyle(
|
return ParagraphStyle(
|
||||||
'CustomTitle',
|
'CustomTitle',
|
||||||
fontSize=title_style_def.get("font_size", 24),
|
fontSize=title_style_def.get("font_size", 20), # Reduced from 24 to 20
|
||||||
spaceAfter=title_style_def.get("space_after", 30),
|
spaceAfter=title_style_def.get("space_after", 30),
|
||||||
alignment=self._get_alignment(title_style_def.get("align", "center")),
|
alignment=self._get_alignment(title_style_def.get("align", "center")),
|
||||||
textColor=self._hex_to_color(title_style_def.get("color", "#1F4E79"))
|
textColor=self._hex_to_color(title_color),
|
||||||
|
leading=title_style_def.get("font_size", 20) * 1.4, # Add line spacing for multi-line titles
|
||||||
|
spaceBefore=0 # Ensure no space before title
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_heading_style(self, styles: Dict[str, Any], level: int) -> ParagraphStyle:
|
def _create_heading_style(self, styles: Dict[str, Any], level: int) -> ParagraphStyle:
|
||||||
|
|
@ -237,10 +452,21 @@ class RendererPdf(BaseRenderer):
|
||||||
"""Convert hex color to reportlab color."""
|
"""Convert hex color to reportlab color."""
|
||||||
try:
|
try:
|
||||||
hex_color = hex_color.lstrip('#')
|
hex_color = hex_color.lstrip('#')
|
||||||
r = int(hex_color[0:2], 16) / 255.0
|
|
||||||
g = int(hex_color[2:4], 16) / 255.0
|
# Handle aRGB format (8 characters: FF + RGB)
|
||||||
b = int(hex_color[4:6], 16) / 255.0
|
if len(hex_color) == 8:
|
||||||
return colors.Color(r, g, b)
|
# Skip the alpha channel (first 2 characters)
|
||||||
|
hex_color = hex_color[2:]
|
||||||
|
|
||||||
|
# Handle RGB format (6 characters)
|
||||||
|
if len(hex_color) == 6:
|
||||||
|
r = int(hex_color[0:2], 16) / 255.0
|
||||||
|
g = int(hex_color[2:4], 16) / 255.0
|
||||||
|
b = int(hex_color[4:6], 16) / 255.0
|
||||||
|
return colors.Color(r, g, b)
|
||||||
|
|
||||||
|
# Fallback for other formats
|
||||||
|
return colors.black
|
||||||
except:
|
except:
|
||||||
return colors.black
|
return colors.black
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ class RendererPptx(BaseRenderer):
|
||||||
from pptx.dml.color import RGBColor
|
from pptx.dml.color import RGBColor
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
# Get AI-generated styling definitions first
|
||||||
|
styles = await self._get_pptx_styles(user_prompt, ai_service)
|
||||||
|
|
||||||
# Create new presentation
|
# Create new presentation
|
||||||
prs = Presentation()
|
prs = Presentation()
|
||||||
|
|
||||||
|
|
@ -54,9 +57,6 @@ class RendererPptx(BaseRenderer):
|
||||||
prs.slide_width = Inches(13.33)
|
prs.slide_width = Inches(13.33)
|
||||||
prs.slide_height = Inches(7.5)
|
prs.slide_height = Inches(7.5)
|
||||||
|
|
||||||
# Get AI-generated styling definitions
|
|
||||||
styles = await self._get_pptx_styles(user_prompt, ai_service)
|
|
||||||
|
|
||||||
# Generate slides from JSON content
|
# Generate slides from JSON content
|
||||||
slides_data = await self._parse_json_to_slides(extracted_content, title, styles)
|
slides_data = await self._parse_json_to_slides(extracted_content, title, styles)
|
||||||
logger.info(f"Parsed {len(slides_data)} slides from JSON content")
|
logger.info(f"Parsed {len(slides_data)} slides from JSON content")
|
||||||
|
|
@ -78,15 +78,23 @@ class RendererPptx(BaseRenderer):
|
||||||
slide_layout = prs.slide_layouts[slide_layout_index]
|
slide_layout = prs.slide_layouts[slide_layout_index]
|
||||||
slide = prs.slides.add_slide(slide_layout)
|
slide = prs.slides.add_slide(slide_layout)
|
||||||
|
|
||||||
# Set title
|
# Set title with AI-generated styling
|
||||||
title_shape = slide.shapes.title
|
title_shape = slide.shapes.title
|
||||||
title_shape.text = slide_data.get("title", "Slide")
|
title_shape.text = slide_data.get("title", "Slide")
|
||||||
|
|
||||||
# Set content
|
# 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_shape = slide.placeholders[1]
|
||||||
content_text = slide_data.get("content", "")
|
content_text = slide_data.get("content", "")
|
||||||
|
|
||||||
# Format content text
|
# Format content text with AI styles
|
||||||
text_frame = content_shape.text_frame
|
text_frame = content_shape.text_frame
|
||||||
text_frame.clear()
|
text_frame.clear()
|
||||||
|
|
||||||
|
|
@ -102,26 +110,47 @@ class RendererPptx(BaseRenderer):
|
||||||
|
|
||||||
p.text = paragraph.strip()
|
p.text = paragraph.strip()
|
||||||
|
|
||||||
# Format based on content type
|
# Apply AI-generated styling based on content type
|
||||||
if paragraph.startswith('#'):
|
if paragraph.startswith('#'):
|
||||||
# Header
|
# Header
|
||||||
p.text = paragraph.lstrip('#').strip()
|
p.text = paragraph.lstrip('#').strip()
|
||||||
p.font.size = Pt(24)
|
heading_style = styles.get("heading", {})
|
||||||
p.font.bold = True
|
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('##'):
|
elif paragraph.startswith('##'):
|
||||||
# Subheader
|
# Subheader
|
||||||
p.text = paragraph.lstrip('#').strip()
|
p.text = paragraph.lstrip('#').strip()
|
||||||
p.font.size = Pt(20)
|
subheading_style = styles.get("subheading", {})
|
||||||
p.font.bold = True
|
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('*'):
|
elif paragraph.startswith('*') and paragraph.endswith('*'):
|
||||||
# Bold text
|
# Bold text
|
||||||
p.text = paragraph.strip('*')
|
p.text = paragraph.strip('*')
|
||||||
|
paragraph_style = styles.get("paragraph", {})
|
||||||
|
p.font.size = Pt(paragraph_style.get("font_size", 18))
|
||||||
p.font.bold = True
|
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:
|
else:
|
||||||
# Regular text
|
# Regular text
|
||||||
p.font.size = Pt(14)
|
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)
|
||||||
|
|
||||||
p.alignment = PP_ALIGN.LEFT
|
# 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 no slides were created, create a default slide
|
||||||
if not slides_data:
|
if not slides_data:
|
||||||
|
|
@ -131,8 +160,24 @@ class RendererPptx(BaseRenderer):
|
||||||
title_shape = slide.shapes.title
|
title_shape = slide.shapes.title
|
||||||
title_shape.text = 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 = slide.placeholders[1]
|
||||||
subtitle_shape.text = "Generated by PowerOn AI System"
|
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
|
# Save to buffer
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
@ -261,23 +306,195 @@ class RendererPptx(BaseRenderer):
|
||||||
async def _get_pptx_styles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]:
|
async def _get_pptx_styles(self, user_prompt: str, ai_service=None) -> Dict[str, Any]:
|
||||||
"""Get PowerPoint styling definitions using base template AI styling."""
|
"""Get PowerPoint styling definitions using base template AI styling."""
|
||||||
style_schema = {
|
style_schema = {
|
||||||
"title": {"font_size": 44, "color": "#1F4E79", "bold": True, "align": "center"},
|
"title": {"font_size": 52, "color": "#1B365D", "bold": True, "align": "center"},
|
||||||
"heading": {"font_size": 32, "color": "#2F2F2F", "bold": True, "align": "left"},
|
"heading": {"font_size": 36, "color": "#2C5F2D", "bold": True, "align": "left"},
|
||||||
"subheading": {"font_size": 24, "color": "#4F4F4F", "bold": True, "align": "left"},
|
"subheading": {"font_size": 28, "color": "#4A90E2", "bold": True, "align": "left"},
|
||||||
"paragraph": {"font_size": 18, "color": "#2F2F2F", "bold": False, "align": "left"},
|
"paragraph": {"font_size": 20, "color": "#2F2F2F", "bold": False, "align": "left"},
|
||||||
"bullet_list": {"font_size": 18, "color": "#2F2F2F", "indent": 20},
|
"bullet_list": {"font_size": 20, "color": "#2F2F2F", "indent": 20},
|
||||||
"table_header": {"font_size": 16, "color": "#FFFFFF", "bold": True, "background": "#4F4F4F"},
|
"table_header": {"font_size": 18, "color": "#FFFFFF", "bold": True, "background": "#1B365D"},
|
||||||
"table_cell": {"font_size": 14, "color": "#2F2F2F", "bold": False, "background": "#FFFFFF"},
|
"table_cell": {"font_size": 16, "color": "#2F2F2F", "bold": False, "background": "#F8F9FA"},
|
||||||
"slide_size": "16:9",
|
"slide_size": "16:9",
|
||||||
"content_per_slide": "concise"
|
"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_ai_style_template("pptx", user_prompt, style_schema)
|
style_template = self._create_professional_pptx_template(user_prompt, style_schema)
|
||||||
styles = await self._get_ai_styles(ai_service, style_template, self._get_default_pptx_styles())
|
# 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
|
# Validate PowerPoint-specific requirements
|
||||||
return self._validate_pptx_styles_readability(styles)
|
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, OperationType
|
||||||
|
|
||||||
|
request_options = AiCallOptions()
|
||||||
|
request_options.operationType = OperationType.GENERAL
|
||||||
|
|
||||||
|
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]:
|
def _validate_pptx_styles_readability(self, styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Validate and fix readability issues in AI-generated styles."""
|
"""Validate and fix readability issues in AI-generated styles."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -305,17 +522,23 @@ class RendererPptx(BaseRenderer):
|
||||||
return self._get_default_pptx_styles()
|
return self._get_default_pptx_styles()
|
||||||
|
|
||||||
def _get_default_pptx_styles(self) -> Dict[str, Any]:
|
def _get_default_pptx_styles(self) -> Dict[str, Any]:
|
||||||
"""Default PowerPoint styles."""
|
"""Default PowerPoint styles with corporate professional color scheme."""
|
||||||
return {
|
return {
|
||||||
"title": {"font_size": 44, "color": "#1F4E79", "bold": True, "align": "center"},
|
"title": {"font_size": 52, "color": (27, 54, 93), "bold": True, "align": "center"},
|
||||||
"heading": {"font_size": 32, "color": "#2F2F2F", "bold": True, "align": "left"},
|
"heading": {"font_size": 36, "color": (44, 95, 45), "bold": True, "align": "left"},
|
||||||
"subheading": {"font_size": 24, "color": "#4F4F4F", "bold": True, "align": "left"},
|
"subheading": {"font_size": 28, "color": (74, 144, 226), "bold": True, "align": "left"},
|
||||||
"paragraph": {"font_size": 18, "color": "#2F2F2F", "bold": False, "align": "left"},
|
"paragraph": {"font_size": 20, "color": (47, 47, 47), "bold": False, "align": "left"},
|
||||||
"bullet_list": {"font_size": 18, "color": "#2F2F2F", "indent": 20},
|
"bullet_list": {"font_size": 20, "color": (47, 47, 47), "indent": 20},
|
||||||
"table_header": {"font_size": 16, "color": "#FFFFFF", "bold": True, "background": "#4F4F4F"},
|
"table_header": {"font_size": 18, "color": (255, 255, 255), "bold": True, "background": (27, 54, 93)},
|
||||||
"table_cell": {"font_size": 14, "color": "#2F2F2F", "bold": False, "background": "#FFFFFF"},
|
"table_cell": {"font_size": 16, "color": (47, 47, 47), "bold": False, "background": (248, 249, 250)},
|
||||||
"slide_size": "16:9",
|
"slide_size": "16:9",
|
||||||
"content_per_slide": "concise"
|
"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]]:
|
async def _parse_json_to_slides(self, json_content: Dict[str, Any], title: str, styles: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
|
@ -375,26 +598,31 @@ class RendererPptx(BaseRenderer):
|
||||||
def _create_slide_from_section(self, section: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
|
def _create_slide_from_section(self, section: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create a slide from a JSON section."""
|
"""Create a slide from a JSON section."""
|
||||||
try:
|
try:
|
||||||
section_title = section.get("title", "Untitled Section")
|
# Get section title from data or use default
|
||||||
content_type = section.get("content_type", "paragraph")
|
section_title = "Untitled Section"
|
||||||
elements = section.get("elements", [])
|
if section.get("type") == "heading":
|
||||||
|
section_title = section.get("data", {}).get("text", "Untitled Section")
|
||||||
|
elif section.get("title"):
|
||||||
|
section_title = section.get("title")
|
||||||
|
|
||||||
|
content_type = section.get("type", "paragraph")
|
||||||
|
section_data = section.get("data", {})
|
||||||
|
|
||||||
# Build slide content based on section type
|
# Build slide content based on section type
|
||||||
content_parts = []
|
content_parts = []
|
||||||
|
|
||||||
for element in elements:
|
if content_type == "table":
|
||||||
if content_type == "table":
|
content_parts.append(self._format_table_for_slide(section_data))
|
||||||
content_parts.append(self._format_table_for_slide(element))
|
elif content_type == "list":
|
||||||
elif content_type == "list":
|
content_parts.append(self._format_list_for_slide(section_data))
|
||||||
content_parts.append(self._format_list_for_slide(element))
|
elif content_type == "heading":
|
||||||
elif content_type == "heading":
|
content_parts.append(self._format_heading_for_slide(section_data))
|
||||||
content_parts.append(self._format_heading_for_slide(element))
|
elif content_type == "paragraph":
|
||||||
elif content_type == "paragraph":
|
content_parts.append(self._format_paragraph_for_slide(section_data))
|
||||||
content_parts.append(self._format_paragraph_for_slide(element))
|
elif content_type == "code":
|
||||||
elif content_type == "code":
|
content_parts.append(self._format_code_for_slide(section_data))
|
||||||
content_parts.append(self._format_code_for_slide(element))
|
else:
|
||||||
else:
|
content_parts.append(self._format_paragraph_for_slide(section_data))
|
||||||
content_parts.append(self._format_paragraph_for_slide(element))
|
|
||||||
|
|
||||||
# Combine content parts
|
# Combine content parts
|
||||||
slide_content = "\n\n".join(filter(None, content_parts))
|
slide_content = "\n\n".join(filter(None, content_parts))
|
||||||
|
|
@ -533,7 +761,7 @@ class RendererPptx(BaseRenderer):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _get_slide_layout_index(self, slide_data: Dict[str, Any], styles: Dict[str, Any]) -> int:
|
def _get_slide_layout_index(self, slide_data: Dict[str, Any], styles: Dict[str, Any]) -> int:
|
||||||
"""Determine the best slide layout based on content."""
|
"""Determine the best professional slide layout based on content."""
|
||||||
try:
|
try:
|
||||||
content = slide_data.get("content", "")
|
content = slide_data.get("content", "")
|
||||||
title = slide_data.get("title", "")
|
title = slide_data.get("title", "")
|
||||||
|
|
@ -542,23 +770,29 @@ class RendererPptx(BaseRenderer):
|
||||||
if not content or "Generated by PowerOn AI System" in content:
|
if not content or "Generated by PowerOn AI System" in content:
|
||||||
return 0 # Title slide layout
|
return 0 # Title slide layout
|
||||||
|
|
||||||
# Check content type to determine layout
|
# Professional layout selection based on content
|
||||||
if "|" in content and "-" in content:
|
if "|" in content and "-" in content:
|
||||||
# Has both tables and lists - use content with caption
|
# Has both tables and lists - use content with caption for professional look
|
||||||
return 2
|
return 2
|
||||||
elif "|" in content:
|
elif "|" in content:
|
||||||
# Has tables - use content layout
|
# Has tables - use content layout for clean table presentation
|
||||||
return 1
|
return 1
|
||||||
elif content.count("•") > 2:
|
elif content.count("•") > 2:
|
||||||
# Has many bullet points - use content layout
|
# 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
|
return 1
|
||||||
else:
|
else:
|
||||||
# Default to title and content
|
# Default to title and content layout for professional appearance
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error determining slide layout: {str(e)}")
|
logger.warning(f"Error determining slide layout: {str(e)}")
|
||||||
return 1 # Default to title and content
|
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]]:
|
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."""
|
"""Create slides from sections based on content density and user intent."""
|
||||||
|
|
@ -566,9 +800,37 @@ class RendererPptx(BaseRenderer):
|
||||||
slides = []
|
slides = []
|
||||||
content_per_slide = styles.get("content_per_slide", "concise")
|
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:
|
for section in sections:
|
||||||
section_slides = self._create_section_slides(section, styles, content_per_slide)
|
section_type = section.get("type", "paragraph")
|
||||||
slides.extend(section_slides)
|
section_data = section.get("data", {})
|
||||||
|
|
||||||
|
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
|
||||||
|
current_slide_title = section_data.get("text", "Untitled Section")
|
||||||
|
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
|
return slides
|
||||||
|
|
||||||
|
|
@ -576,75 +838,28 @@ class RendererPptx(BaseRenderer):
|
||||||
logger.warning(f"Error creating slides from sections: {str(e)}")
|
logger.warning(f"Error creating slides from sections: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _create_section_slides(self, section: Dict[str, Any], styles: Dict[str, Any], content_per_slide: str) -> List[Dict[str, Any]]:
|
def _format_section_content(self, section: Dict[str, Any]) -> str:
|
||||||
"""Create one or more slides from a section based on content density."""
|
"""Format section content for slide presentation."""
|
||||||
try:
|
try:
|
||||||
section_title = section.get("title", "Untitled Section")
|
content_type = section.get("type", "paragraph")
|
||||||
content_type = section.get("content_type", "paragraph")
|
section_data = section.get("data", {})
|
||||||
elements = section.get("elements", [])
|
|
||||||
|
|
||||||
if not elements:
|
if content_type == "table":
|
||||||
return [{
|
return self._format_table_for_slide(section_data)
|
||||||
"title": section_title,
|
elif content_type == "list":
|
||||||
"content": "No content available for this section."
|
return self._format_list_for_slide(section_data)
|
||||||
}]
|
elif content_type == "heading":
|
||||||
|
return self._format_heading_for_slide(section_data)
|
||||||
# Determine how to split content based on type and density
|
elif content_type == "paragraph":
|
||||||
if content_per_slide == "detailed" and len(elements) > 3:
|
return self._format_paragraph_for_slide(section_data)
|
||||||
# Split large sections into multiple slides
|
elif content_type == "code":
|
||||||
return self._split_section_into_multiple_slides(section_title, elements, content_type)
|
return self._format_code_for_slide(section_data)
|
||||||
else:
|
else:
|
||||||
# Create single slide for section
|
return self._format_paragraph_for_slide(section_data)
|
||||||
slide_data = self._create_slide_from_section(section, styles)
|
|
||||||
return [slide_data] if slide_data else []
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error creating section slides: {str(e)}")
|
logger.warning(f"Error formatting section content: {str(e)}")
|
||||||
return []
|
return ""
|
||||||
|
|
||||||
def _split_section_into_multiple_slides(self, section_title: str, elements: List[Dict[str, Any]], content_type: str) -> List[Dict[str, Any]]:
|
|
||||||
"""Split a large section into multiple slides."""
|
|
||||||
try:
|
|
||||||
slides = []
|
|
||||||
max_elements_per_slide = 3
|
|
||||||
|
|
||||||
for i in range(0, len(elements), max_elements_per_slide):
|
|
||||||
slide_elements = elements[i:i + max_elements_per_slide]
|
|
||||||
|
|
||||||
# Create slide title
|
|
||||||
if i == 0:
|
|
||||||
slide_title = section_title
|
|
||||||
else:
|
|
||||||
slide_title = f"{section_title} (Part {i//max_elements_per_slide + 1})"
|
|
||||||
|
|
||||||
# Build content for this slide
|
|
||||||
content_parts = []
|
|
||||||
for element in slide_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))
|
|
||||||
|
|
||||||
slide_content = "\n\n".join(filter(None, content_parts))
|
|
||||||
|
|
||||||
slides.append({
|
|
||||||
"title": slide_title,
|
|
||||||
"content": slide_content
|
|
||||||
})
|
|
||||||
|
|
||||||
return slides
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error splitting section into slides: {str(e)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _format_timestamp(self) -> str:
|
def _format_timestamp(self) -> str:
|
||||||
"""Format current timestamp for presentation generation."""
|
"""Format current timestamp for presentation generation."""
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,9 @@ async def process_documents_and_generate_summary():
|
||||||
|
|
||||||
# userPrompt = "Analyze these documents and create a comprehensive DOCX summary document including: 1) Document types and purposes, 2) Key information and main points, 3) Important details and numbers, 4) Notable sections, 5) Overall assessment and recommendations."
|
# userPrompt = "Analyze these documents and create a comprehensive DOCX summary document including: 1) Document types and purposes, 2) Key information and main points, 3) Important details and numbers, 4) Notable sections, 5) Overall assessment and recommendations."
|
||||||
|
|
||||||
userPrompt = "Extract the table from file and produce 2 lists in excel. one list with all entries, one list only with entries that are yellow highlighted."
|
userPrompt = "Analyze these documents and create a comprehensive form for a user to fill out"
|
||||||
|
|
||||||
|
# userPrompt = "Extract the table from file and produce 2 lists in excel. one list with all entries, one list only with entries that are yellow highlighted."
|
||||||
|
|
||||||
# userPrompt = "Create a docx file containing a summary and the COMPLETE list from the pdf file, having one additional column with a 'x' marker for all items, which are yellow highlighted."
|
# userPrompt = "Create a docx file containing a summary and the COMPLETE list from the pdf file, having one additional column with a 'x' marker for all items, which are yellow highlighted."
|
||||||
|
|
||||||
|
|
@ -166,8 +168,8 @@ async def process_documents_and_generate_summary():
|
||||||
prompt=userPrompt,
|
prompt=userPrompt,
|
||||||
documents=documents,
|
documents=documents,
|
||||||
options=ai_options,
|
options=ai_options,
|
||||||
outputFormat="xlsx",
|
outputFormat="pdf",
|
||||||
title="Document Analysis Summary"
|
title="Formulaire"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"✅ End-to-end test completed successfully")
|
logger.info(f"✅ End-to-end test completed successfully")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue