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.
@@ -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)