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.
mcp_server/tools.py ADDED
@@ -0,0 +1,492 @@
1
+ import json
2
+ import os
3
+ from typing import Dict, Optional
4
+
5
+ from pptx import Presentation
6
+
7
+
8
+ class TemplateAnalyzer:
9
+ """Analyzes PowerPoint templates to extract raw layout and placeholder information."""
10
+
11
+ def __init__(self):
12
+ self.template_path = os.getenv("DECK_TEMPLATE_FOLDER")
13
+ self.output_folder = os.getenv("DECK_OUTPUT_FOLDER")
14
+
15
+ def analyze_pptx_template(self, template_name: str) -> Dict:
16
+ """
17
+ Analyze a PowerPoint template and extract raw layout information.
18
+
19
+ Args:
20
+ template_name: Name of the template file (with or without .pptx extension)
21
+
22
+ Returns:
23
+ Dictionary containing template structure with placeholder indices
24
+ """
25
+ # Ensure template name has .pptx extension
26
+ if not template_name.endswith(".pptx"):
27
+ template_name += ".pptx"
28
+
29
+ # Build template path
30
+ if not self.template_path:
31
+ raise RuntimeError("DECK_TEMPLATE_FOLDER environment variable not set")
32
+
33
+ template_path = os.path.join(self.template_path, template_name)
34
+
35
+ if not os.path.exists(template_path):
36
+ raise FileNotFoundError(f"Template file not found: {template_path}")
37
+
38
+ try:
39
+ prs = Presentation(template_path)
40
+
41
+ # Extract basic template info
42
+ base_name = os.path.splitext(template_name)[0]
43
+ template_info = {"name": base_name.replace("_", " ").title(), "version": "1.0"}
44
+
45
+ # Extract raw layout data
46
+ layouts = self._extract_layouts(prs)
47
+
48
+ # Validate template and generate warnings
49
+ validation_results = self._validate_template(layouts)
50
+
51
+ # Generate basic aliases structure (empty for user to fill)
52
+ aliases = self._generate_aliases_template()
53
+
54
+ result = {"template_info": template_info, "layouts": layouts, "aliases": aliases}
55
+
56
+ # Add validation results
57
+ if validation_results["warnings"] or validation_results["errors"]:
58
+ result["validation"] = validation_results
59
+
60
+ # Save to output folder as .g.json
61
+ self._save_json_mapping(base_name, result)
62
+
63
+ # Print validation results
64
+ self._print_validation_results(validation_results)
65
+
66
+ return result
67
+
68
+ except Exception as e:
69
+ raise RuntimeError(f"Error analyzing template: {str(e)}")
70
+
71
+ def _extract_layouts(self, presentation: Presentation) -> Dict:
72
+ """Extract raw layout data from all slide layouts."""
73
+ layouts = {}
74
+
75
+ for idx, layout in enumerate(presentation.slide_layouts):
76
+ layout_info = self._extract_single_layout(layout, idx)
77
+ if layout_info:
78
+ # Use actual PowerPoint layout name
79
+ layout_name = f"layout_{idx}" # fallback
80
+ try:
81
+ if hasattr(layout, "name") and layout.name:
82
+ layout_name = layout.name
83
+ except Exception:
84
+ pass # Keep fallback name
85
+
86
+ layouts[layout_name] = layout_info
87
+
88
+ return layouts
89
+
90
+ def _extract_single_layout(self, layout, index: int) -> Optional[Dict]:
91
+ """
92
+ Extract raw placeholder data from a single slide layout.
93
+
94
+ Args:
95
+ layout: PowerPoint slide layout object
96
+ index: Layout index in the template
97
+
98
+ Returns:
99
+ Dictionary with raw layout information
100
+ """
101
+ try:
102
+ placeholders = {}
103
+
104
+ # Extract each placeholder's index and available properties
105
+ for shape in layout.placeholders:
106
+ placeholder_idx = shape.placeholder_format.idx
107
+
108
+ # Try to get more information about the placeholder
109
+ placeholder_info = f"placeholder_{placeholder_idx}"
110
+
111
+ # Check if placeholder has a name or type information
112
+ try:
113
+ if hasattr(shape, "name") and shape.name:
114
+ placeholder_info = shape.name
115
+ elif hasattr(shape.placeholder_format, "type"):
116
+ placeholder_type = shape.placeholder_format.type
117
+ placeholder_info = f"type_{placeholder_type}"
118
+ except Exception:
119
+ pass # Keep default name if we can't get more info
120
+
121
+ placeholders[str(placeholder_idx)] = placeholder_info
122
+
123
+ return {"index": index, "placeholders": placeholders}
124
+
125
+ except Exception as e:
126
+ print(f"Warning: Could not analyze layout {index}: {str(e)}")
127
+ return None
128
+
129
+ def _generate_aliases_template(self) -> Dict:
130
+ """Generate basic aliases template for user configuration."""
131
+ return {
132
+ "table": "Title and Content",
133
+ "bullets": "Title and Content",
134
+ "content": "Title and Content",
135
+ "title": "Title Slide",
136
+ }
137
+
138
+ def _validate_template(self, layouts: Dict) -> Dict:
139
+ """
140
+ Validate template layouts and detect common issues.
141
+
142
+ Args:
143
+ layouts: Dictionary of layout definitions
144
+
145
+ Returns:
146
+ Dictionary containing validation results with warnings and errors
147
+ """
148
+ validation_results = {"warnings": [], "errors": [], "layout_analysis": {}}
149
+
150
+ all_placeholder_names = []
151
+
152
+ for layout_name, layout_info in layouts.items():
153
+ placeholders = layout_info.get("placeholders", {})
154
+ layout_warnings = []
155
+ layout_errors = []
156
+
157
+ # Check for duplicate placeholder names within layout
158
+ placeholder_names = list(placeholders.values())
159
+ unique_names = set(placeholder_names)
160
+
161
+ if len(placeholder_names) != len(unique_names):
162
+ for name in unique_names:
163
+ count = placeholder_names.count(name)
164
+ if count > 1:
165
+ layout_errors.append(
166
+ f"Duplicate placeholder name '{name}' appears {count} times"
167
+ )
168
+
169
+ # Check for column layouts with inconsistent naming
170
+ if "column" in layout_name.lower():
171
+ self._validate_column_layout(
172
+ layout_name, placeholders, layout_warnings, layout_errors
173
+ )
174
+
175
+ # Check for comparison layouts
176
+ if "comparison" in layout_name.lower():
177
+ self._validate_comparison_layout(
178
+ layout_name, placeholders, layout_warnings, layout_errors
179
+ )
180
+
181
+ # Track all placeholder names across layouts
182
+ all_placeholder_names.extend(placeholder_names)
183
+
184
+ # Store layout-specific validation results
185
+ if layout_warnings or layout_errors:
186
+ validation_results["layout_analysis"][layout_name] = {
187
+ "warnings": layout_warnings,
188
+ "errors": layout_errors,
189
+ }
190
+ validation_results["warnings"].extend(
191
+ [f"{layout_name}: {w}" for w in layout_warnings]
192
+ )
193
+ validation_results["errors"].extend([f"{layout_name}: {e}" for e in layout_errors])
194
+
195
+ # Global validation checks
196
+ self._validate_global_consistency(all_placeholder_names, validation_results, layouts)
197
+
198
+ return validation_results
199
+
200
+ def _validate_column_layout(
201
+ self, layout_name: str, placeholders: Dict, warnings: list, errors: list
202
+ ) -> None:
203
+ """Validate column-based layouts for consistent naming patterns."""
204
+ placeholder_names = list(placeholders.values())
205
+
206
+ # Check for expected column patterns
207
+ col_titles = [
208
+ name for name in placeholder_names if "col" in name.lower() and "title" in name.lower()
209
+ ]
210
+ col_contents = [
211
+ name
212
+ for name in placeholder_names
213
+ if "col" in name.lower() and ("text" in name.lower() or "content" in name.lower())
214
+ ]
215
+
216
+ # Extract column numbers from names and track specific placeholders that need fixing
217
+ title_cols = []
218
+ content_cols = []
219
+ fix_suggestions = []
220
+
221
+ for name in col_titles:
222
+ try:
223
+ # Extract number from names like "Col 1 Title" or "Col 2 Title Placeholder"
224
+ parts = name.lower().split()
225
+ for i, part in enumerate(parts):
226
+ if part == "col" and i + 1 < len(parts):
227
+ col_num = int(parts[i + 1])
228
+ title_cols.append((col_num, name))
229
+ break
230
+ except (ValueError, IndexError):
231
+ warnings.append(f"Could not parse column number from title placeholder: '{name}'")
232
+
233
+ for name in col_contents:
234
+ try:
235
+ parts = name.lower().split()
236
+ for i, part in enumerate(parts):
237
+ if part == "col" and i + 1 < len(parts):
238
+ col_num = int(parts[i + 1])
239
+ content_cols.append((col_num, name))
240
+ break
241
+ except (ValueError, IndexError):
242
+ warnings.append(f"Could not parse column number from content placeholder: '{name}'")
243
+
244
+ # Check for consistent column numbering
245
+ title_nums = sorted([col[0] for col in title_cols])
246
+ content_nums = sorted([col[0] for col in content_cols])
247
+
248
+ # For layouts with titles, check title/content pairs match
249
+ if "title" in layout_name.lower() and title_cols:
250
+ if title_nums != content_nums:
251
+ # Find content placeholders that need fixing
252
+ expected_content_nums = title_nums
253
+ for expected_num in expected_content_nums:
254
+ if expected_num not in content_nums:
255
+ # Find what column number this content actually has
256
+ for actual_num, content_name in content_cols:
257
+ if actual_num != expected_num and expected_num not in [
258
+ c[0] for c in content_cols
259
+ ]:
260
+ # This content placeholder has wrong number
261
+ correct_name = content_name.replace(
262
+ f"Col {actual_num}", f"Col {expected_num}"
263
+ )
264
+ fix_suggestions.append(
265
+ f"In PowerPoint: Rename '{content_name}' → '{correct_name}'"
266
+ )
267
+ break
268
+
269
+ # Also check for duplicate column numbers in content
270
+ content_num_counts = {}
271
+ for num, name in content_cols:
272
+ if num not in content_num_counts:
273
+ content_num_counts[num] = []
274
+ content_num_counts[num].append(name)
275
+
276
+ for num, names in content_num_counts.items():
277
+ if len(names) > 1:
278
+ # Multiple content placeholders have same column number
279
+ for i, name in enumerate(
280
+ names[1:], start=2
281
+ ): # Skip first one, fix the rest
282
+ # Find the next available column number
283
+ next_num = num + i - 1
284
+ while next_num in content_num_counts and next_num != num:
285
+ next_num += 1
286
+ if next_num <= len(title_nums):
287
+ correct_name = name.replace(f"Col {num}", f"Col {next_num}")
288
+ fix_suggestions.append(
289
+ f"In PowerPoint: Rename '{name}' → '{correct_name}'"
290
+ )
291
+
292
+ error_msg = (
293
+ f"Column title numbers {title_nums} don't match content numbers {content_nums}"
294
+ )
295
+ if fix_suggestions:
296
+ error_msg += (
297
+ f". Required fixes in '{layout_name}' layout: {'; '.join(fix_suggestions)}"
298
+ )
299
+ errors.append(error_msg)
300
+
301
+ # Check for proper sequential numbering
302
+ if title_nums and title_nums != list(range(1, len(title_nums) + 1)):
303
+ warnings.append(
304
+ f"Column numbers not sequential: {title_nums} (expected: {list(range(1, len(title_nums) + 1))})"
305
+ )
306
+
307
+ # Check for proper content numbering in content-only layouts
308
+ elif content_cols:
309
+ if content_nums != list(range(1, len(content_nums) + 1)):
310
+ warnings.append(
311
+ f"Column numbers not sequential: {content_nums} (expected: {list(range(1, len(content_nums) + 1))})"
312
+ )
313
+
314
+ def _validate_comparison_layout(
315
+ self, layout_name: str, placeholders: Dict, warnings: list, errors: list
316
+ ) -> None:
317
+ """Validate comparison layouts for proper left/right structure."""
318
+ placeholder_names = list(placeholders.values())
319
+
320
+ text_placeholders = [
321
+ name
322
+ for name in placeholder_names
323
+ if "text" in name.lower() and "placeholder" in name.lower()
324
+ ]
325
+ content_placeholders = [
326
+ name
327
+ for name in placeholder_names
328
+ if "content" in name.lower() and "placeholder" in name.lower()
329
+ ]
330
+
331
+ if len(text_placeholders) < 2:
332
+ errors.append(
333
+ "Comparison layout should have at least 2 text placeholders for left/right titles"
334
+ )
335
+
336
+ if len(content_placeholders) < 2:
337
+ errors.append(
338
+ "Comparison layout should have at least 2 content placeholders for left/right content"
339
+ )
340
+
341
+ def _validate_global_consistency(
342
+ self, all_placeholder_names: list, validation_results: Dict, layouts: Dict = None
343
+ ) -> None:
344
+ """Validate global consistency across all layouts."""
345
+ # Track patterns by layout for more specific reporting
346
+ layout_patterns = {}
347
+ unique_patterns = set()
348
+
349
+ if layouts:
350
+ for layout_name, layout_info in layouts.items():
351
+ layout_patterns[layout_name] = set()
352
+ placeholders = layout_info.get("placeholders", {})
353
+
354
+ for name in placeholders.values():
355
+ if "placeholder" in str(name).lower():
356
+ # Extract pattern like "Text Placeholder", "Content Placeholder"
357
+ parts = str(name).split()
358
+ if len(parts) >= 2:
359
+ pattern = f"{parts[0]} {parts[1]}"
360
+ layout_patterns[layout_name].add(pattern)
361
+ unique_patterns.add(pattern)
362
+ else:
363
+ # Fallback to old method if layouts not provided
364
+ for name in all_placeholder_names:
365
+ if "placeholder" in name.lower():
366
+ parts = name.split()
367
+ if len(parts) >= 2:
368
+ pattern = f"{parts[0]} {parts[1]}"
369
+ unique_patterns.add(pattern)
370
+
371
+ # Check for mixed naming conventions
372
+ if len(unique_patterns) > 1:
373
+ warning_msg = (
374
+ f"Multiple placeholder naming patterns detected: {sorted(unique_patterns)}"
375
+ )
376
+
377
+ # Add specific layout information if available
378
+ if layouts and layout_patterns:
379
+ layouts_with_patterns = []
380
+ for layout_name, patterns in layout_patterns.items():
381
+ if patterns: # Only include layouts that have patterns
382
+ pattern_list = sorted(patterns)
383
+ layouts_with_patterns.append(f"{layout_name} ({', '.join(pattern_list)})")
384
+
385
+ if layouts_with_patterns:
386
+ warning_msg += f". Affected layouts: {'; '.join(layouts_with_patterns)}"
387
+
388
+ validation_results["warnings"].append(warning_msg)
389
+
390
+ def _print_validation_results(self, validation_results: Dict) -> None:
391
+ """Print validation results to console with formatting."""
392
+ if not validation_results["warnings"] and not validation_results["errors"]:
393
+ print("\n✓ Template validation passed - no issues detected")
394
+ return
395
+
396
+ print("\n" + "=" * 60)
397
+ print("TEMPLATE VALIDATION RESULTS")
398
+ print("=" * 60)
399
+
400
+ if validation_results["errors"]:
401
+ print(f"\n❌ ERRORS ({len(validation_results['errors'])}):")
402
+ for i, error in enumerate(validation_results["errors"], 1):
403
+ print(f" {i}. {error}")
404
+
405
+ if validation_results["warnings"]:
406
+ print(f"\n⚠️ WARNINGS ({len(validation_results['warnings'])}):")
407
+ for i, warning in enumerate(validation_results["warnings"], 1):
408
+ print(f" {i}. {warning}")
409
+
410
+ print("\n" + "=" * 60)
411
+ print("RECOMMENDED ACTIONS:")
412
+ print("=" * 60)
413
+
414
+ if validation_results["errors"]:
415
+ print("\n🔧 Fix these errors in your PowerPoint template:")
416
+ print(" • Open your PowerPoint template file")
417
+ print(" • Open View > Slide Master to edit the template layouts")
418
+ print(" • On Mac: Open Arrange > Selection Pane to see all placeholder objects")
419
+ print(" • Select the placeholder objects and rename them in the Selection Pane")
420
+ print(" • Rename placeholders as specified in the error messages above")
421
+ print(" • Ensure column layouts have consistent numbering (Col 1, Col 2, etc.)")
422
+ print(" • Verify comparison layouts have proper left/right structure")
423
+ print(" • Close Slide Master view when finished")
424
+
425
+ if validation_results["warnings"]:
426
+ print("\n💡 Consider these improvements:")
427
+ print(" • Use consistent placeholder naming patterns")
428
+ print(" • Ensure column numbers are sequential (1, 2, 3, 4)")
429
+ print(" • Follow naming conventions like 'Col 1 Title Placeholder 2'")
430
+
431
+ print("\n📝 After fixing placeholder names in PowerPoint, regenerate the template mapping:")
432
+ print(" python src/deckbuilder/cli_tools.py analyze default --verbose")
433
+ print("\n💡 The analyzer will show ✅ validation passed when all issues are resolved")
434
+ print("=" * 60)
435
+
436
+ def _save_json_mapping(self, template_name: str, data: Dict) -> None:
437
+ """
438
+ Save the JSON mapping to output folder as .g.json file.
439
+
440
+ Args:
441
+ template_name: Base name of the template (without extension)
442
+ data: Dictionary to save as JSON
443
+ """
444
+ if not self.output_folder:
445
+ print("Warning: DECK_OUTPUT_FOLDER not set, saving to current directory")
446
+ output_folder = "."
447
+ else:
448
+ output_folder = self.output_folder
449
+
450
+ # Ensure output folder exists
451
+ os.makedirs(output_folder, exist_ok=True)
452
+
453
+ # Create output filename
454
+ output_filename = f"{template_name}.g.json"
455
+ output_path = os.path.join(output_folder, output_filename)
456
+
457
+ # Save JSON (overwrite if exists)
458
+ with open(output_path, "w", encoding="utf-8") as f:
459
+ json.dump(data, f, indent=2, ensure_ascii=False)
460
+
461
+ print(f"\nTemplate mapping saved to: {output_path}")
462
+ print(f"Rename to {template_name}.json when ready to use with deckbuilder")
463
+
464
+
465
+ def analyze_pptx_template(template_name: str) -> Dict:
466
+ """
467
+ Convenience function to analyze a PowerPoint template.
468
+
469
+ Args:
470
+ template_name: Name of the template file (with or without .pptx extension)
471
+
472
+ Returns:
473
+ Dictionary containing raw template structure for user mapping
474
+ """
475
+ analyzer = TemplateAnalyzer()
476
+ return analyzer.analyze_pptx_template(template_name)
477
+
478
+
479
+ def test_with_default_template():
480
+ """Test the analyzer with the default template."""
481
+ try:
482
+ result = analyze_pptx_template("default")
483
+ print("Raw Template Structure:")
484
+ print(json.dumps(result, indent=2))
485
+ return result
486
+ except Exception as e:
487
+ print(f"Error analyzing template: {str(e)}")
488
+ return None
489
+
490
+
491
+ if __name__ == "__main__":
492
+ test_with_default_template()