From bcbaf41f4fd4b9128b18ee7edc9886e892cd6470 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 29 Dec 2025 02:09:33 +0100
Subject: [PATCH] fixed generation to renderer
---
.../serviceAi/subStructureGeneration.py | 17 ++
.../renderers/rendererPptx.py | 81 +++++--
.../renderers/rendererXlsx.py | 198 +++++++++++++-----
3 files changed, 222 insertions(+), 74 deletions(-)
diff --git a/modules/services/serviceAi/subStructureGeneration.py b/modules/services/serviceAi/subStructureGeneration.py
index a11fba62..84e659a4 100644
--- a/modules/services/serviceAi/subStructureGeneration.py
+++ b/modules/services/serviceAi/subStructureGeneration.py
@@ -160,6 +160,13 @@ IMPORTANT - CHAPTER INDEPENDENCE:
- One chapter does NOT have information about another chapter
- Each chapter must provide its own context and be understandable alone
+CRITICAL - CHAPTERS WITHOUT CONTENT PARTS:
+- If contentPartIds is EMPTY, generationHint MUST be VERY DETAILED with all context needed to generate content from scratch
+- Include: what to generate, what information to include, purpose, specific details
+- Without content parts, AI relies ENTIRELY on generationHint
+- GOOD: "Create [specific content] with [details]. Include [information]. Purpose: [explanation]."
+- BAD: "Create title" or "Add section" (too vague)
+
For each chapter:
- chapter id
- level (1, 2, 3, etc.)
@@ -171,6 +178,7 @@ For each chapter:
}}
}}
- generationHint: Description of the content (must be self-contained with all necessary context)
+ * If contentPartIds is EMPTY, generationHint MUST be VERY DETAILED with all context needed to generate content from scratch
OUTPUT FORMAT: {outputFormat}
@@ -197,6 +205,15 @@ RETURN JSON:
}},
"generationHint": "Create introduction section",
"sections": []
+ }},
+ {{
+ "id": "chapter_2",
+ "level": 1,
+ "title": "Main Title",
+ "contentPartIds": [],
+ "contentPartInstructions": {{}},
+ "generationHint": "Create [specific content description] with [formatting details]. Include [required information]. Purpose: [explanation of what this chapter provides].",
+ "sections": []
}}
]
}}]
diff --git a/modules/services/serviceGeneration/renderers/rendererPptx.py b/modules/services/serviceGeneration/renderers/rendererPptx.py
index 2fc93892..9e6f41c9 100644
--- a/modules/services/serviceGeneration/renderers/rendererPptx.py
+++ b/modules/services/serviceGeneration/renderers/rendererPptx.py
@@ -78,12 +78,24 @@ class RendererPptx(BaseRenderer):
slide_images = list(slide_data.get("images", [])) # Make copy so we can append
slide_content = slide_data.get('content', '')
hasSections = slide_sections and len(slide_sections) > 0
+ hasImages = len(slide_images) > 0
logger.info(f"Slide {i+1}: '{slide_data.get('title', 'No title')}' - sections: {len(slide_sections)}, images: {len(slide_images)}, content: {len(slide_content)} chars")
- # Determine layout: first slide (i==0) uses title slide layout, others use title+content
+ # Determine layout: first slide (i==0) uses title slide layout
+ # For image-only slides, use blank layout to avoid placeholder interference
+ # Otherwise use title+content layout
if i == 0:
slideLayoutIndex = 0 # Title slide layout
+ elif hasImages and not hasSections and not slide_content:
+ # Image-only slide: use blank layout (typically index 6, fallback to 5 if not available)
+ try:
+ slideLayoutIndex = 6 # Blank layout
+ # Verify layout exists, fallback if not
+ if slideLayoutIndex >= len(prs.slide_layouts):
+ slideLayoutIndex = 5 # Alternative blank layout
+ except (AttributeError, IndexError):
+ slideLayoutIndex = 1 # Fallback to title+content
else:
slideLayoutIndex = 1 # Title and content layout
@@ -91,18 +103,32 @@ class RendererPptx(BaseRenderer):
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 - LEFT ALIGNED by default
- 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)
+ # For blank layouts, add title as textbox since there's no title placeholder
+ try:
+ title_shape = slide.shapes.title
+ title_shape.text = slide_data.get("title", "Slide")
+
+ # Apply title styling - LEFT ALIGNED by default
+ 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._getSafeColor(title_style.get("color", (31, 78, 121)))
+ title_shape.text_frame.paragraphs[0].font.color.rgb = RGBColor(*title_color)
+ # Set left alignment for title
+ title_shape.text_frame.paragraphs[0].alignment = PP_ALIGN.LEFT
+ except AttributeError:
+ # Blank layout has no title placeholder - add title as textbox
+ from pptx.util import Inches
+ titleBox = slide.shapes.add_textbox(Inches(0.5), Inches(0.3), prs.slide_width - Inches(1), Inches(0.8))
+ titleFrame = titleBox.text_frame
+ titleFrame.text = slide_data.get("title", "Slide")
+ title_style = styles.get("title", {})
+ titleFrame.paragraphs[0].font.size = Pt(title_style.get("font_size", 44))
+ titleFrame.paragraphs[0].font.bold = title_style.get("bold", True)
title_color = self._getSafeColor(title_style.get("color", (31, 78, 121)))
- title_shape.text_frame.paragraphs[0].font.color.rgb = RGBColor(*title_color)
- # Set left alignment for title
- title_shape.text_frame.paragraphs[0].alignment = PP_ALIGN.LEFT
+ titleFrame.paragraphs[0].font.color.rgb = RGBColor(*title_color)
+ titleFrame.paragraphs[0].alignment = PP_ALIGN.LEFT
# Render sections with proper PowerPoint objects (tables, lists, etc.)
if hasSections:
@@ -229,6 +255,7 @@ class RendererPptx(BaseRenderer):
self._addParagraphToSlide(slide, element, styles, text_frame)
# Handle images after processing sections (images may have been extracted from sections)
+ # Update hasImages in case images were added during section processing
hasImages = len(slide_images) > 0
if hasImages:
self._addImagesToSlide(slide, slide_images, styles)
@@ -1138,9 +1165,20 @@ JSON ONLY. NO OTHER TEXT."""
# Single image: center it
img = images[0]
base64Data = img.get("base64Data")
- if base64Data:
- imageBytes = base64.b64decode(base64Data)
- imageStream = io.BytesIO(imageBytes)
+ # Validate base64Data is present and not empty
+ if base64Data and isinstance(base64Data, str) and len(base64Data.strip()) > 0:
+ try:
+ imageBytes = base64.b64decode(base64Data)
+ if len(imageBytes) == 0:
+ logger.error("Decoded image bytes are empty")
+ return
+ imageStream = io.BytesIO(imageBytes)
+ except Exception as decode_error:
+ logger.error(f"Failed to decode base64 image data: {str(decode_error)}")
+ return
+ else:
+ logger.error(f"Invalid base64Data: present={bool(base64Data)}, type={type(base64Data)}, length={len(base64Data) if base64Data else 0}")
+ return
# Get image dimensions
try:
@@ -1175,7 +1213,16 @@ JSON ONLY. NO OTHER TEXT."""
imageStream.seek(0)
# Add image to slide
- slide.shapes.add_picture(imageStream, left, top, width=finalWidth, height=finalHeight)
+ try:
+ slide.shapes.add_picture(imageStream, left, top, width=finalWidth, height=finalHeight)
+ except Exception as add_error:
+ # If add_picture fails, try with explicit format
+ imageStream.seek(0)
+ # Ensure we have valid image data
+ if len(imageBytes) > 0:
+ slide.shapes.add_picture(imageStream, left, top, width=finalWidth, height=finalHeight)
+ else:
+ raise Exception(f"Empty image data: {add_error}")
# Add caption if available
caption = img.get("caption") or img.get("altText")
@@ -1217,6 +1264,8 @@ JSON ONLY. NO OTHER TEXT."""
except Exception as e:
logger.error(f"Error embedding images in PPTX slide: {str(e)}")
+ import traceback
+ logger.error(f"Traceback: {traceback.format_exc()}")
def _addTableToSlide(self, slide, element: Dict[str, Any], styles: Dict[str, Any], top: float) -> None:
"""Add a PowerPoint table to slide."""
diff --git a/modules/services/serviceGeneration/renderers/rendererXlsx.py b/modules/services/serviceGeneration/renderers/rendererXlsx.py
index 1051e7bf..c1992f94 100644
--- a/modules/services/serviceGeneration/renderers/rendererXlsx.py
+++ b/modules/services/serviceGeneration/renderers/rendererXlsx.py
@@ -113,14 +113,37 @@ class RendererXlsx(BaseRenderer):
analysisSheet = wb.create_sheet("Analysis", 2)
# Add content to sheets
- self._populateSummarySheet(summarySheet, title)
+ self._populateSummarySheet(summarySheet, title, wb)
self._populateDataSheet(dataSheet, content)
self._populateAnalysisSheet(analysisSheet, content)
- # Save to buffer
+ # Ensure workbook has at least one sheet (Excel requirement)
+ if len(wb.worksheets) == 0:
+ wb.create_sheet("Sheet1")
+
+ # Save to buffer with error handling
buffer = io.BytesIO()
- wb.save(buffer)
- buffer.seek(0)
+ try:
+ wb.save(buffer)
+ buffer.seek(0)
+ except Exception as save_error:
+ self.logger.error(f"Error saving Excel workbook: {str(save_error)}")
+ # Try to fix common issues and retry
+ try:
+ # Remove any invalid sheet names or empty sheets
+ for sheet in list(wb.worksheets):
+ if not sheet.title or len(sheet.title.strip()) == 0:
+ wb.remove(sheet)
+ # Ensure at least one sheet exists
+ if len(wb.worksheets) == 0:
+ wb.create_sheet("Sheet1")
+ # Retry save
+ buffer = io.BytesIO()
+ wb.save(buffer)
+ buffer.seek(0)
+ except Exception as retry_error:
+ self.logger.error(f"Retry save also failed: {str(retry_error)}")
+ raise Exception(f"Failed to save Excel workbook: {str(save_error)}")
# Convert to base64
excelBytes = buffer.getvalue()
@@ -132,7 +155,7 @@ class RendererXlsx(BaseRenderer):
self.logger.error(f"Error generating Excel: {str(e)}")
raise
- def _populateSummarySheet(self, sheet, title: str):
+ def _populateSummarySheet(self, sheet, title: str, wb: Workbook = None):
"""Populate the summary sheet."""
try:
# Title
@@ -150,7 +173,11 @@ class RendererXlsx(BaseRenderer):
sheet['A6'] = "Key Metrics:"
sheet['A6'].font = Font(bold=True)
sheet['A7'] = "Total Items:"
- sheet['B7'] = "=COUNTA(Data!A:A)-1" # Count non-empty cells in Data sheet
+ # Only add formula if Data sheet exists (check workbook sheets)
+ if wb and "Data" in [s.title for s in wb.worksheets]:
+ sheet['B7'] = "=COUNTA(Data!A:A)-1" # Count non-empty cells in Data sheet
+ else:
+ sheet['B7'] = "N/A" # Data sheet not available
# Auto-adjust column widths
sheet.column_dimensions['A'].width = 20
@@ -167,7 +194,7 @@ class RendererXlsx(BaseRenderer):
for col, header in enumerate(headers, 1):
cell = sheet.cell(row=1, column=col, value=header)
cell.font = Font(bold=True)
- cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid")
+ cell.fill = PatternFill(start_color="FFCCCCCC", end_color="FFCCCCCC", fill_type="solid")
# Process content
lines = content.split('\n')
@@ -271,10 +298,33 @@ class RendererXlsx(BaseRenderer):
# Populate sheets with content
self._populateExcelSheets(sheets, jsonContent, styles)
- # Save to buffer
+ # Ensure workbook has at least one sheet (Excel requirement)
+ if len(wb.worksheets) == 0:
+ wb.create_sheet("Sheet1")
+
+ # Save to buffer with error handling
buffer = io.BytesIO()
- wb.save(buffer)
- buffer.seek(0)
+ try:
+ wb.save(buffer)
+ buffer.seek(0)
+ except Exception as save_error:
+ self.logger.error(f"Error saving Excel workbook: {str(save_error)}")
+ # Try to fix common issues and retry
+ try:
+ # Remove any invalid sheet names or empty sheets
+ for sheet in list(wb.worksheets):
+ if not sheet.title or len(sheet.title.strip()) == 0:
+ wb.remove(sheet)
+ # Ensure at least one sheet exists
+ if len(wb.worksheets) == 0:
+ wb.create_sheet("Sheet1")
+ # Retry save
+ buffer = io.BytesIO()
+ wb.save(buffer)
+ buffer.seek(0)
+ except Exception as retry_error:
+ self.logger.error(f"Retry save also failed: {str(retry_error)}")
+ raise Exception(f"Failed to save Excel workbook: {str(save_error)}")
# Convert to base64
excelBytes = buffer.getvalue()
@@ -348,30 +398,46 @@ class RendererXlsx(BaseRenderer):
# Fix table header contrast
if "table_header" in styles:
header = styles["table_header"]
- bgColor = header.get("background", "#FFFFFF")
- textColor = header.get("text_color", "#000000")
+ bgColor = header.get("background", "FFFFFFFF")
+ textColor = header.get("text_color", "FF000000")
+
+ # Normalize colors (remove # if present, ensure aRGB format)
+ bgColor = self._normalizeColor(bgColor)
+ textColor = self._normalizeColor(textColor)
# If both are white or both are dark, fix it
- if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
- header["background"] = "#FF4F4F4F"
- header["text_color"] = "#FFFFFFFF"
- elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
- header["background"] = "#FF4F4F4F"
- header["text_color"] = "#FFFFFFFF"
+ if bgColor.upper() == "FFFFFFFF" and textColor.upper() == "FFFFFFFF":
+ header["background"] = "FF4F4F4F"
+ header["text_color"] = "FFFFFFFF"
+ elif bgColor.upper() == "FF000000" and textColor.upper() == "FF000000":
+ header["background"] = "FF4F4F4F"
+ header["text_color"] = "FFFFFFFF"
+ else:
+ # Ensure colors are in correct format
+ header["background"] = bgColor
+ header["text_color"] = textColor
# Fix table cell contrast
if "table_cell" in styles:
cell = styles["table_cell"]
- bgColor = cell.get("background", "#FFFFFF")
- textColor = cell.get("text_color", "#000000")
+ bgColor = cell.get("background", "FFFFFFFF")
+ textColor = cell.get("text_color", "FF000000")
+
+ # Normalize colors (remove # if present, ensure aRGB format)
+ bgColor = self._normalizeColor(bgColor)
+ textColor = self._normalizeColor(textColor)
# If both are white or both are dark, fix it
- if bgColor.upper() == "#FFFFFF" and textColor.upper() == "#FFFFFF":
- cell["background"] = "#FFFFFFFF"
- cell["text_color"] = "#FF2F2F2F"
- elif bgColor.upper() == "#000000" and textColor.upper() == "#000000":
- cell["background"] = "#FFFFFFFF"
- cell["text_color"] = "#FF2F2F2F"
+ if bgColor.upper() == "FFFFFFFF" and textColor.upper() == "FFFFFFFF":
+ cell["background"] = "FFFFFFFF"
+ cell["text_color"] = "FF2F2F2F"
+ elif bgColor.upper() == "FF000000" and textColor.upper() == "FF000000":
+ cell["background"] = "FFFFFFFF"
+ cell["text_color"] = "FF2F2F2F"
+ else:
+ # Ensure colors are in correct format
+ cell["background"] = bgColor
+ cell["text_color"] = textColor
return styles
@@ -379,16 +445,39 @@ class RendererXlsx(BaseRenderer):
self.logger.warning(f"Style validation failed: {str(e)}")
return self._getDefaultStyleSet()
+ def _normalizeColor(self, colorValue: str) -> str:
+ """Normalize color to aRGB format without # prefix."""
+ if not isinstance(colorValue, str):
+ return "FF000000"
+
+ # Remove # prefix if present
+ if colorValue.startswith('#'):
+ colorValue = colorValue[1:]
+
+ # Convert to uppercase for consistency
+ colorValue = colorValue.upper()
+
+ # Ensure aRGB format (8 characters)
+ if len(colorValue) == 6:
+ # Convert RRGGBB to AARRGGBB (add FF alpha channel)
+ return f"FF{colorValue}"
+ elif len(colorValue) == 8:
+ # Already aRGB format
+ return colorValue
+ else:
+ # Unexpected format, return default black
+ return "FF000000"
+
def _getDefaultStyleSet(self) -> Dict[str, Any]:
"""Default Excel style set - used when no style instructions present."""
return {
- "title": {"font_size": 16, "color": "#FF1F4E79", "bold": True, "align": "left"},
- "heading": {"font_size": 14, "color": "#FF2F2F2F", "bold": True, "align": "left"},
- "table_header": {"background": "#FF4F4F4F", "text_color": "#FFFFFFFF", "bold": True, "align": "center"},
- "table_cell": {"background": "#FFFFFFFF", "text_color": "#FF2F2F2F", "bold": False, "align": "left"},
- "bullet_list": {"font_size": 11, "color": "#FF2F2F2F", "indent": 2},
- "paragraph": {"font_size": 11, "color": "#FF2F2F2F", "bold": False, "align": "left"},
- "code_block": {"font": "Courier New", "font_size": 10, "color": "#FF2F2F2F", "background": "#FFF5F5F5"}
+ "title": {"font_size": 16, "color": "FF1F4E79", "bold": True, "align": "left"},
+ "heading": {"font_size": 14, "color": "FF2F2F2F", "bold": True, "align": "left"},
+ "table_header": {"background": "FF4F4F4F", "text_color": "FFFFFFFF", "bold": True, "align": "center"},
+ "table_cell": {"background": "FFFFFFFF", "text_color": "FF2F2F2F", "bold": False, "align": "left"},
+ "bullet_list": {"font_size": 11, "color": "FF2F2F2F", "indent": 2},
+ "paragraph": {"font_size": 11, "color": "FF2F2F2F", "bold": False, "align": "left"},
+ "code_block": {"font": "Courier New", "font_size": 10, "color": "FF2F2F2F", "background": "FFF5F5F5"}
}
async def _getAiStylesWithExcelColors(self, aiService, styleTemplate: str, defaultStyles: Dict[str, Any]) -> Dict[str, Any]:
@@ -450,37 +539,26 @@ class RendererXlsx(BaseRenderer):
"""Get a safe aRGB color value for Excel (without # prefix)."""
if not isinstance(colorValue, str):
return default
-
- # Remove # prefix if present
- if colorValue.startswith('#'):
- colorValue = colorValue[1:]
-
- if len(colorValue) == 6:
- # Convert RRGGBB to AARRGGBB
- return f"FF{colorValue}"
- elif len(colorValue) == 8:
- # Already aRGB format
- return colorValue
- else:
- # Unexpected format, return default
+ # Use the normalize function for consistency
+ try:
+ normalized = self._normalizeColor(colorValue)
+ return normalized
+ except Exception:
return default
def _convertColorsFormat(self, styles: Dict[str, Any]) -> Dict[str, Any]:
- """Convert hex colors to aRGB format for Excel compatibility."""
+ """Convert hex colors to aRGB format for Excel compatibility (without # prefix)."""
try:
self.services.utils.debugLogToFile(f"CONVERTING COLORS IN STYLES: {styles}", "EXCEL_RENDERER")
for styleName, styleConfig in styles.items():
if isinstance(styleConfig, dict):
for prop, value in styleConfig.items():
- if isinstance(value, str) and value.startswith('#') and len(value) == 7:
- # Convert #RRGGBB to #AARRGGBB (add FF alpha channel)
- styles[styleName][prop] = f"FF{value[1:]}"
- elif isinstance(value, str) and value.startswith('#') and len(value) == 9:
- pass # Already aRGB format
- elif isinstance(value, str) and value.startswith('#'):
- pass # Unexpected format, keep as is
+ if isinstance(value, str):
+ # Normalize color to aRGB format without # prefix
+ styles[styleName][prop] = self._normalizeColor(value)
return styles
except Exception as e:
+ self.logger.warning(f"Color conversion failed: {str(e)}")
return styles
def _createExcelSheets(self, wb: Workbook, jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> Dict[str, Any]:
@@ -835,13 +913,13 @@ class RendererXlsx(BaseRenderer):
self.logger.warning(f"Could not add section to sheet: {str(e)}")
return startRow + 1
- def _sanitizeCellValue(self, value: Any) -> str:
- """Sanitize cell value: remove markdown, convert to string, handle None."""
+ def _sanitizeCellValue(self, value: Any) -> Any:
+ """Sanitize cell value: remove markdown, convert to string, handle None, limit length."""
if value is None:
return ""
if isinstance(value, dict):
# Extract value from dict if present
- return str(value.get("value", ""))
+ value = value.get("value", "")
if isinstance(value, (int, float)):
return value # Keep numbers as-is
# Convert to string and remove markdown formatting
@@ -852,7 +930,11 @@ class RendererXlsx(BaseRenderer):
text = text.replace("*", "")
# Remove other markdown
text = text.replace("__", "").replace("_", "")
- return text.strip()
+ text = text.strip()
+ # Excel cell value limit is 32,767 characters - truncate if necessary
+ if len(text) > 32767:
+ text = text[:32764] + "..."
+ return text
def _addTableToExcel(self, sheet, element: Dict[str, Any], styles: Dict[str, Any], startRow: int) -> int:
"""Add a table element to Excel sheet with proper formatting and borders."""