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,22 @@
1
+ """
2
+ Deckbuilder - PowerPoint Presentation Engine
3
+
4
+ Core presentation generation engine with template support and
5
+ structured frontmatter processing.
6
+ """
7
+
8
+ from .engine import Deckbuilder, get_deckbuilder_client
9
+ from .structured_frontmatter import (
10
+ StructuredFrontmatterConverter,
11
+ StructuredFrontmatterRegistry,
12
+ StructuredFrontmatterValidator,
13
+ )
14
+
15
+ __version__ = "1.0.0"
16
+ __all__ = [
17
+ "Deckbuilder",
18
+ "get_deckbuilder_client",
19
+ "StructuredFrontmatterRegistry",
20
+ "StructuredFrontmatterConverter",
21
+ "StructuredFrontmatterValidator",
22
+ ]
deckbuilder/cli.py ADDED
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Deckbuilder CLI - Standalone Command Line Interface
4
+
5
+ Complete command-line interface for Deckbuilder presentation generation,
6
+ template management, and PlaceKitten image processing. Designed for
7
+ local development and standalone usage without MCP server dependency.
8
+
9
+ Usage:
10
+ deckbuilder create presentation.md
11
+ deckbuilder analyze default
12
+ deckbuilder generate-image 800 600 --filter grayscale
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ # Conditional imports for development vs installed package
23
+ try:
24
+ # Try package imports first (for installed package)
25
+ from deckbuilder.engine import Deckbuilder
26
+ from deckbuilder.cli_tools import TemplateManager
27
+ from placekitten import PlaceKitten
28
+ except ImportError:
29
+ # Fallback to development imports (when running from source)
30
+ current_dir = Path(__file__).parent
31
+ project_root = current_dir.parent.parent
32
+ sys.path.insert(0, str(project_root))
33
+
34
+ from src.deckbuilder.engine import Deckbuilder # noqa: E402
35
+ from src.deckbuilder.cli_tools import TemplateManager # noqa: E402
36
+ from src.placekitten import PlaceKitten # noqa: E402
37
+
38
+
39
+ class DeckbuilderCLI:
40
+ """Standalone Deckbuilder command-line interface"""
41
+
42
+ def __init__(self, templates_path=None, output_path=None):
43
+ self.setup_environment(templates_path, output_path)
44
+
45
+ def setup_environment(self, templates_path=None, output_path=None):
46
+ """Setup environment variables with priority: CLI args > env vars > defaults"""
47
+
48
+ # Template folder resolution priority
49
+ if templates_path:
50
+ # 1. CLI argument has highest priority
51
+ os.environ["DECK_TEMPLATE_FOLDER"] = str(Path(templates_path).resolve())
52
+ elif not os.getenv("DECK_TEMPLATE_FOLDER"):
53
+ # 2. Environment variable already set (skip)
54
+ # 3. Default location: ./templates/
55
+ default_templates = Path.cwd() / "templates"
56
+ if default_templates.exists():
57
+ os.environ["DECK_TEMPLATE_FOLDER"] = str(default_templates)
58
+ else:
59
+ # Will trigger error message in commands that need templates
60
+ pass
61
+
62
+ # Output folder resolution priority
63
+ if output_path:
64
+ # 1. CLI argument has highest priority
65
+ output_folder = Path(output_path)
66
+ output_folder.mkdir(parents=True, exist_ok=True)
67
+ os.environ["DECK_OUTPUT_FOLDER"] = str(output_folder.resolve())
68
+ elif not os.getenv("DECK_OUTPUT_FOLDER"):
69
+ # 2. Environment variable already set (skip)
70
+ # 3. Default location: current directory
71
+ os.environ["DECK_OUTPUT_FOLDER"] = str(Path.cwd())
72
+
73
+ # Default template name
74
+ if not os.getenv("DECK_TEMPLATE_NAME"):
75
+ os.environ["DECK_TEMPLATE_NAME"] = "default"
76
+
77
+ def _validate_templates_folder(self):
78
+ """Validate templates folder exists and provide helpful error message"""
79
+ template_folder = os.getenv("DECK_TEMPLATE_FOLDER")
80
+ if not template_folder or not Path(template_folder).exists():
81
+ print("❌ Template folder not found: ./templates/")
82
+ print("💡 Run 'deckbuilder init' to create template folder and copy default files")
83
+ return False
84
+ return True
85
+
86
+ def _get_available_templates(self):
87
+ """Get list of available templates with error handling"""
88
+ template_folder = os.getenv("DECK_TEMPLATE_FOLDER")
89
+ if not template_folder or not Path(template_folder).exists():
90
+ return []
91
+
92
+ template_path = Path(template_folder)
93
+ return [template.stem for template in template_path.glob("*.pptx")]
94
+
95
+ def create_presentation(
96
+ self, input_file: str, output_name: Optional[str] = None, template: Optional[str] = None
97
+ ) -> str:
98
+ """
99
+ Create presentation from markdown or JSON file
100
+
101
+ Args:
102
+ input_file: Path to markdown (.md) or JSON (.json) input file
103
+ output_name: Optional output filename (without extension)
104
+ template: Optional template name to use
105
+
106
+ Returns:
107
+ str: Path to generated presentation file
108
+ """
109
+ input_path = Path(input_file)
110
+
111
+ if not input_path.exists():
112
+ print(f"❌ Input file not found: {input_file}")
113
+ print("💡 Check file path or create the file first")
114
+ raise FileNotFoundError(f"Input file not found: {input_file}")
115
+
116
+ # Validate templates folder exists
117
+ if not self._validate_templates_folder():
118
+ return
119
+
120
+ # Determine output filename
121
+ if not output_name:
122
+ output_name = input_path.stem
123
+
124
+ # Set template if provided
125
+ if template:
126
+ os.environ["DECK_TEMPLATE_NAME"] = template
127
+
128
+ # Initialize Deckbuilder
129
+ db = Deckbuilder()
130
+
131
+ try:
132
+ if input_path.suffix.lower() == ".md":
133
+ # Process markdown file
134
+ content = input_path.read_text(encoding="utf-8")
135
+ result = db.create_presentation_from_markdown(
136
+ markdown_content=content, fileName=output_name
137
+ )
138
+ elif input_path.suffix.lower() == ".json":
139
+ # Process JSON file
140
+ with open(input_path, "r", encoding="utf-8") as f:
141
+ json_data = json.load(f)
142
+ result = db.create_presentation(json_data=json_data, fileName=output_name)
143
+ else:
144
+ raise ValueError(
145
+ f"Unsupported file format: {input_path.suffix}. "
146
+ "Supported formats: .md, .json"
147
+ )
148
+
149
+ print(f"✅ Presentation created successfully: {result}")
150
+ return result
151
+
152
+ except Exception as e:
153
+ print(f"❌ Error creating presentation: {e}")
154
+ raise
155
+
156
+ def analyze_template(self, template_name: str = "default", verbose: bool = False):
157
+ """Analyze PowerPoint template structure"""
158
+ if not self._validate_templates_folder():
159
+ return
160
+ manager = TemplateManager()
161
+ manager.analyze_template(template_name, verbose=verbose)
162
+
163
+ def validate_template(self, template_name: str = "default"):
164
+ """Validate template and mappings"""
165
+ if not self._validate_templates_folder():
166
+ return
167
+ manager = TemplateManager()
168
+ manager.validate_template(template_name)
169
+
170
+ def document_template(self, template_name: str = "default", output_file: Optional[str] = None):
171
+ """Generate comprehensive template documentation"""
172
+ if not self._validate_templates_folder():
173
+ return
174
+ manager = TemplateManager()
175
+ manager.document_template(template_name, output_file)
176
+
177
+ def enhance_template(
178
+ self,
179
+ template_name: str = "default",
180
+ mapping_file: Optional[str] = None,
181
+ no_backup: bool = False,
182
+ use_conventions: bool = True,
183
+ ):
184
+ """Enhance template with improved placeholder names"""
185
+ if not self._validate_templates_folder():
186
+ return
187
+ manager = TemplateManager()
188
+ create_backup = not no_backup
189
+ manager.enhance_template(template_name, mapping_file, create_backup, use_conventions)
190
+
191
+ def generate_placeholder_image(
192
+ self,
193
+ width: int,
194
+ height: int,
195
+ image_id: Optional[int] = None,
196
+ filter_type: Optional[str] = None,
197
+ output_file: Optional[str] = None,
198
+ ):
199
+ """Generate PlaceKitten placeholder image"""
200
+ pk = PlaceKitten()
201
+
202
+ try:
203
+ # Generate image with optional parameters
204
+ image = pk.generate(
205
+ width=width, height=height, image_id=image_id, filter_type=filter_type
206
+ )
207
+
208
+ # Set output filename
209
+ if not output_file:
210
+ filter_suffix = f"_{filter_type}" if filter_type else ""
211
+ id_suffix = f"_id{image_id}" if image_id else ""
212
+ output_file = f"placeholder_{width}x{height}{id_suffix}{filter_suffix}.jpg"
213
+
214
+ # Save image
215
+ result = image.save(output_file)
216
+ print(f"✅ Placeholder image generated: {result}")
217
+ return result
218
+
219
+ except Exception as e:
220
+ print(f"❌ Error generating image: {e}")
221
+ raise
222
+
223
+ def smart_crop_image(
224
+ self,
225
+ input_file: str,
226
+ width: int,
227
+ height: int,
228
+ save_steps: bool = False,
229
+ output_file: Optional[str] = None,
230
+ ):
231
+ """Apply smart cropping to an existing image"""
232
+ try:
233
+ from placekitten.processor import ImageProcessor
234
+ except ImportError:
235
+ from src.placekitten.processor import ImageProcessor
236
+
237
+ input_path = Path(input_file)
238
+ if not input_path.exists():
239
+ raise FileNotFoundError(f"Input image not found: {input_file}")
240
+
241
+ try:
242
+ processor = ImageProcessor(str(input_path))
243
+
244
+ # Apply smart cropping
245
+ result_processor = processor.smart_crop(
246
+ width=width, height=height, save_steps=save_steps, output_prefix="smart_crop"
247
+ )
248
+
249
+ # Set output filename
250
+ if not output_file:
251
+ output_file = f"smart_cropped_{width}x{height}_{input_path.name}"
252
+
253
+ # Save result
254
+ result = result_processor.save(output_file)
255
+ print(f"✅ Smart crop completed: {result}")
256
+
257
+ if save_steps:
258
+ print("📁 Processing steps saved with 'smart_crop_' prefix")
259
+
260
+ return result
261
+
262
+ except Exception as e:
263
+ print(f"❌ Error processing image: {e}")
264
+ raise
265
+
266
+ def list_templates(self):
267
+ """List available templates"""
268
+ if not self._validate_templates_folder():
269
+ return
270
+
271
+ templates = self._get_available_templates()
272
+ if templates:
273
+ print("📋 Available templates:")
274
+ for template in templates:
275
+ print(f" • {template}")
276
+ else:
277
+ print("❌ No templates found in template folder")
278
+ print("💡 Run 'deckbuilder init' to copy default template files")
279
+
280
+ def init_templates(self, path: str = "./templates"):
281
+ """Initialize template folder with default files and provide setup guidance"""
282
+ import shutil
283
+
284
+ target_path = Path(path).resolve()
285
+
286
+ # Create template folder
287
+ target_path.mkdir(parents=True, exist_ok=True)
288
+
289
+ try:
290
+ # Find the package assets (development structure)
291
+ assets_path = Path(__file__).parent.parent.parent / "assets" / "templates"
292
+ if assets_path.exists():
293
+ source_pptx = assets_path / "default.pptx"
294
+ source_json = assets_path / "default.json"
295
+ else:
296
+ print("❌ Could not locate template assets")
297
+ print("💡 Default templates not found in package")
298
+ return
299
+
300
+ # Copy template files
301
+ files_copied = []
302
+ if source_pptx.exists():
303
+ shutil.copy2(source_pptx, target_path / "default.pptx")
304
+ files_copied.append("default.pptx")
305
+
306
+ if source_json.exists():
307
+ shutil.copy2(source_json, target_path / "default.json")
308
+ files_copied.append("default.json")
309
+
310
+ if not files_copied:
311
+ print("❌ No template files found to copy")
312
+ return
313
+
314
+ # Success message
315
+ print(f"✅ Template folder created at {target_path}")
316
+ print(f"📁 Copied: {', '.join(files_copied)}")
317
+ print()
318
+
319
+ # Environment variable guidance
320
+ print("💡 To make this permanent, add to your .bash_profile:")
321
+ print(f'export DECK_TEMPLATE_FOLDER="{target_path}"')
322
+ print(f'export DECK_OUTPUT_FOLDER="{target_path.parent}"')
323
+ print('export DECK_TEMPLATE_NAME="default"')
324
+ print()
325
+ print("Then reload: source ~/.bash_profile")
326
+ print()
327
+ print("🚀 Ready to use! Try: deckbuilder create example.md")
328
+
329
+ except Exception as e:
330
+ print(f"❌ Error setting up templates: {e}")
331
+ print("💡 Make sure you have write permissions to the target directory")
332
+
333
+ def get_config(self):
334
+ """Display current configuration"""
335
+ print("🔧 Deckbuilder Configuration:")
336
+ print(f" Template Folder: {os.getenv('DECK_TEMPLATE_FOLDER', 'Not set')}")
337
+ print(f" Output Folder: {os.getenv('DECK_OUTPUT_FOLDER', 'Not set')}")
338
+ print(f" Default Template: {os.getenv('DECK_TEMPLATE_NAME', 'Not set')}")
339
+
340
+ def show_completion_help(self):
341
+ """Show tab completion installation instructions"""
342
+ print("🔧 Tab Completion Setup")
343
+ print()
344
+ print("To enable tab completion for deckbuilder commands:")
345
+ print()
346
+ print("1. Download the completion script:")
347
+ completion_url = (
348
+ "https://raw.githubusercontent.com/teknologika/deckbuilder/main/"
349
+ "deckbuilder-completion.bash"
350
+ )
351
+ print(f" curl -o ~/.deckbuilder-completion.bash {completion_url}")
352
+ print()
353
+ print("2. Add to your .bash_profile:")
354
+ print(' echo "source ~/.deckbuilder-completion.bash" >> ~/.bash_profile')
355
+ print()
356
+ print("3. Reload your shell:")
357
+ print(" source ~/.bash_profile")
358
+ print()
359
+ print("✨ After setup, you can use TAB to complete:")
360
+ print(" • Commands: deckbuilder <TAB>")
361
+ print(" • Template names: deckbuilder analyze <TAB>")
362
+ print(" • File paths: deckbuilder create <TAB>")
363
+ print(" • Global flags: deckbuilder -<TAB>")
364
+ print()
365
+ print("For system-wide installation:")
366
+ print(" sudo curl -o /etc/bash_completion.d/deckbuilder")
367
+ print(f" {completion_url}")
368
+
369
+
370
+ def create_parser():
371
+ """Create command-line argument parser"""
372
+ parser = argparse.ArgumentParser(
373
+ prog="deckbuilder",
374
+ description="Deckbuilder CLI - Intelligent PowerPoint presentation generation",
375
+ formatter_class=argparse.RawDescriptionHelpFormatter,
376
+ epilog="""
377
+ Examples:
378
+ # Create presentation from markdown
379
+ deckbuilder create presentation.md
380
+ deckbuilder -t ~/templates -o ~/output create slides.md
381
+
382
+ # Template management
383
+ deckbuilder analyze default --verbose
384
+ deckbuilder validate default
385
+ deckbuilder document default --output template_docs.md
386
+
387
+ # Image generation
388
+ deckbuilder image 800 600 --filter grayscale --output placeholder.jpg
389
+ deckbuilder crop input.jpg 1920 1080 --save-steps
390
+
391
+ # Configuration and setup
392
+ deckbuilder config
393
+ deckbuilder templates
394
+ deckbuilder completion
395
+ deckbuilder init
396
+ """,
397
+ )
398
+
399
+ # Global arguments (apply to all commands)
400
+ parser.add_argument(
401
+ "-t", "--templates", metavar="PATH", help="Template folder path (default: ./templates/)"
402
+ )
403
+ parser.add_argument(
404
+ "-o", "--output", metavar="PATH", help="Output folder path (default: current directory)"
405
+ )
406
+
407
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
408
+
409
+ # Create presentation command
410
+ create_parser = subparsers.add_parser(
411
+ "create", help="Create presentation from markdown or JSON file"
412
+ )
413
+ create_parser.add_argument("input_file", help="Input markdown (.md) or JSON (.json) file")
414
+ create_parser.add_argument("--output", "-o", help="Output filename (without extension)")
415
+ create_parser.add_argument("--template", "-t", help="Template name to use (default: 'default')")
416
+
417
+ # Template analysis commands
418
+ analyze_parser = subparsers.add_parser("analyze", help="Analyze template structure")
419
+ analyze_parser.add_argument("template", nargs="?", default="default", help="Template name")
420
+ analyze_parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
421
+
422
+ validate_parser = subparsers.add_parser("validate", help="Validate template and mappings")
423
+ validate_parser.add_argument("template", nargs="?", default="default", help="Template name")
424
+
425
+ document_parser = subparsers.add_parser("document", help="Generate template documentation")
426
+ document_parser.add_argument("template", nargs="?", default="default", help="Template name")
427
+ document_parser.add_argument("--output", "-o", help="Output documentation file")
428
+
429
+ enhance_parser = subparsers.add_parser("enhance", help="Enhance template placeholders")
430
+ enhance_parser.add_argument("template", nargs="?", default="default", help="Template name")
431
+ enhance_parser.add_argument("--mapping", help="Custom mapping file")
432
+ enhance_parser.add_argument("--no-backup", action="store_true", help="Skip backup creation")
433
+ enhance_parser.add_argument(
434
+ "--no-conventions",
435
+ action="store_false",
436
+ dest="use_conventions",
437
+ help="Don't use naming conventions",
438
+ )
439
+
440
+ # PlaceKitten image generation
441
+ image_parser = subparsers.add_parser("image", help="Generate placeholder image")
442
+ image_parser.add_argument("width", type=int, help="Image width")
443
+ image_parser.add_argument("height", type=int, help="Image height")
444
+ image_parser.add_argument("--id", type=int, help="Specific kitten image ID (1-6)")
445
+ image_parser.add_argument("--filter", help="Filter to apply (grayscale, sepia, blur, etc.)")
446
+ image_parser.add_argument("--output", "-o", help="Output filename")
447
+
448
+ # Smart crop command
449
+ crop_parser = subparsers.add_parser("crop", help="Apply smart cropping to image")
450
+ crop_parser.add_argument("input_file", help="Input image file")
451
+ crop_parser.add_argument("width", type=int, help="Target width")
452
+ crop_parser.add_argument("height", type=int, help="Target height")
453
+ crop_parser.add_argument("--save-steps", action="store_true", help="Save processing steps")
454
+ crop_parser.add_argument("--output", "-o", help="Output filename")
455
+
456
+ # Configuration commands
457
+ subparsers.add_parser("config", help="Show current configuration")
458
+ subparsers.add_parser("templates", help="List available templates")
459
+ subparsers.add_parser("completion", help="Show tab completion setup instructions")
460
+
461
+ # Init command
462
+ init_parser = subparsers.add_parser(
463
+ "init", help="Initialize template folder with default files"
464
+ )
465
+ init_parser.add_argument(
466
+ "path", nargs="?", default="./templates", help="Template folder path (default: ./templates)"
467
+ )
468
+
469
+ return parser
470
+
471
+
472
+ def main():
473
+ """Main CLI entry point"""
474
+ parser = create_parser()
475
+ args = parser.parse_args()
476
+
477
+ if not args.command:
478
+ parser.print_help()
479
+ return
480
+
481
+ # Initialize CLI with global arguments
482
+ cli = DeckbuilderCLI(templates_path=args.templates, output_path=args.output)
483
+
484
+ try:
485
+ # Route commands
486
+ if args.command == "create":
487
+ cli.create_presentation(
488
+ input_file=args.input_file, output_name=args.output, template=args.template
489
+ )
490
+
491
+ elif args.command == "analyze":
492
+ cli.analyze_template(args.template, verbose=args.verbose)
493
+
494
+ elif args.command == "validate":
495
+ cli.validate_template(args.template)
496
+
497
+ elif args.command == "document":
498
+ cli.document_template(args.template, args.output)
499
+
500
+ elif args.command == "enhance":
501
+ cli.enhance_template(
502
+ template_name=args.template,
503
+ mapping_file=args.mapping,
504
+ no_backup=args.no_backup,
505
+ use_conventions=args.use_conventions,
506
+ )
507
+
508
+ elif args.command == "image":
509
+ cli.generate_placeholder_image(
510
+ width=args.width,
511
+ height=args.height,
512
+ image_id=args.id,
513
+ filter_type=args.filter,
514
+ output_file=args.output,
515
+ )
516
+
517
+ elif args.command == "crop":
518
+ cli.smart_crop_image(
519
+ input_file=args.input_file,
520
+ width=args.width,
521
+ height=args.height,
522
+ save_steps=args.save_steps,
523
+ output_file=args.output,
524
+ )
525
+
526
+ elif args.command == "config":
527
+ cli.get_config()
528
+
529
+ elif args.command == "templates":
530
+ cli.list_templates()
531
+
532
+ elif args.command == "completion":
533
+ cli.show_completion_help()
534
+
535
+ elif args.command == "init":
536
+ cli.init_templates(args.path)
537
+
538
+ except Exception as e:
539
+ print(f"❌ Command failed: {e}")
540
+ sys.exit(1)
541
+
542
+
543
+ if __name__ == "__main__":
544
+ main()