deckbuilder 1.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deckbuilder/__init__.py +22 -0
- deckbuilder/cli.py +544 -0
- deckbuilder/cli_tools.py +739 -0
- deckbuilder/engine.py +1546 -0
- deckbuilder/image_handler.py +291 -0
- deckbuilder/layout_intelligence.json +288 -0
- deckbuilder/layout_intelligence.py +398 -0
- deckbuilder/naming_conventions.py +541 -0
- deckbuilder/placeholder_types.py +101 -0
- deckbuilder/placekitten_integration.py +280 -0
- deckbuilder/structured_frontmatter.py +862 -0
- deckbuilder/table_styles.py +37 -0
- deckbuilder-1.0.0b1.dist-info/METADATA +378 -0
- deckbuilder-1.0.0b1.dist-info/RECORD +37 -0
- deckbuilder-1.0.0b1.dist-info/WHEEL +5 -0
- deckbuilder-1.0.0b1.dist-info/entry_points.txt +3 -0
- deckbuilder-1.0.0b1.dist-info/licenses/LICENSE +201 -0
- deckbuilder-1.0.0b1.dist-info/top_level.txt +4 -0
- mcp_server/__init__.py +9 -0
- mcp_server/content_analysis.py +436 -0
- mcp_server/content_optimization.py +822 -0
- mcp_server/layout_recommendations.py +595 -0
- mcp_server/main.py +550 -0
- mcp_server/tools.py +492 -0
- placekitten/README.md +561 -0
- placekitten/__init__.py +44 -0
- placekitten/core.py +184 -0
- placekitten/filters.py +183 -0
- placekitten/images/ACuteKitten-1.png +0 -0
- placekitten/images/ACuteKitten-2.png +0 -0
- placekitten/images/ACuteKitten-3.png +0 -0
- placekitten/images/TwoKitttens Playing-1.png +0 -0
- placekitten/images/TwoKitttens Playing-2.png +0 -0
- placekitten/images/TwoKitttensSleeping-1.png +0 -0
- placekitten/processor.py +262 -0
- placekitten/smart_crop.py +314 -0
- shared/__init__.py +9 -0
@@ -0,0 +1,822 @@
|
|
1
|
+
"""
|
2
|
+
Content Optimization Engine for Content-First Presentation Intelligence
|
3
|
+
|
4
|
+
This module implements Tool #3: optimize_content_for_layout() which takes user content
|
5
|
+
and a chosen layout, then optimizes the content structure and generates ready-to-use
|
6
|
+
YAML for immediate presentation creation.
|
7
|
+
|
8
|
+
Design Philosophy: Transform raw content into presentation-ready structure that
|
9
|
+
maximizes communication effectiveness within the chosen layout constraints.
|
10
|
+
"""
|
11
|
+
|
12
|
+
import re
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class ContentOptimizationResult:
|
19
|
+
"""Result of content optimization with YAML and analysis"""
|
20
|
+
|
21
|
+
yaml_structure: str
|
22
|
+
content_mapping: Dict[str, str]
|
23
|
+
formatting_applied: List[str]
|
24
|
+
title_generated: str
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class GapAnalysis:
|
29
|
+
"""Analysis of how well content fits the chosen layout"""
|
30
|
+
|
31
|
+
content_fit: str # excellent|good|fair|poor
|
32
|
+
missing_elements: List[str]
|
33
|
+
recommendations: List[str]
|
34
|
+
layout_utilization: float
|
35
|
+
|
36
|
+
|
37
|
+
class ContentOptimizationEngine:
|
38
|
+
"""
|
39
|
+
Optimizes raw content for specific PowerPoint layouts and generates ready-to-use YAML.
|
40
|
+
|
41
|
+
The final step in the content-first workflow that transforms analyzed content
|
42
|
+
into presentation-ready structured frontmatter.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self):
|
46
|
+
"""Initialize with layout templates and optimization rules"""
|
47
|
+
self.layout_templates = self._build_layout_templates()
|
48
|
+
self.formatting_rules = self._build_formatting_rules()
|
49
|
+
|
50
|
+
def optimize_content_for_layout(
|
51
|
+
self, content: str, chosen_layout: str, slide_context: Optional[Dict] = None
|
52
|
+
) -> Dict[str, Any]:
|
53
|
+
"""
|
54
|
+
Optimize content structure and generate ready-to-use YAML.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
content: Raw content to optimize
|
58
|
+
chosen_layout: Layout selected from recommend_slide_approach
|
59
|
+
slide_context: Optional context from previous tools
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Dictionary with optimized YAML, gap analysis, and presentation tips
|
63
|
+
"""
|
64
|
+
# Clean and prepare content
|
65
|
+
cleaned_content = self._clean_content(content)
|
66
|
+
|
67
|
+
# Generate optimized content structure
|
68
|
+
optimization_result = self._optimize_content_structure(
|
69
|
+
cleaned_content, chosen_layout, slide_context
|
70
|
+
)
|
71
|
+
|
72
|
+
# Perform gap analysis
|
73
|
+
gap_analysis = self._analyze_content_layout_fit(
|
74
|
+
cleaned_content, chosen_layout, optimization_result
|
75
|
+
)
|
76
|
+
|
77
|
+
# Generate presentation tips
|
78
|
+
presentation_tips = self._generate_presentation_tips(
|
79
|
+
chosen_layout, gap_analysis, slide_context
|
80
|
+
)
|
81
|
+
|
82
|
+
return {
|
83
|
+
"optimized_content": {
|
84
|
+
"yaml_structure": optimization_result.yaml_structure,
|
85
|
+
"content_mapping": optimization_result.content_mapping,
|
86
|
+
"formatting_applied": optimization_result.formatting_applied,
|
87
|
+
"title_generated": optimization_result.title_generated,
|
88
|
+
},
|
89
|
+
"gap_analysis": {
|
90
|
+
"content_fit": gap_analysis.content_fit,
|
91
|
+
"missing_elements": gap_analysis.missing_elements,
|
92
|
+
"recommendations": gap_analysis.recommendations,
|
93
|
+
"layout_utilization": gap_analysis.layout_utilization,
|
94
|
+
},
|
95
|
+
"presentation_tips": presentation_tips,
|
96
|
+
}
|
97
|
+
|
98
|
+
def _clean_content(self, content: str) -> str:
|
99
|
+
"""Clean and normalize the input content."""
|
100
|
+
# Remove excessive whitespace
|
101
|
+
cleaned = re.sub(r"\n\s*\n", "\n\n", content.strip())
|
102
|
+
|
103
|
+
# Normalize quotes
|
104
|
+
cleaned = re.sub(r'["""]', '"', cleaned)
|
105
|
+
cleaned = re.sub(r"[''']", "'", cleaned)
|
106
|
+
|
107
|
+
return cleaned
|
108
|
+
|
109
|
+
def _optimize_content_structure(
|
110
|
+
self, content: str, chosen_layout: str, slide_context: Optional[Dict]
|
111
|
+
) -> ContentOptimizationResult:
|
112
|
+
"""Optimize content structure for the chosen layout."""
|
113
|
+
layout_handler = self._get_layout_handler(chosen_layout)
|
114
|
+
|
115
|
+
if layout_handler:
|
116
|
+
return layout_handler(content, slide_context)
|
117
|
+
else:
|
118
|
+
# Fallback to basic Title and Content layout
|
119
|
+
return self._optimize_for_title_and_content(content, slide_context)
|
120
|
+
|
121
|
+
def _get_layout_handler(self, layout: str):
|
122
|
+
"""Get the appropriate optimization handler for the layout."""
|
123
|
+
handlers = {
|
124
|
+
"Four Columns": self._optimize_for_four_columns,
|
125
|
+
"Comparison": self._optimize_for_comparison,
|
126
|
+
"Two Content": self._optimize_for_two_content,
|
127
|
+
"Title and Content": self._optimize_for_title_and_content,
|
128
|
+
"Section Header": self._optimize_for_section_header,
|
129
|
+
"Title Slide": self._optimize_for_title_slide,
|
130
|
+
}
|
131
|
+
return handlers.get(layout)
|
132
|
+
|
133
|
+
def _optimize_for_four_columns(
|
134
|
+
self, content: str, slide_context: Optional[Dict]
|
135
|
+
) -> ContentOptimizationResult:
|
136
|
+
"""Optimize content for Four Columns layout."""
|
137
|
+
# Extract title
|
138
|
+
title = self._extract_or_generate_title(content, slide_context)
|
139
|
+
|
140
|
+
# Parse content into 4 columns
|
141
|
+
columns = self._parse_content_into_columns(content, 4)
|
142
|
+
|
143
|
+
# Apply formatting
|
144
|
+
formatted_columns = []
|
145
|
+
formatting_applied = []
|
146
|
+
|
147
|
+
for col in columns:
|
148
|
+
formatted_col = self._apply_content_formatting(col)
|
149
|
+
formatted_columns.append(formatted_col)
|
150
|
+
if formatted_col != col:
|
151
|
+
formatting_applied.append("content optimization")
|
152
|
+
|
153
|
+
# Generate YAML
|
154
|
+
yaml_structure = self._generate_four_columns_yaml(title, formatted_columns)
|
155
|
+
|
156
|
+
# Create content mapping
|
157
|
+
content_mapping = {
|
158
|
+
"title": title,
|
159
|
+
**{f"column_{i + 1}": col for i, col in enumerate(formatted_columns)},
|
160
|
+
}
|
161
|
+
|
162
|
+
return ContentOptimizationResult(
|
163
|
+
yaml_structure=yaml_structure,
|
164
|
+
content_mapping=content_mapping,
|
165
|
+
formatting_applied=formatting_applied,
|
166
|
+
title_generated=title,
|
167
|
+
)
|
168
|
+
|
169
|
+
def _optimize_for_comparison(
|
170
|
+
self, content: str, slide_context: Optional[Dict]
|
171
|
+
) -> ContentOptimizationResult:
|
172
|
+
"""Optimize content for Comparison layout."""
|
173
|
+
title = self._extract_or_generate_title(content, slide_context)
|
174
|
+
|
175
|
+
# Parse content into left/right comparison
|
176
|
+
left_content, right_content = self._parse_content_into_comparison(content)
|
177
|
+
|
178
|
+
# Apply formatting
|
179
|
+
left_formatted = self._apply_content_formatting(left_content["content"])
|
180
|
+
right_formatted = self._apply_content_formatting(right_content["content"])
|
181
|
+
|
182
|
+
formatting_applied = []
|
183
|
+
if left_formatted != left_content["content"] or right_formatted != right_content["content"]:
|
184
|
+
formatting_applied.append("comparison content optimization")
|
185
|
+
|
186
|
+
# Generate YAML
|
187
|
+
yaml_structure = self._generate_comparison_yaml(
|
188
|
+
title, left_content["title"], left_formatted, right_content["title"], right_formatted
|
189
|
+
)
|
190
|
+
|
191
|
+
content_mapping = {
|
192
|
+
"title": title,
|
193
|
+
"left_title": left_content["title"],
|
194
|
+
"left_content": left_formatted,
|
195
|
+
"right_title": right_content["title"],
|
196
|
+
"right_content": right_formatted,
|
197
|
+
}
|
198
|
+
|
199
|
+
return ContentOptimizationResult(
|
200
|
+
yaml_structure=yaml_structure,
|
201
|
+
content_mapping=content_mapping,
|
202
|
+
formatting_applied=formatting_applied,
|
203
|
+
title_generated=title,
|
204
|
+
)
|
205
|
+
|
206
|
+
def _optimize_for_two_content(
|
207
|
+
self, content: str, slide_context: Optional[Dict]
|
208
|
+
) -> ContentOptimizationResult:
|
209
|
+
"""Optimize content for Two Content layout."""
|
210
|
+
title = self._extract_or_generate_title(content, slide_context)
|
211
|
+
|
212
|
+
# Parse into two sections
|
213
|
+
sections = self._parse_content_into_columns(content, 2)
|
214
|
+
|
215
|
+
# Apply formatting
|
216
|
+
formatted_sections = []
|
217
|
+
formatting_applied = []
|
218
|
+
|
219
|
+
for section in sections:
|
220
|
+
formatted_section = self._apply_content_formatting(section)
|
221
|
+
formatted_sections.append(formatted_section)
|
222
|
+
if formatted_section != section:
|
223
|
+
formatting_applied.append("section content optimization")
|
224
|
+
|
225
|
+
# Generate YAML
|
226
|
+
yaml_structure = self._generate_two_content_yaml(title, formatted_sections)
|
227
|
+
|
228
|
+
content_mapping = {
|
229
|
+
"title": title,
|
230
|
+
"section_1": formatted_sections[0] if len(formatted_sections) > 0 else "",
|
231
|
+
"section_2": formatted_sections[1] if len(formatted_sections) > 1 else "",
|
232
|
+
}
|
233
|
+
|
234
|
+
return ContentOptimizationResult(
|
235
|
+
yaml_structure=yaml_structure,
|
236
|
+
content_mapping=content_mapping,
|
237
|
+
formatting_applied=formatting_applied,
|
238
|
+
title_generated=title,
|
239
|
+
)
|
240
|
+
|
241
|
+
def _optimize_for_title_and_content(
|
242
|
+
self, content: str, slide_context: Optional[Dict]
|
243
|
+
) -> ContentOptimizationResult:
|
244
|
+
"""Optimize content for Title and Content layout."""
|
245
|
+
title = self._extract_or_generate_title(content, slide_context)
|
246
|
+
|
247
|
+
# Extract main content (everything after title)
|
248
|
+
main_content = self._extract_main_content(content, title)
|
249
|
+
|
250
|
+
# Apply formatting and structure
|
251
|
+
formatted_content = self._apply_content_formatting(main_content)
|
252
|
+
formatted_content = self._structure_as_bullets(formatted_content)
|
253
|
+
|
254
|
+
formatting_applied = (
|
255
|
+
["bullet point structuring"] if formatted_content != main_content else []
|
256
|
+
)
|
257
|
+
|
258
|
+
# Generate YAML
|
259
|
+
yaml_structure = self._generate_title_and_content_yaml(title, formatted_content)
|
260
|
+
|
261
|
+
content_mapping = {"title": title, "content": formatted_content}
|
262
|
+
|
263
|
+
return ContentOptimizationResult(
|
264
|
+
yaml_structure=yaml_structure,
|
265
|
+
content_mapping=content_mapping,
|
266
|
+
formatting_applied=formatting_applied,
|
267
|
+
title_generated=title,
|
268
|
+
)
|
269
|
+
|
270
|
+
def _optimize_for_section_header(
|
271
|
+
self, content: str, slide_context: Optional[Dict]
|
272
|
+
) -> ContentOptimizationResult:
|
273
|
+
"""Optimize content for Section Header layout."""
|
274
|
+
title = self._extract_or_generate_title(content, slide_context)
|
275
|
+
subtitle = self._extract_subtitle_or_summary(content)
|
276
|
+
|
277
|
+
# Generate YAML
|
278
|
+
yaml_structure = self._generate_section_header_yaml(title, subtitle)
|
279
|
+
|
280
|
+
content_mapping = {"title": title, "subtitle": subtitle}
|
281
|
+
|
282
|
+
return ContentOptimizationResult(
|
283
|
+
yaml_structure=yaml_structure,
|
284
|
+
content_mapping=content_mapping,
|
285
|
+
formatting_applied=["section header formatting"],
|
286
|
+
title_generated=title,
|
287
|
+
)
|
288
|
+
|
289
|
+
def _optimize_for_title_slide(
|
290
|
+
self, content: str, slide_context: Optional[Dict]
|
291
|
+
) -> ContentOptimizationResult:
|
292
|
+
"""Optimize content for Title Slide layout."""
|
293
|
+
title = self._extract_or_generate_title(content, slide_context)
|
294
|
+
subtitle = self._extract_subtitle_or_summary(content)
|
295
|
+
|
296
|
+
# Generate YAML
|
297
|
+
yaml_structure = self._generate_title_slide_yaml(title, subtitle)
|
298
|
+
|
299
|
+
content_mapping = {"title": title, "subtitle": subtitle}
|
300
|
+
|
301
|
+
return ContentOptimizationResult(
|
302
|
+
yaml_structure=yaml_structure,
|
303
|
+
content_mapping=content_mapping,
|
304
|
+
formatting_applied=["title slide formatting"],
|
305
|
+
title_generated=title,
|
306
|
+
)
|
307
|
+
|
308
|
+
def _extract_or_generate_title(self, content: str, slide_context: Optional[Dict]) -> str:
|
309
|
+
"""Extract title from content or generate from context."""
|
310
|
+
# Try to extract from first line if it looks like a title
|
311
|
+
lines = content.strip().split("\n")
|
312
|
+
first_line = lines[0].strip() if lines else ""
|
313
|
+
|
314
|
+
# Check if first line is title-like
|
315
|
+
if len(first_line) < 80 and not first_line.endswith(".") and len(first_line.split()) <= 8:
|
316
|
+
return first_line
|
317
|
+
|
318
|
+
# Try to generate from slide context
|
319
|
+
if slide_context and "message_intent" in slide_context:
|
320
|
+
intent = slide_context["message_intent"]
|
321
|
+
# Extract key words from intent
|
322
|
+
key_words = [word for word in intent.split() if len(word) > 3][:3]
|
323
|
+
if key_words:
|
324
|
+
return " ".join(word.capitalize() for word in key_words)
|
325
|
+
|
326
|
+
# Extract key terms from content
|
327
|
+
content_words = re.findall(r"\b[A-Za-z]{4,}\b", content)
|
328
|
+
if content_words:
|
329
|
+
# Take first few significant words
|
330
|
+
title_words = content_words[:3]
|
331
|
+
return " ".join(word.capitalize() for word in title_words)
|
332
|
+
|
333
|
+
return "Content Overview"
|
334
|
+
|
335
|
+
def _parse_content_into_columns(self, content: str, num_columns: int) -> List[str]:
|
336
|
+
"""Parse content into specified number of columns."""
|
337
|
+
# Look for explicit list items first
|
338
|
+
bullet_items = re.findall(r"[-*•]\s*([^\n]+)", content)
|
339
|
+
if len(bullet_items) >= num_columns:
|
340
|
+
return bullet_items[:num_columns]
|
341
|
+
|
342
|
+
# Look for numbered list items
|
343
|
+
numbered_items = re.findall(r"\d+\)\s*([^\n]+)", content)
|
344
|
+
if len(numbered_items) >= num_columns:
|
345
|
+
return numbered_items[:num_columns]
|
346
|
+
|
347
|
+
# Try comma-separated items
|
348
|
+
comma_parts = [part.strip() for part in content.split(",")]
|
349
|
+
if len(comma_parts) >= num_columns:
|
350
|
+
return comma_parts[:num_columns]
|
351
|
+
|
352
|
+
# Try colon-separated items
|
353
|
+
colon_items = re.findall(r"([^:]+):\s*([^,\n]+)", content)
|
354
|
+
if len(colon_items) >= num_columns:
|
355
|
+
return [f"{item[0]}: {item[1]}" for item in colon_items[:num_columns]]
|
356
|
+
|
357
|
+
# Split by sentences/phrases
|
358
|
+
sentences = re.split(r"[.!?]+", content)
|
359
|
+
clean_sentences = [s.strip() for s in sentences if s.strip()]
|
360
|
+
|
361
|
+
if len(clean_sentences) >= num_columns:
|
362
|
+
return clean_sentences[:num_columns]
|
363
|
+
|
364
|
+
# Fallback: split content into equal parts
|
365
|
+
words = content.split()
|
366
|
+
if len(words) < num_columns * 3: # Too few words
|
367
|
+
# Pad with placeholder content
|
368
|
+
result = [content] if words else []
|
369
|
+
while len(result) < num_columns:
|
370
|
+
result.append(f"Additional content for column {len(result) + 1}")
|
371
|
+
return result
|
372
|
+
|
373
|
+
# Split words roughly equally
|
374
|
+
words_per_column = len(words) // num_columns
|
375
|
+
columns = []
|
376
|
+
for i in range(num_columns):
|
377
|
+
start = i * words_per_column
|
378
|
+
end = start + words_per_column if i < num_columns - 1 else len(words)
|
379
|
+
columns.append(" ".join(words[start:end]))
|
380
|
+
|
381
|
+
return columns
|
382
|
+
|
383
|
+
def _parse_content_into_comparison(self, content: str) -> Tuple[Dict[str, str], Dict[str, str]]:
|
384
|
+
"""Parse content into left/right comparison structure."""
|
385
|
+
# Look for explicit vs/versus patterns
|
386
|
+
vs_match = re.search(r"(.+?)\s+(?:vs|versus|compared to)\s+(.+)", content, re.IGNORECASE)
|
387
|
+
if vs_match:
|
388
|
+
left_content = vs_match.group(1).strip()
|
389
|
+
right_content = vs_match.group(2).strip()
|
390
|
+
|
391
|
+
return (
|
392
|
+
{"title": self._extract_comparison_title(left_content), "content": left_content},
|
393
|
+
{"title": self._extract_comparison_title(right_content), "content": right_content},
|
394
|
+
)
|
395
|
+
|
396
|
+
# Look for contrasting words
|
397
|
+
contrast_patterns = [
|
398
|
+
r"(.+?)\s+(?:but|however|while)\s+(.+)",
|
399
|
+
r"(.+?)\s+(?:whereas|although)\s+(.+)",
|
400
|
+
]
|
401
|
+
|
402
|
+
for pattern in contrast_patterns:
|
403
|
+
match = re.search(pattern, content, re.IGNORECASE)
|
404
|
+
if match:
|
405
|
+
left_content = match.group(1).strip()
|
406
|
+
right_content = match.group(2).strip()
|
407
|
+
|
408
|
+
return (
|
409
|
+
{"title": "Current Approach", "content": left_content},
|
410
|
+
{"title": "Alternative Approach", "content": right_content},
|
411
|
+
)
|
412
|
+
|
413
|
+
# Split content in half
|
414
|
+
sentences = re.split(r"[.!?]+", content)
|
415
|
+
clean_sentences = [s.strip() for s in sentences if s.strip()]
|
416
|
+
|
417
|
+
mid_point = len(clean_sentences) // 2
|
418
|
+
left_content = ". ".join(clean_sentences[:mid_point])
|
419
|
+
right_content = ". ".join(clean_sentences[mid_point:])
|
420
|
+
|
421
|
+
return (
|
422
|
+
{"title": "Option A", "content": left_content},
|
423
|
+
{"title": "Option B", "content": right_content},
|
424
|
+
)
|
425
|
+
|
426
|
+
def _extract_comparison_title(self, content: str) -> str:
|
427
|
+
"""Extract a short title from comparison content."""
|
428
|
+
words = content.split()[:3]
|
429
|
+
return " ".join(word.capitalize() for word in words)
|
430
|
+
|
431
|
+
def _extract_main_content(self, content: str, title: str) -> str:
|
432
|
+
"""Extract main content excluding the title."""
|
433
|
+
if title and title in content:
|
434
|
+
# Remove title from content
|
435
|
+
content_without_title = content.replace(title, "", 1).strip()
|
436
|
+
return content_without_title if content_without_title else content
|
437
|
+
return content
|
438
|
+
|
439
|
+
def _extract_subtitle_or_summary(self, content: str) -> str:
|
440
|
+
"""Extract subtitle or create a summary."""
|
441
|
+
# Look for a second line that could be a subtitle
|
442
|
+
lines = [line.strip() for line in content.split("\n") if line.strip()]
|
443
|
+
if len(lines) >= 2:
|
444
|
+
second_line = lines[1]
|
445
|
+
if len(second_line) < 100: # Reasonable subtitle length
|
446
|
+
return second_line
|
447
|
+
|
448
|
+
# Create a summary from content
|
449
|
+
sentences = re.split(r"[.!?]+", content)
|
450
|
+
clean_sentences = [s.strip() for s in sentences if s.strip()]
|
451
|
+
|
452
|
+
if clean_sentences:
|
453
|
+
# Take first sentence as summary, limited length
|
454
|
+
summary = clean_sentences[0]
|
455
|
+
if len(summary) > 100:
|
456
|
+
summary = summary[:97] + "..."
|
457
|
+
return summary
|
458
|
+
|
459
|
+
return "Overview"
|
460
|
+
|
461
|
+
def _apply_content_formatting(self, content: str) -> str:
|
462
|
+
"""Apply smart formatting to content."""
|
463
|
+
if not content:
|
464
|
+
return content
|
465
|
+
|
466
|
+
# Emphasize key numbers and percentages
|
467
|
+
content = re.sub(r"(\d+%)", r"**\1**", content)
|
468
|
+
content = re.sub(r"(\$[\d,]+)", r"**\1**", content)
|
469
|
+
|
470
|
+
# Emphasize superlatives and strong words
|
471
|
+
strong_words = [
|
472
|
+
"best",
|
473
|
+
"fastest",
|
474
|
+
"highest",
|
475
|
+
"lowest",
|
476
|
+
"most",
|
477
|
+
"least",
|
478
|
+
"critical",
|
479
|
+
"important",
|
480
|
+
"key",
|
481
|
+
]
|
482
|
+
for word in strong_words:
|
483
|
+
content = re.sub(rf"\b({word})\b", r"**\1**", content, flags=re.IGNORECASE)
|
484
|
+
|
485
|
+
return content
|
486
|
+
|
487
|
+
def _structure_as_bullets(self, content: str) -> str:
|
488
|
+
"""Structure content as bullet points if appropriate."""
|
489
|
+
# If already has bullets, keep as is
|
490
|
+
if re.search(r"^\s*[-*•]", content, re.MULTILINE):
|
491
|
+
return content
|
492
|
+
|
493
|
+
# Split sentences and convert to bullets
|
494
|
+
sentences = re.split(r"[.!?]+", content)
|
495
|
+
clean_sentences = [s.strip() for s in sentences if s.strip() and len(s) > 10]
|
496
|
+
|
497
|
+
if len(clean_sentences) >= 2:
|
498
|
+
return "\n".join(f"- {sentence}" for sentence in clean_sentences)
|
499
|
+
|
500
|
+
return content
|
501
|
+
|
502
|
+
# YAML Generation Methods
|
503
|
+
def _generate_four_columns_yaml(self, title: str, columns: List[str]) -> str:
|
504
|
+
"""Generate YAML for Four Columns layout."""
|
505
|
+
yaml_columns = []
|
506
|
+
for i, col in enumerate(columns):
|
507
|
+
col_title = f"Column {i + 1}"
|
508
|
+
# Try to extract a title from the column content
|
509
|
+
if ":" in col:
|
510
|
+
parts = col.split(":", 1)
|
511
|
+
col_title = parts[0].strip()
|
512
|
+
col_content = parts[1].strip()
|
513
|
+
else:
|
514
|
+
col_content = col
|
515
|
+
|
516
|
+
yaml_columns.append(
|
517
|
+
f""" - title: {col_title}
|
518
|
+
content: "{col_content}" """
|
519
|
+
)
|
520
|
+
|
521
|
+
return f"""---
|
522
|
+
layout: Four Columns
|
523
|
+
title: {title}
|
524
|
+
columns:
|
525
|
+
{chr(10).join(yaml_columns)}
|
526
|
+
---"""
|
527
|
+
|
528
|
+
def _generate_comparison_yaml(
|
529
|
+
self, title: str, left_title: str, left_content: str, right_title: str, right_content: str
|
530
|
+
) -> str:
|
531
|
+
"""Generate YAML for Comparison layout."""
|
532
|
+
return f"""---
|
533
|
+
layout: Comparison
|
534
|
+
title: {title}
|
535
|
+
comparison:
|
536
|
+
left:
|
537
|
+
title: {left_title}
|
538
|
+
content: "{left_content}"
|
539
|
+
right:
|
540
|
+
title: {right_title}
|
541
|
+
content: "{right_content}"
|
542
|
+
---"""
|
543
|
+
|
544
|
+
def _generate_two_content_yaml(self, title: str, sections: List[str]) -> str:
|
545
|
+
"""Generate YAML for Two Content layout."""
|
546
|
+
section1 = sections[0] if len(sections) > 0 else "Content for first section"
|
547
|
+
section2 = sections[1] if len(sections) > 1 else "Content for second section"
|
548
|
+
|
549
|
+
return f"""---
|
550
|
+
layout: Two Content
|
551
|
+
title: {title}
|
552
|
+
sections:
|
553
|
+
- title: Section 1
|
554
|
+
content: "{section1}"
|
555
|
+
- title: Section 2
|
556
|
+
content: "{section2}"
|
557
|
+
---"""
|
558
|
+
|
559
|
+
def _generate_title_and_content_yaml(self, title: str, content: str) -> str:
|
560
|
+
"""Generate YAML for Title and Content layout."""
|
561
|
+
return f"""---
|
562
|
+
layout: Title and Content
|
563
|
+
title: {title}
|
564
|
+
---
|
565
|
+
{content}"""
|
566
|
+
|
567
|
+
def _generate_section_header_yaml(self, title: str, subtitle: str) -> str:
|
568
|
+
"""Generate YAML for Section Header layout."""
|
569
|
+
return f"""---
|
570
|
+
layout: Section Header
|
571
|
+
title: {title}
|
572
|
+
subtitle: {subtitle}
|
573
|
+
---"""
|
574
|
+
|
575
|
+
def _generate_title_slide_yaml(self, title: str, subtitle: str) -> str:
|
576
|
+
"""Generate YAML for Title Slide layout."""
|
577
|
+
return f"""---
|
578
|
+
layout: Title Slide
|
579
|
+
title: {title}
|
580
|
+
subtitle: {subtitle}
|
581
|
+
---"""
|
582
|
+
|
583
|
+
def _analyze_content_layout_fit(
|
584
|
+
self, content: str, chosen_layout: str, optimization_result: ContentOptimizationResult
|
585
|
+
) -> GapAnalysis:
|
586
|
+
"""Analyze how well the content fits the chosen layout."""
|
587
|
+
# Analyze content characteristics
|
588
|
+
# content_length = len(content.split()) # Future: use for length analysis
|
589
|
+
# layout_requirements = self._get_layout_requirements(chosen_layout)
|
590
|
+
# Future: use for validation
|
591
|
+
|
592
|
+
# Assess content fit
|
593
|
+
fit_score = self._calculate_fit_score(content, chosen_layout, optimization_result)
|
594
|
+
|
595
|
+
if fit_score >= 0.8:
|
596
|
+
content_fit = "excellent"
|
597
|
+
elif fit_score >= 0.6:
|
598
|
+
content_fit = "good"
|
599
|
+
elif fit_score >= 0.4:
|
600
|
+
content_fit = "fair"
|
601
|
+
else:
|
602
|
+
content_fit = "poor"
|
603
|
+
|
604
|
+
# Identify missing elements
|
605
|
+
missing_elements = self._identify_missing_elements(content, chosen_layout)
|
606
|
+
|
607
|
+
# Generate recommendations
|
608
|
+
recommendations = self._generate_fit_recommendations(
|
609
|
+
content_fit, missing_elements, chosen_layout
|
610
|
+
)
|
611
|
+
|
612
|
+
# Calculate layout utilization
|
613
|
+
layout_utilization = min(fit_score, 1.0)
|
614
|
+
|
615
|
+
return GapAnalysis(
|
616
|
+
content_fit=content_fit,
|
617
|
+
missing_elements=missing_elements,
|
618
|
+
recommendations=recommendations,
|
619
|
+
layout_utilization=layout_utilization,
|
620
|
+
)
|
621
|
+
|
622
|
+
def _calculate_fit_score(
|
623
|
+
self, content: str, layout: str, optimization_result: ContentOptimizationResult
|
624
|
+
) -> float:
|
625
|
+
"""Calculate a fit score between content and layout."""
|
626
|
+
score = 0.5 # Base score
|
627
|
+
|
628
|
+
# Bonus for having appropriate content structure
|
629
|
+
if layout == "Four Columns":
|
630
|
+
content_elements = len(optimization_result.content_mapping) - 1 # Exclude title
|
631
|
+
if content_elements >= 4:
|
632
|
+
score += 0.3
|
633
|
+
elif content_elements >= 3:
|
634
|
+
score += 0.2
|
635
|
+
elif content_elements >= 2:
|
636
|
+
score += 0.1
|
637
|
+
|
638
|
+
elif layout == "Comparison":
|
639
|
+
if (
|
640
|
+
"left_content" in optimization_result.content_mapping
|
641
|
+
and "right_content" in optimization_result.content_mapping
|
642
|
+
):
|
643
|
+
score += 0.3
|
644
|
+
# Bonus for balanced content
|
645
|
+
left_len = len(optimization_result.content_mapping["left_content"].split())
|
646
|
+
right_len = len(optimization_result.content_mapping["right_content"].split())
|
647
|
+
if 0.5 <= left_len / max(right_len, 1) <= 2.0:
|
648
|
+
score += 0.2
|
649
|
+
|
650
|
+
elif layout in ["Title and Content", "Section Header", "Title Slide"]:
|
651
|
+
# These layouts are very flexible
|
652
|
+
score += 0.3
|
653
|
+
|
654
|
+
# Bonus for appropriate content length
|
655
|
+
word_count = len(content.split())
|
656
|
+
if 50 <= word_count <= 200: # Sweet spot for slide content
|
657
|
+
score += 0.2
|
658
|
+
elif 20 <= word_count <= 300: # Acceptable range
|
659
|
+
score += 0.1
|
660
|
+
|
661
|
+
return min(score, 1.0)
|
662
|
+
|
663
|
+
def _identify_missing_elements(self, content: str, layout: str) -> List[str]:
|
664
|
+
"""Identify elements missing for optimal layout utilization."""
|
665
|
+
missing = []
|
666
|
+
|
667
|
+
if layout == "Four Columns":
|
668
|
+
content_elements = len(self._parse_content_into_columns(content, 4))
|
669
|
+
if content_elements < 4:
|
670
|
+
missing.append(
|
671
|
+
f"Need {4 - content_elements} more content elements for full utilization"
|
672
|
+
)
|
673
|
+
|
674
|
+
elif layout == "Comparison":
|
675
|
+
if not any(
|
676
|
+
word in content.lower()
|
677
|
+
for word in ["vs", "versus", "compared", "against", "but", "however"]
|
678
|
+
):
|
679
|
+
missing.append("Content lacks clear comparison elements")
|
680
|
+
|
681
|
+
# Check for visual elements that could enhance the slide
|
682
|
+
if not re.search(r"\d+%|\$[\d,]+", content):
|
683
|
+
if layout in ["Four Columns", "Title and Content"]:
|
684
|
+
missing.append("Consider adding metrics or data points for impact")
|
685
|
+
|
686
|
+
return missing
|
687
|
+
|
688
|
+
def _generate_fit_recommendations(
|
689
|
+
self, content_fit: str, missing_elements: List[str], layout: str
|
690
|
+
) -> List[str]:
|
691
|
+
"""Generate recommendations for improving content-layout fit."""
|
692
|
+
recommendations = []
|
693
|
+
|
694
|
+
if content_fit == "poor":
|
695
|
+
recommendations.append(
|
696
|
+
"Consider switching to a simpler layout like 'Title and Content'"
|
697
|
+
)
|
698
|
+
elif content_fit == "fair":
|
699
|
+
recommendations.append(
|
700
|
+
"Content structure could be improved for better layout utilization"
|
701
|
+
)
|
702
|
+
|
703
|
+
if missing_elements:
|
704
|
+
recommendations.extend([f"Suggestion: {element}" for element in missing_elements])
|
705
|
+
|
706
|
+
# Layout-specific recommendations
|
707
|
+
if layout == "Four Columns" and content_fit in ["fair", "poor"]:
|
708
|
+
recommendations.append("Try organizing content into 4 distinct categories or points")
|
709
|
+
elif layout == "Comparison" and content_fit in ["fair", "poor"]:
|
710
|
+
recommendations.append("Highlight the contrasting elements more clearly")
|
711
|
+
|
712
|
+
return recommendations
|
713
|
+
|
714
|
+
def _generate_presentation_tips(
|
715
|
+
self, layout: str, gap_analysis: GapAnalysis, slide_context: Optional[Dict]
|
716
|
+
) -> Dict[str, str]:
|
717
|
+
"""Generate presentation delivery tips."""
|
718
|
+
tips = {}
|
719
|
+
|
720
|
+
# Layout-specific delivery guidance
|
721
|
+
if layout == "Four Columns":
|
722
|
+
tips["delivery_guidance"] = (
|
723
|
+
"Present columns in logical order, allow time for audience to "
|
724
|
+
"process each section"
|
725
|
+
)
|
726
|
+
elif layout == "Comparison":
|
727
|
+
tips["delivery_guidance"] = (
|
728
|
+
"Guide audience through left side first, then right, conclude with recommendation"
|
729
|
+
)
|
730
|
+
elif layout == "Title and Content":
|
731
|
+
tips["delivery_guidance"] = (
|
732
|
+
"Use title to set context, walk through content points systematically"
|
733
|
+
)
|
734
|
+
else:
|
735
|
+
tips["delivery_guidance"] = "Keep focus on key message, use slide as visual support"
|
736
|
+
|
737
|
+
# Audience adaptation
|
738
|
+
if slide_context and "audience" in slide_context:
|
739
|
+
audience = slide_context["audience"]
|
740
|
+
if audience == "board":
|
741
|
+
tips["audience_adaptation"] = (
|
742
|
+
"Focus on high-level insights, minimize technical details"
|
743
|
+
)
|
744
|
+
elif audience == "technical":
|
745
|
+
tips["audience_adaptation"] = (
|
746
|
+
"Include technical details, be prepared for deep-dive questions"
|
747
|
+
)
|
748
|
+
else:
|
749
|
+
tips["audience_adaptation"] = "Balance detail level, check for understanding"
|
750
|
+
else:
|
751
|
+
tips["audience_adaptation"] = "Adapt detail level based on audience expertise"
|
752
|
+
|
753
|
+
# Timing estimate
|
754
|
+
word_count = sum(
|
755
|
+
len(mapping.split())
|
756
|
+
for mapping in gap_analysis.__dict__.values()
|
757
|
+
if isinstance(mapping, str)
|
758
|
+
)
|
759
|
+
if word_count < 50:
|
760
|
+
tips["timing_estimate"] = "1-2 minutes"
|
761
|
+
elif word_count < 100:
|
762
|
+
tips["timing_estimate"] = "2-3 minutes"
|
763
|
+
else:
|
764
|
+
tips["timing_estimate"] = "3-4 minutes"
|
765
|
+
|
766
|
+
return tips
|
767
|
+
|
768
|
+
def _get_layout_requirements(self, layout: str) -> Dict[str, Any]:
|
769
|
+
"""Get requirements and characteristics for each layout."""
|
770
|
+
return {
|
771
|
+
"Four Columns": {"min_elements": 2, "max_elements": 4, "flexibility": "medium"},
|
772
|
+
"Comparison": {"min_elements": 2, "max_elements": 2, "flexibility": "low"},
|
773
|
+
"Two Content": {"min_elements": 1, "max_elements": 2, "flexibility": "high"},
|
774
|
+
"Title and Content": {
|
775
|
+
"min_elements": 1,
|
776
|
+
"max_elements": "unlimited",
|
777
|
+
"flexibility": "very_high",
|
778
|
+
},
|
779
|
+
"Section Header": {"min_elements": 1, "max_elements": 2, "flexibility": "high"},
|
780
|
+
"Title Slide": {"min_elements": 1, "max_elements": 2, "flexibility": "high"},
|
781
|
+
}.get(layout, {"min_elements": 1, "max_elements": "unlimited", "flexibility": "high"})
|
782
|
+
|
783
|
+
def _build_layout_templates(self) -> Dict[str, Dict]:
|
784
|
+
"""Build templates for different layouts."""
|
785
|
+
return {
|
786
|
+
"Four Columns": {
|
787
|
+
"required_fields": ["title", "columns"],
|
788
|
+
"optimal_content_length": "20-40 words per column",
|
789
|
+
},
|
790
|
+
"Comparison": {
|
791
|
+
"required_fields": ["title", "left", "right"],
|
792
|
+
"optimal_content_length": "30-60 words per side",
|
793
|
+
},
|
794
|
+
}
|
795
|
+
|
796
|
+
def _build_formatting_rules(self) -> Dict[str, List[str]]:
|
797
|
+
"""Build formatting rules for different content types."""
|
798
|
+
return {
|
799
|
+
"numbers": ["**{number}**"],
|
800
|
+
"percentages": ["**{percentage}**"],
|
801
|
+
"currency": ["**{amount}**"],
|
802
|
+
"emphasis": ["**{word}**"],
|
803
|
+
}
|
804
|
+
|
805
|
+
|
806
|
+
# Helper function for easy import
|
807
|
+
def optimize_content_for_layout(
|
808
|
+
content: str, chosen_layout: str, slide_context: Optional[Dict] = None
|
809
|
+
) -> Dict[str, Any]:
|
810
|
+
"""
|
811
|
+
Convenience function for optimizing content for a specific layout.
|
812
|
+
|
813
|
+
Args:
|
814
|
+
content: Raw content to optimize
|
815
|
+
chosen_layout: Layout selected from recommend_slide_approach
|
816
|
+
slide_context: Optional context from previous tools
|
817
|
+
|
818
|
+
Returns:
|
819
|
+
Dictionary with optimized YAML, gap analysis, and presentation tips
|
820
|
+
"""
|
821
|
+
engine = ContentOptimizationEngine()
|
822
|
+
return engine.optimize_content_for_layout(content, chosen_layout, slide_context)
|