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,541 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Robust Convention-Based Naming System for PowerPoint Template Placeholders
|
4
|
+
|
5
|
+
Provides standardized, semantic placeholder naming using multi-tier detection
|
6
|
+
with graceful fallbacks for template variations and missing elements.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import re
|
10
|
+
from dataclasses import dataclass
|
11
|
+
from typing import Dict, List, Optional
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class PlaceholderContext:
|
16
|
+
"""Context information for placeholder naming decisions"""
|
17
|
+
|
18
|
+
layout_name: str
|
19
|
+
placeholder_idx: str
|
20
|
+
placeholder_type: Optional[str] = None
|
21
|
+
total_placeholders: int = 0
|
22
|
+
existing_names: List[str] = None
|
23
|
+
powerpoint_type: Optional[int] = None
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class SemanticInfo:
|
28
|
+
"""Semantic information for a placeholder"""
|
29
|
+
|
30
|
+
content_type: str
|
31
|
+
position: str
|
32
|
+
index: int = 1
|
33
|
+
confidence: float = 1.0 # How confident we are in this detection
|
34
|
+
|
35
|
+
|
36
|
+
class NamingConvention:
|
37
|
+
"""
|
38
|
+
Robust convention-based placeholder naming system with multi-tier detection.
|
39
|
+
|
40
|
+
Handles template variations gracefully and provides semantic names even when
|
41
|
+
footer elements or other placeholders are missing.
|
42
|
+
|
43
|
+
Format: {ContentType}_{Position}_{Index}
|
44
|
+
Example: title_top_1, content_col1_1, date_footer_1
|
45
|
+
"""
|
46
|
+
|
47
|
+
def __init__(self):
|
48
|
+
self.layout_mappings = self._build_layout_mappings()
|
49
|
+
self.powerpoint_type_map = self._build_powerpoint_type_map()
|
50
|
+
self.universal_patterns = self._build_universal_patterns()
|
51
|
+
|
52
|
+
def generate_placeholder_name(self, context: PlaceholderContext) -> str:
|
53
|
+
"""
|
54
|
+
Generate standardized placeholder name using multi-tier detection.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
context: PlaceholderContext with layout and placeholder information
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
Standardized placeholder name with confidence score
|
61
|
+
"""
|
62
|
+
semantic_info = self._detect_semantic_info(context)
|
63
|
+
|
64
|
+
# Generate name from semantic info
|
65
|
+
if semantic_info.position and semantic_info.position != "main":
|
66
|
+
return f"{semantic_info.content_type}_{semantic_info.position}_{semantic_info.index}"
|
67
|
+
else:
|
68
|
+
return f"{semantic_info.content_type}_{semantic_info.index}"
|
69
|
+
|
70
|
+
def _detect_semantic_info(self, context: PlaceholderContext) -> SemanticInfo:
|
71
|
+
"""Multi-tier detection with confidence scoring"""
|
72
|
+
|
73
|
+
# Tier 1: Exact layout + index mapping (highest confidence)
|
74
|
+
if semantic_info := self._tier1_exact_mapping(context):
|
75
|
+
semantic_info.confidence = 1.0
|
76
|
+
return semantic_info
|
77
|
+
|
78
|
+
# Tier 2: PowerPoint built-in types (high confidence)
|
79
|
+
if semantic_info := self._tier2_powerpoint_types(context):
|
80
|
+
semantic_info.confidence = 0.9
|
81
|
+
return semantic_info
|
82
|
+
|
83
|
+
# Tier 3: Universal patterns (medium confidence)
|
84
|
+
if semantic_info := self._tier3_universal_patterns(context):
|
85
|
+
semantic_info.confidence = 0.7
|
86
|
+
return semantic_info
|
87
|
+
|
88
|
+
# Tier 4: Layout-based inference (lower confidence)
|
89
|
+
if semantic_info := self._tier4_layout_inference(context):
|
90
|
+
semantic_info.confidence = 0.5
|
91
|
+
return semantic_info
|
92
|
+
|
93
|
+
# Tier 5: Generic fallback (lowest confidence)
|
94
|
+
return self._tier5_generic_fallback(context)
|
95
|
+
|
96
|
+
def _tier1_exact_mapping(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
|
97
|
+
"""Tier 1: Exact layout + index mapping"""
|
98
|
+
layout_key = self._normalize_layout_name(context.layout_name)
|
99
|
+
|
100
|
+
if layout_key not in self.layout_mappings:
|
101
|
+
return None
|
102
|
+
|
103
|
+
mapping = self.layout_mappings[layout_key]
|
104
|
+
idx = context.placeholder_idx
|
105
|
+
|
106
|
+
# Check required placeholders first
|
107
|
+
if idx in mapping.get("required", {}):
|
108
|
+
info = mapping["required"][idx]
|
109
|
+
return SemanticInfo(content_type=info["type"], position=info["position"], index=1)
|
110
|
+
|
111
|
+
# Check optional placeholders (like footer elements)
|
112
|
+
if idx in mapping.get("optional", {}):
|
113
|
+
info = mapping["optional"][idx]
|
114
|
+
return SemanticInfo(content_type=info["type"], position=info["position"], index=1)
|
115
|
+
|
116
|
+
return None
|
117
|
+
|
118
|
+
def _tier2_powerpoint_types(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
|
119
|
+
"""Tier 2: PowerPoint built-in placeholder types"""
|
120
|
+
if context.powerpoint_type is None:
|
121
|
+
return None
|
122
|
+
|
123
|
+
type_mapping = self.powerpoint_type_map.get(context.powerpoint_type)
|
124
|
+
if not type_mapping:
|
125
|
+
return None
|
126
|
+
|
127
|
+
# Determine position based on layout and index
|
128
|
+
position = self._infer_position_from_layout(context)
|
129
|
+
|
130
|
+
return SemanticInfo(content_type=type_mapping["type"], position=position, index=1)
|
131
|
+
|
132
|
+
def _tier3_universal_patterns(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
|
133
|
+
"""Tier 3: Universal patterns that work across layouts"""
|
134
|
+
idx = context.placeholder_idx
|
135
|
+
|
136
|
+
# Universal pattern: Index 0 is almost always the main title
|
137
|
+
if idx == "0":
|
138
|
+
return SemanticInfo(content_type="title", position="top", index=1)
|
139
|
+
|
140
|
+
# Universal pattern: Footer elements (may not exist in all templates)
|
141
|
+
if idx in ["10", "11", "12"]:
|
142
|
+
footer_types = {"10": "date", "11": "footer", "12": "slide_number"}
|
143
|
+
return SemanticInfo(content_type=footer_types[idx], position="footer", index=1)
|
144
|
+
|
145
|
+
# Universal pattern: Index 1 is often main content
|
146
|
+
if idx == "1":
|
147
|
+
return SemanticInfo(content_type="content", position="main", index=1)
|
148
|
+
|
149
|
+
return None
|
150
|
+
|
151
|
+
def _tier4_layout_inference(self, context: PlaceholderContext) -> Optional[SemanticInfo]:
|
152
|
+
"""Tier 4: Infer from layout structure and index patterns"""
|
153
|
+
layout_name = context.layout_name.lower()
|
154
|
+
idx = int(context.placeholder_idx) if context.placeholder_idx.isdigit() else 0
|
155
|
+
|
156
|
+
# Column layouts: Infer column position from index
|
157
|
+
if "column" in layout_name:
|
158
|
+
return self._infer_column_semantic(context, idx)
|
159
|
+
|
160
|
+
# Comparison layouts: Infer left/right from index
|
161
|
+
if "comparison" in layout_name:
|
162
|
+
return self._infer_comparison_semantic(context, idx)
|
163
|
+
|
164
|
+
# Picture layouts: Infer picture vs caption
|
165
|
+
if "picture" in layout_name or "caption" in layout_name:
|
166
|
+
return self._infer_picture_semantic(context, idx)
|
167
|
+
|
168
|
+
# Agenda/list layouts: Infer item position
|
169
|
+
if "agenda" in layout_name or "6" in layout_name:
|
170
|
+
return self._infer_agenda_semantic(context, idx)
|
171
|
+
|
172
|
+
return None
|
173
|
+
|
174
|
+
def _tier5_generic_fallback(self, context: PlaceholderContext) -> SemanticInfo:
|
175
|
+
"""Tier 5: Generic fallback naming"""
|
176
|
+
return SemanticInfo(
|
177
|
+
content_type="content",
|
178
|
+
position="main",
|
179
|
+
index=int(context.placeholder_idx) if context.placeholder_idx.isdigit() else 1,
|
180
|
+
confidence=0.1,
|
181
|
+
)
|
182
|
+
|
183
|
+
def _infer_column_semantic(
|
184
|
+
self, context: PlaceholderContext, idx: int
|
185
|
+
) -> Optional[SemanticInfo]:
|
186
|
+
"""Infer semantic info for column layouts"""
|
187
|
+
# Four columns: title=13,15,17,19 content=14,16,18,20
|
188
|
+
# Three columns: title=13,15,17 content=14,16,18
|
189
|
+
|
190
|
+
if idx in [13, 14]: # Column 1
|
191
|
+
content_type = "title" if idx == 13 else "content"
|
192
|
+
return SemanticInfo(content_type=content_type, position="col1", index=1)
|
193
|
+
elif idx in [15, 16]: # Column 2
|
194
|
+
content_type = "title" if idx == 15 else "content"
|
195
|
+
return SemanticInfo(content_type=content_type, position="col2", index=1)
|
196
|
+
elif idx in [17, 18]: # Column 3
|
197
|
+
content_type = "title" if idx == 17 else "content"
|
198
|
+
return SemanticInfo(content_type=content_type, position="col3", index=1)
|
199
|
+
elif idx in [19, 20]: # Column 4 (four columns only)
|
200
|
+
content_type = "title" if idx == 19 else "content"
|
201
|
+
return SemanticInfo(content_type=content_type, position="col4", index=1)
|
202
|
+
|
203
|
+
return None
|
204
|
+
|
205
|
+
def _infer_comparison_semantic(
|
206
|
+
self, context: PlaceholderContext, idx: int
|
207
|
+
) -> Optional[SemanticInfo]:
|
208
|
+
"""Infer semantic info for comparison layouts"""
|
209
|
+
if idx in [1, 2]: # Left side
|
210
|
+
content_type = "title" if idx == 1 else "content"
|
211
|
+
return SemanticInfo(content_type=content_type, position="left", index=1)
|
212
|
+
elif idx in [3, 4]: # Right side
|
213
|
+
content_type = "title" if idx == 3 else "content"
|
214
|
+
return SemanticInfo(content_type=content_type, position="right", index=1)
|
215
|
+
|
216
|
+
return None
|
217
|
+
|
218
|
+
def _infer_picture_semantic(
|
219
|
+
self, context: PlaceholderContext, idx: int
|
220
|
+
) -> Optional[SemanticInfo]:
|
221
|
+
"""Infer semantic info for picture layouts"""
|
222
|
+
if idx == 1:
|
223
|
+
return SemanticInfo(content_type="image", position="main", index=1)
|
224
|
+
elif idx == 2:
|
225
|
+
return SemanticInfo(content_type="text", position="caption", index=1)
|
226
|
+
|
227
|
+
return None
|
228
|
+
|
229
|
+
def _infer_agenda_semantic(
|
230
|
+
self, context: PlaceholderContext, idx: int
|
231
|
+
) -> Optional[SemanticInfo]:
|
232
|
+
"""Infer semantic info for agenda layouts"""
|
233
|
+
# Agenda items mapping based on actual template structure
|
234
|
+
agenda_mapping = {
|
235
|
+
28: ("number", "item1"),
|
236
|
+
18: ("content", "item1"),
|
237
|
+
29: ("number", "item2"),
|
238
|
+
20: ("content", "item2"),
|
239
|
+
30: ("number", "item3"),
|
240
|
+
22: ("content", "item3"),
|
241
|
+
31: ("number", "item4"),
|
242
|
+
19: ("content", "item4"),
|
243
|
+
32: ("number", "item5"),
|
244
|
+
21: ("content", "item5"),
|
245
|
+
33: ("number", "item6"),
|
246
|
+
34: ("content", "item6"),
|
247
|
+
}
|
248
|
+
|
249
|
+
if idx in agenda_mapping:
|
250
|
+
content_type, position = agenda_mapping[idx]
|
251
|
+
return SemanticInfo(content_type=content_type, position=position, index=1)
|
252
|
+
|
253
|
+
return None
|
254
|
+
|
255
|
+
def _infer_position_from_layout(self, context: PlaceholderContext) -> str:
|
256
|
+
"""Infer position based on layout type and index"""
|
257
|
+
idx = context.placeholder_idx
|
258
|
+
layout_name = context.layout_name.lower()
|
259
|
+
|
260
|
+
if idx == "0":
|
261
|
+
return "top"
|
262
|
+
elif idx in ["10", "11", "12"]:
|
263
|
+
return "footer"
|
264
|
+
elif "column" in layout_name:
|
265
|
+
return "col1" # Default to first column
|
266
|
+
elif "comparison" in layout_name:
|
267
|
+
return "left" # Default to left side
|
268
|
+
else:
|
269
|
+
return "main"
|
270
|
+
|
271
|
+
def _normalize_layout_name(self, layout_name: str) -> str:
|
272
|
+
"""Normalize layout name for mapping lookup"""
|
273
|
+
return layout_name.lower().replace(" ", "_")
|
274
|
+
|
275
|
+
def _build_layout_mappings(self) -> Dict[str, Dict]:
|
276
|
+
"""Build comprehensive layout mappings with required/optional elements"""
|
277
|
+
return {
|
278
|
+
"title_slide": {
|
279
|
+
"required": {
|
280
|
+
"0": {"type": "title", "position": "top"},
|
281
|
+
"1": {"type": "subtitle", "position": "main"},
|
282
|
+
},
|
283
|
+
"optional": {
|
284
|
+
"10": {"type": "date", "position": "footer"},
|
285
|
+
"11": {"type": "footer", "position": "footer"},
|
286
|
+
"12": {"type": "slide_number", "position": "footer"},
|
287
|
+
},
|
288
|
+
},
|
289
|
+
"title_and_content": {
|
290
|
+
"required": {
|
291
|
+
"0": {"type": "title", "position": "top"},
|
292
|
+
"1": {"type": "content", "position": "main"},
|
293
|
+
},
|
294
|
+
"optional": {
|
295
|
+
"10": {"type": "date", "position": "footer"},
|
296
|
+
"11": {"type": "footer", "position": "footer"},
|
297
|
+
"12": {"type": "slide_number", "position": "footer"},
|
298
|
+
},
|
299
|
+
},
|
300
|
+
"section_header": {
|
301
|
+
"required": {
|
302
|
+
"0": {"type": "title", "position": "top"},
|
303
|
+
"1": {"type": "text", "position": "main"},
|
304
|
+
},
|
305
|
+
"optional": {
|
306
|
+
"10": {"type": "date", "position": "footer"},
|
307
|
+
"11": {"type": "footer", "position": "footer"},
|
308
|
+
"12": {"type": "slide_number", "position": "footer"},
|
309
|
+
},
|
310
|
+
},
|
311
|
+
"two_content": {
|
312
|
+
"required": {
|
313
|
+
"0": {"type": "title", "position": "top"},
|
314
|
+
"1": {"type": "content", "position": "left"},
|
315
|
+
"2": {"type": "content", "position": "right"},
|
316
|
+
},
|
317
|
+
"optional": {
|
318
|
+
"10": {"type": "date", "position": "footer"},
|
319
|
+
"11": {"type": "footer", "position": "footer"},
|
320
|
+
"12": {"type": "slide_number", "position": "footer"},
|
321
|
+
},
|
322
|
+
},
|
323
|
+
"comparison": {
|
324
|
+
"required": {
|
325
|
+
"0": {"type": "title", "position": "top"},
|
326
|
+
"1": {"type": "title", "position": "left"},
|
327
|
+
"2": {"type": "content", "position": "left"},
|
328
|
+
"3": {"type": "title", "position": "right"},
|
329
|
+
"4": {"type": "content", "position": "right"},
|
330
|
+
},
|
331
|
+
"optional": {
|
332
|
+
"10": {"type": "date", "position": "footer"},
|
333
|
+
"11": {"type": "footer", "position": "footer"},
|
334
|
+
"12": {"type": "slide_number", "position": "footer"},
|
335
|
+
},
|
336
|
+
},
|
337
|
+
"three_columns_with_titles": {
|
338
|
+
"required": {
|
339
|
+
"0": {"type": "title", "position": "top"},
|
340
|
+
"13": {"type": "title", "position": "col1"},
|
341
|
+
"14": {"type": "content", "position": "col1"},
|
342
|
+
"15": {"type": "title", "position": "col2"},
|
343
|
+
"16": {"type": "content", "position": "col2"},
|
344
|
+
"17": {"type": "title", "position": "col3"},
|
345
|
+
"18": {"type": "content", "position": "col3"},
|
346
|
+
},
|
347
|
+
"optional": {
|
348
|
+
"10": {"type": "date", "position": "footer"},
|
349
|
+
"11": {"type": "footer", "position": "footer"},
|
350
|
+
"12": {"type": "slide_number", "position": "footer"},
|
351
|
+
},
|
352
|
+
},
|
353
|
+
"four_columns_with_titles": {
|
354
|
+
"required": {
|
355
|
+
"0": {"type": "title", "position": "top"},
|
356
|
+
"13": {"type": "title", "position": "col1"},
|
357
|
+
"14": {"type": "content", "position": "col1"},
|
358
|
+
"15": {"type": "title", "position": "col2"},
|
359
|
+
"16": {"type": "content", "position": "col2"},
|
360
|
+
"17": {"type": "title", "position": "col3"},
|
361
|
+
"18": {"type": "content", "position": "col3"},
|
362
|
+
"19": {"type": "title", "position": "col4"},
|
363
|
+
"20": {"type": "content", "position": "col4"},
|
364
|
+
},
|
365
|
+
"optional": {
|
366
|
+
"10": {"type": "date", "position": "footer"},
|
367
|
+
"11": {"type": "footer", "position": "footer"},
|
368
|
+
"12": {"type": "slide_number", "position": "footer"},
|
369
|
+
},
|
370
|
+
},
|
371
|
+
"picture_with_caption": {
|
372
|
+
"required": {
|
373
|
+
"0": {"type": "title", "position": "top"},
|
374
|
+
"1": {"type": "image", "position": "main"},
|
375
|
+
"2": {"type": "text", "position": "caption"},
|
376
|
+
},
|
377
|
+
"optional": {
|
378
|
+
"10": {"type": "date", "position": "footer"},
|
379
|
+
"11": {"type": "footer", "position": "footer"},
|
380
|
+
"12": {"type": "slide_number", "position": "footer"},
|
381
|
+
},
|
382
|
+
},
|
383
|
+
# Additional layouts can be added here
|
384
|
+
}
|
385
|
+
|
386
|
+
def _build_powerpoint_type_map(self) -> Dict[int, Dict[str, str]]:
|
387
|
+
"""Build mapping of PowerPoint placeholder types to semantic types"""
|
388
|
+
# Note: These constants would come from python-pptx PP_PLACEHOLDER enum
|
389
|
+
return {
|
390
|
+
1: {"type": "title"}, # PP_PLACEHOLDER.TITLE
|
391
|
+
2: {"type": "content"}, # PP_PLACEHOLDER.BODY
|
392
|
+
3: {"type": "text"}, # PP_PLACEHOLDER.TEXT
|
393
|
+
4: {"type": "date"}, # PP_PLACEHOLDER.DATE
|
394
|
+
5: {"type": "slide_number"}, # PP_PLACEHOLDER.SLIDE_NUMBER
|
395
|
+
6: {"type": "footer"}, # PP_PLACEHOLDER.FOOTER
|
396
|
+
7: {"type": "subtitle"}, # PP_PLACEHOLDER.SUBTITLE
|
397
|
+
18: {"type": "image"}, # PP_PLACEHOLDER.PICTURE
|
398
|
+
}
|
399
|
+
|
400
|
+
def _build_universal_patterns(self) -> Dict[str, Dict]:
|
401
|
+
"""Build universal patterns that work across all layouts"""
|
402
|
+
return {
|
403
|
+
"title_indices": ["0"],
|
404
|
+
"footer_indices": ["10", "11", "12"],
|
405
|
+
"main_content_indices": ["1"],
|
406
|
+
}
|
407
|
+
|
408
|
+
def convert_template_to_conventions(self, template_analysis: Dict) -> Dict:
|
409
|
+
"""
|
410
|
+
Convert existing template analysis to use convention-based naming.
|
411
|
+
|
412
|
+
Args:
|
413
|
+
template_analysis: Template analysis from CLI tools
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
Updated analysis with convention-based placeholder names
|
417
|
+
"""
|
418
|
+
converted_analysis = template_analysis.copy()
|
419
|
+
layouts = converted_analysis.get("layouts", {})
|
420
|
+
|
421
|
+
for layout_name, layout_info in layouts.items():
|
422
|
+
placeholders = layout_info.get("placeholders", {})
|
423
|
+
converted_placeholders = {}
|
424
|
+
|
425
|
+
for idx, current_name in placeholders.items():
|
426
|
+
context = PlaceholderContext(
|
427
|
+
layout_name=layout_name,
|
428
|
+
placeholder_idx=idx,
|
429
|
+
total_placeholders=len(placeholders),
|
430
|
+
existing_names=list(placeholders.values()),
|
431
|
+
)
|
432
|
+
|
433
|
+
convention_name = self.generate_placeholder_name(context)
|
434
|
+
converted_placeholders[idx] = convention_name
|
435
|
+
|
436
|
+
layout_info["placeholders"] = converted_placeholders
|
437
|
+
|
438
|
+
return converted_analysis
|
439
|
+
|
440
|
+
def validate_naming_consistency(self, template_analysis: Dict) -> Dict:
|
441
|
+
"""
|
442
|
+
Validate naming consistency and detect improvement opportunities.
|
443
|
+
|
444
|
+
Returns:
|
445
|
+
Validation results with consistency scores and recommendations
|
446
|
+
"""
|
447
|
+
layouts = template_analysis.get("layouts", {})
|
448
|
+
validation_results = {
|
449
|
+
"consistency_score": 0.0,
|
450
|
+
"convention_compliance": 0.0,
|
451
|
+
"issues": [],
|
452
|
+
"recommendations": [],
|
453
|
+
"pattern_analysis": {},
|
454
|
+
}
|
455
|
+
|
456
|
+
# Analyze current naming patterns
|
457
|
+
all_names = []
|
458
|
+
convention_names = []
|
459
|
+
|
460
|
+
for layout_name, layout_info in layouts.items():
|
461
|
+
placeholders = layout_info.get("placeholders", {})
|
462
|
+
all_names.extend(placeholders.values())
|
463
|
+
|
464
|
+
# Generate convention names for comparison
|
465
|
+
for idx in placeholders.keys():
|
466
|
+
context = PlaceholderContext(
|
467
|
+
layout_name=layout_name,
|
468
|
+
placeholder_idx=idx,
|
469
|
+
total_placeholders=len(placeholders),
|
470
|
+
)
|
471
|
+
convention_name = self.generate_placeholder_name(context)
|
472
|
+
convention_names.append(convention_name)
|
473
|
+
|
474
|
+
# Calculate compliance scores
|
475
|
+
if all_names:
|
476
|
+
convention_compliant = sum(
|
477
|
+
1 for name in all_names if self._follows_convention_format(name)
|
478
|
+
)
|
479
|
+
validation_results["convention_compliance"] = convention_compliant / len(all_names)
|
480
|
+
|
481
|
+
# Generate recommendations
|
482
|
+
if validation_results["convention_compliance"] < 0.8:
|
483
|
+
validation_results["recommendations"].append(
|
484
|
+
"Consider using 'enhance --use-conventions' to apply standardized naming"
|
485
|
+
)
|
486
|
+
|
487
|
+
# Detect naming patterns
|
488
|
+
patterns = {}
|
489
|
+
for name in all_names:
|
490
|
+
pattern = self._extract_naming_pattern(name)
|
491
|
+
patterns[pattern] = patterns.get(pattern, 0) + 1
|
492
|
+
|
493
|
+
validation_results["pattern_analysis"] = patterns
|
494
|
+
|
495
|
+
return validation_results
|
496
|
+
|
497
|
+
def _follows_convention_format(self, placeholder_name: str) -> bool:
|
498
|
+
"""Check if placeholder name follows convention format"""
|
499
|
+
# Convention format: {ContentType}_{Position}_{Index} or {ContentType}_{Index}
|
500
|
+
pattern = r"^(title|subtitle|content|text|image|number|date|footer|slide_number)_([a-z0-9]+_)?\d+$"
|
501
|
+
return bool(re.match(pattern, placeholder_name.lower()))
|
502
|
+
|
503
|
+
def _extract_naming_pattern(self, placeholder_name: str) -> str:
|
504
|
+
"""Extract naming pattern from placeholder name"""
|
505
|
+
# Simplify name to pattern (e.g., "Title 1" -> "Title N")
|
506
|
+
return re.sub(r"\d+", "N", placeholder_name)
|
507
|
+
|
508
|
+
|
509
|
+
def test_naming_convention():
|
510
|
+
"""Test the robust naming convention system"""
|
511
|
+
convention = NamingConvention()
|
512
|
+
|
513
|
+
test_cases = [
|
514
|
+
# Test Four Columns layout
|
515
|
+
PlaceholderContext("Four Columns With Titles", "13"),
|
516
|
+
PlaceholderContext("Four Columns With Titles", "14"),
|
517
|
+
PlaceholderContext("Four Columns With Titles", "0"),
|
518
|
+
PlaceholderContext("Four Columns With Titles", "10"), # Optional footer
|
519
|
+
# Test unknown layout
|
520
|
+
PlaceholderContext("Custom Layout", "1"),
|
521
|
+
PlaceholderContext("Custom Layout", "5"),
|
522
|
+
# Test minimal template (no footer)
|
523
|
+
PlaceholderContext("Simple Title", "0"),
|
524
|
+
PlaceholderContext("Simple Title", "1"),
|
525
|
+
]
|
526
|
+
|
527
|
+
print("Testing Robust Naming Convention:")
|
528
|
+
print("=" * 50)
|
529
|
+
|
530
|
+
for context in test_cases:
|
531
|
+
name = convention.generate_placeholder_name(context)
|
532
|
+
semantic_info = convention._detect_semantic_info(context)
|
533
|
+
print(f"Layout: {context.layout_name}")
|
534
|
+
print(f"Index: {context.placeholder_idx}")
|
535
|
+
print(f"Generated: {name}")
|
536
|
+
print(f"Confidence: {semantic_info.confidence:.1f}")
|
537
|
+
print("-" * 30)
|
538
|
+
|
539
|
+
|
540
|
+
if __name__ == "__main__":
|
541
|
+
test_naming_convention()
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"""
|
2
|
+
PowerPoint Placeholder Type Constants and Mappings
|
3
|
+
|
4
|
+
This module defines semantic groupings of PowerPoint placeholder types for
|
5
|
+
generic content placement without hardcoding layout names. This allows the
|
6
|
+
deckbuilder to work with any PowerPoint template by detecting placeholder
|
7
|
+
types rather than relying on specific layout configurations.
|
8
|
+
|
9
|
+
Based on python-pptx PP_PLACEHOLDER_TYPE enumeration.
|
10
|
+
"""
|
11
|
+
|
12
|
+
from pptx.enum.shapes import PP_PLACEHOLDER_TYPE
|
13
|
+
|
14
|
+
# Title-related placeholders - for slide titles and headings
|
15
|
+
TITLE_PLACEHOLDERS = {
|
16
|
+
PP_PLACEHOLDER_TYPE.TITLE, # TITLE (1) - Standard slide title
|
17
|
+
PP_PLACEHOLDER_TYPE.CENTER_TITLE, # CENTER_TITLE (3) - Centered title (title slides)
|
18
|
+
PP_PLACEHOLDER_TYPE.VERTICAL_TITLE, # VERTICAL_TITLE (5) - Vertical orientation title
|
19
|
+
}
|
20
|
+
|
21
|
+
# Subtitle placeholders - for slide subtitles (typically on title slides)
|
22
|
+
SUBTITLE_PLACEHOLDERS = {PP_PLACEHOLDER_TYPE.SUBTITLE} # SUBTITLE (4) - Subtitle text
|
23
|
+
|
24
|
+
# Main content placeholders - for primary slide content, bullets, paragraphs
|
25
|
+
CONTENT_PLACEHOLDERS = {
|
26
|
+
PP_PLACEHOLDER_TYPE.BODY, # BODY (2) - Main content area
|
27
|
+
PP_PLACEHOLDER_TYPE.VERTICAL_BODY, # VERTICAL_BODY (6) - Vertical text content
|
28
|
+
}
|
29
|
+
|
30
|
+
# Media and object placeholders - for rich content like images, charts, tables
|
31
|
+
MEDIA_PLACEHOLDERS = {
|
32
|
+
PP_PLACEHOLDER_TYPE.PICTURE, # PICTURE (18) - Image placeholders
|
33
|
+
PP_PLACEHOLDER_TYPE.CHART, # CHART (8) - Chart/graph placeholders
|
34
|
+
PP_PLACEHOLDER_TYPE.TABLE, # TABLE (12) - Data table placeholders
|
35
|
+
PP_PLACEHOLDER_TYPE.MEDIA_CLIP, # MEDIA_CLIP (10) - Video/audio content
|
36
|
+
PP_PLACEHOLDER_TYPE.OBJECT, # OBJECT (7) - Generic embedded objects
|
37
|
+
PP_PLACEHOLDER_TYPE.VERTICAL_OBJECT, # VERTICAL_OBJECT (17) - Vertical objects
|
38
|
+
}
|
39
|
+
|
40
|
+
# Layout and metadata placeholders - for slide decorations and information
|
41
|
+
LAYOUT_PLACEHOLDERS = {
|
42
|
+
PP_PLACEHOLDER_TYPE.HEADER, # HEADER (14) - Page header text
|
43
|
+
PP_PLACEHOLDER_TYPE.FOOTER, # FOOTER (15) - Page footer text
|
44
|
+
PP_PLACEHOLDER_TYPE.DATE, # DATE (16) - Date/timestamp
|
45
|
+
PP_PLACEHOLDER_TYPE.SLIDE_NUMBER, # SLIDE_NUMBER (13) - Slide numbering
|
46
|
+
}
|
47
|
+
|
48
|
+
# Specialized placeholders - for specific content types
|
49
|
+
SPECIAL_PLACEHOLDERS = {
|
50
|
+
PP_PLACEHOLDER_TYPE.ORG_CHART, # ORG_CHART (11) - Organization charts
|
51
|
+
PP_PLACEHOLDER_TYPE.BITMAP, # BITMAP (9) - Bitmap images
|
52
|
+
PP_PLACEHOLDER_TYPE.SLIDE_IMAGE, # SLIDE_IMAGE (101) - Slide thumbnail images
|
53
|
+
PP_PLACEHOLDER_TYPE.MIXED, # MIXED (-2) - Mixed content types
|
54
|
+
}
|
55
|
+
|
56
|
+
# All placeholder types grouped by semantic function
|
57
|
+
ALL_PLACEHOLDER_GROUPS = {
|
58
|
+
"title": TITLE_PLACEHOLDERS,
|
59
|
+
"subtitle": SUBTITLE_PLACEHOLDERS,
|
60
|
+
"content": CONTENT_PLACEHOLDERS,
|
61
|
+
"media": MEDIA_PLACEHOLDERS,
|
62
|
+
"layout": LAYOUT_PLACEHOLDERS,
|
63
|
+
"special": SPECIAL_PLACEHOLDERS,
|
64
|
+
}
|
65
|
+
|
66
|
+
|
67
|
+
def get_placeholder_category(placeholder_type):
|
68
|
+
"""
|
69
|
+
Determine the semantic category of a placeholder type.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
placeholder_type: PP_PLACEHOLDER_TYPE enum value
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
str: Category name ('title', 'subtitle', 'content', 'media', 'layout', 'special')
|
76
|
+
None: If placeholder type is not recognized
|
77
|
+
"""
|
78
|
+
for category, types in ALL_PLACEHOLDER_GROUPS.items():
|
79
|
+
if placeholder_type in types:
|
80
|
+
return category
|
81
|
+
return None
|
82
|
+
|
83
|
+
|
84
|
+
def is_title_placeholder(placeholder_type):
|
85
|
+
"""Check if placeholder type is for titles."""
|
86
|
+
return placeholder_type in TITLE_PLACEHOLDERS
|
87
|
+
|
88
|
+
|
89
|
+
def is_subtitle_placeholder(placeholder_type):
|
90
|
+
"""Check if placeholder type is for subtitles."""
|
91
|
+
return placeholder_type in SUBTITLE_PLACEHOLDERS
|
92
|
+
|
93
|
+
|
94
|
+
def is_content_placeholder(placeholder_type):
|
95
|
+
"""Check if placeholder type is for main content."""
|
96
|
+
return placeholder_type in CONTENT_PLACEHOLDERS
|
97
|
+
|
98
|
+
|
99
|
+
def is_media_placeholder(placeholder_type):
|
100
|
+
"""Check if placeholder type is for media/objects."""
|
101
|
+
return placeholder_type in MEDIA_PLACEHOLDERS
|