返回 ppt-master
config.py
根目录 / skills / ppt-master / scripts / config.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Unified Configuration Management Module
4
5 Centrally manages all project configuration items to ensure consistency and maintainability.
6
7 Usage:
8 from config import Config, CANVAS_FORMATS, DESIGN_COLORS
9
10 # Get canvas format
11 ppt169 = Config.get_canvas_format('ppt169')
12
13 # Get color scheme
14 colors = Config.get_color_scheme('consulting')
15 """
16
17 import argparse
18 import json
19 import os
20 from pathlib import Path
21 from typing import Dict, List, Optional, Any
22
23 from console_encoding import configure_utf8_stdio
24
25 configure_utf8_stdio()
26
27
28 # ============================================================
29 # Path Configuration
30 # ============================================================
31
32 # Project root directory
33 PROJECT_ROOT = Path(__file__).parent.parent
34
35 # Core directories
36 SCRIPTS_DIR = PROJECT_ROOT / 'scripts'
37 REFERENCES_DIR = PROJECT_ROOT / 'references'
38 TEMPLATES_DIR = PROJECT_ROOT / 'templates'
39 WORKFLOWS_DIR = PROJECT_ROOT / 'workflows'
40
41 # Repository root directory
42 REPO_ROOT = PROJECT_ROOT.parent.parent
43 EXAMPLES_DIR = REPO_ROOT / 'examples'
44 PROJECTS_DIR = REPO_ROOT / 'projects'
45
46 # Template subdirectories
47 CHART_TEMPLATES_DIR = TEMPLATES_DIR / 'charts'
48
49
50 # ============================================================
51 # Environment Configuration
52 # ============================================================
53
54 USER_CONFIG_DIR = Path.home() / '.ppt-master'
55 USER_ENV_FILE = USER_CONFIG_DIR / '.env'
56
57
58 def get_env_candidates() -> list[Path]:
59 """Return the supported .env lookup order."""
60 return [
61 Path.cwd() / '.env',
62 PROJECT_ROOT / '.env',
63 REPO_ROOT / '.env',
64 USER_ENV_FILE,
65 ]
66
67
68 def resolve_env_path() -> Path:
69 """
70 Return the first existing .env path.
71
72 If no candidate exists, return the CWD .env path so callers can no-op
73 consistently while still showing a useful default location in messages.
74 """
75 candidates = get_env_candidates()
76 for candidate in candidates:
77 if candidate.exists():
78 return candidate
79 return candidates[0]
80
81
82 def strip_env_quotes(value: str) -> str:
83 """Strip matching surrounding quotes from a .env value."""
84 if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
85 return value[1:-1]
86 return value
87
88
89 def strip_inline_env_comment(value: str) -> str:
90 """Strip an unquoted inline ``#`` comment from a .env value.
91
92 Matches standard dotenv behavior: a ``#`` outside surrounding quotes
93 starts a comment and is dropped along with the rest of the line. To keep
94 a literal ``#`` in the value, wrap it in single or double quotes.
95 """
96 stripped = value.lstrip()
97 if stripped.startswith(('"', "'")):
98 quote = stripped[0]
99 end = stripped.find(quote, 1)
100 if end != -1:
101 head = value[: len(value) - len(stripped) + end + 1]
102 tail = value[len(head):]
103 hash_pos = tail.find('#')
104 if hash_pos == -1:
105 return value
106 return head + tail[:hash_pos]
107 return value
108 hash_pos = value.find('#')
109 if hash_pos == -1:
110 return value
111 return value[:hash_pos]
112
113
114 def load_prefixed_env_file(
115 prefixes: tuple[str, ...],
116 *,
117 deprecated_keys: Optional[dict[str, str]] = None,
118 ) -> Optional[Path]:
119 """
120 Load matching keys from the first supported .env file.
121
122 Existing process environment variables always win. Keys outside the
123 requested prefixes are ignored so one shared .env can hold image, search,
124 and narration credentials without leaking unrelated values into the
125 process.
126 """
127 env_path = resolve_env_path()
128 if not env_path.exists():
129 return None
130
131 deprecated_keys = deprecated_keys or {}
132 with env_path.open('r', encoding='utf-8') as fh:
133 for lineno, raw_line in enumerate(fh, start=1):
134 line = raw_line.strip()
135 if not line or line.startswith('#'):
136 continue
137 if line.startswith('export '):
138 line = line[7:].lstrip()
139 if '=' not in line:
140 raise ValueError(
141 f"Invalid line in {env_path}:{lineno}. Expected KEY=VALUE."
142 )
143
144 key, value = line.split('=', 1)
145 key = key.strip()
146 if not key:
147 raise ValueError(
148 f"Invalid line in {env_path}:{lineno}. Missing variable name."
149 )
150 if not any(key.startswith(prefix) for prefix in prefixes):
151 continue
152 if key in deprecated_keys:
153 raise ValueError(
154 f"Unsupported key in {env_path}:{lineno}: {key}\n"
155 f"{deprecated_keys[key]}"
156 )
157 cleaned = strip_inline_env_comment(value).strip()
158 os.environ.setdefault(key, strip_env_quotes(cleaned))
159
160 return env_path
161
162
163 # ============================================================
164 # Canvas Format Configuration
165 # ============================================================
166
167 CANVAS_FORMATS = {
168 'ppt169': {
169 'name': 'PPT 16:9',
170 'dimensions': '1280×720',
171 'viewbox': '0 0 1280 720',
172 'width': 1280,
173 'height': 720,
174 'aspect_ratio': '16:9',
175 'use_case': 'Modern projectors, online presentations'
176 },
177 'ppt43': {
178 'name': 'PPT 4:3',
179 'dimensions': '1024×768',
180 'viewbox': '0 0 1024 768',
181 'width': 1024,
182 'height': 768,
183 'aspect_ratio': '4:3',
184 'use_case': 'Traditional projectors'
185 },
186 'wechat': {
187 'name': 'WeChat Article Header',
188 'dimensions': '900×383',
189 'viewbox': '0 0 900 383',
190 'width': 900,
191 'height': 383,
192 'aspect_ratio': '2.35:1',
193 'use_case': 'WeChat article cover images'
194 },
195 'xiaohongshu': {
196 'name': '小红书',
197 'dimensions': '1242×1660',
198 'viewbox': '0 0 1242 1660',
199 'width': 1242,
200 'height': 1660,
201 'aspect_ratio': '3:4',
202 'use_case': 'Knowledge sharing, product reviews'
203 },
204 'moments': {
205 'name': 'Moments/Instagram',
206 'dimensions': '1080×1080',
207 'viewbox': '0 0 1080 1080',
208 'width': 1080,
209 'height': 1080,
210 'aspect_ratio': '1:1',
211 'use_case': 'Social media square images'
212 },
213 'story': {
214 'name': 'Story/Vertical',
215 'dimensions': '1080×1920',
216 'viewbox': '0 0 1080 1920',
217 'width': 1080,
218 'height': 1920,
219 'aspect_ratio': '9:16',
220 'use_case': 'Short video covers, stories'
221 },
222 'banner': {
223 'name': 'Horizontal Banner',
224 'dimensions': '1920×1080',
225 'viewbox': '0 0 1920 1080',
226 'width': 1920,
227 'height': 1080,
228 'aspect_ratio': '16:9',
229 'use_case': 'Web banners, large screen displays'
230 },
231 'a4': {
232 'name': 'A4 Print',
233 'dimensions': '1240×1754',
234 'viewbox': '0 0 1240 1754',
235 'width': 1240,
236 'height': 1754,
237 'aspect_ratio': '√2:1',
238 'use_case': 'Print documents, PDF export'
239 }
240 }
241
242
243 # ============================================================
244 # Design Color Configuration
245 # ============================================================
246
247 DESIGN_COLORS = {
248 'consulting': {
249 'name': 'Consulting Style',
250 'primary': '#005587',
251 'secondary': '#0076A8',
252 'accent': '#F5A623',
253 'success': '#27AE60',
254 'warning': '#E74C3C',
255 'text_dark': '#1A252F',
256 'text_light': '#FFFFFF',
257 'text_muted': '#7F8C8D',
258 'background': '#FFFFFF',
259 'background_alt': '#F8F9FA'
260 },
261 'general': {
262 'name': 'General Flexible Style',
263 'primary': '#2196F3',
264 'secondary': '#4CAF50',
265 'accent': '#FF9800',
266 'purple': '#9C27B0',
267 'success': '#27AE60',
268 'warning': '#E74C3C',
269 'text_dark': '#2C3E50',
270 'text_light': '#FFFFFF',
271 'text_muted': '#7F8C8D',
272 'background': '#FFFFFF',
273 'background_alt': '#F8F9FA'
274 },
275 'tech': {
276 'name': 'Tech Style',
277 'primary': '#00D1FF',
278 'secondary': '#7B61FF',
279 'accent': '#00FF88',
280 'success': '#00FF88',
281 'warning': '#FF6B6B',
282 'text_dark': '#0A0E17',
283 'text_light': '#FFFFFF',
284 'text_muted': '#8892A0',
285 'background': '#0A0E17',
286 'background_alt': '#1A1F2E'
287 },
288 'academic': {
289 'name': 'Academic Style',
290 'primary': '#8B0000',
291 'secondary': '#1E3A5F',
292 'accent': '#C9B037',
293 'success': '#2E7D32',
294 'warning': '#D32F2F',
295 'text_dark': '#1A1A1A',
296 'text_light': '#FFFFFF',
297 'text_muted': '#666666',
298 'background': '#FFFFFF',
299 'background_alt': '#F5F5F5'
300 },
301 'government': {
302 'name': 'Government Style',
303 'primary': '#C41E3A',
304 'secondary': '#1E3A5F',
305 'accent': '#D4AF37',
306 'success': '#2E7D32',
307 'warning': '#B71C1C',
308 'text_dark': '#1A1A1A',
309 'text_light': '#FFFFFF',
310 'text_muted': '#555555',
311 'background': '#FFFFFF',
312 'background_alt': '#FFF8E1'
313 }
314 }
315
316
317 # ============================================================
318 # Industry Color Templates
319 # ============================================================
320
321 INDUSTRY_COLORS = {
322 'finance': {
323 'name': 'Finance/Banking',
324 'primary': '#003366',
325 'secondary': '#4A90D9',
326 'accent': '#D4AF37'
327 },
328 'healthcare': {
329 'name': 'Healthcare/Medical',
330 'primary': '#00796B',
331 'secondary': '#4DB6AC',
332 'accent': '#FF7043'
333 },
334 'technology': {
335 'name': 'Technology/Internet',
336 'primary': '#1565C0',
337 'secondary': '#42A5F5',
338 'accent': '#00E676'
339 },
340 'education': {
341 'name': 'Education/Training',
342 'primary': '#5E35B1',
343 'secondary': '#7E57C2',
344 'accent': '#FFD54F'
345 },
346 'retail': {
347 'name': 'Retail/Consumer',
348 'primary': '#E53935',
349 'secondary': '#EF5350',
350 'accent': '#FFB300'
351 },
352 'manufacturing': {
353 'name': 'Manufacturing/Industrial',
354 'primary': '#455A64',
355 'secondary': '#78909C',
356 'accent': '#FF6F00'
357 },
358 'energy': {
359 'name': 'Energy/Environmental',
360 'primary': '#2E7D32',
361 'secondary': '#66BB6A',
362 'accent': '#FDD835'
363 },
364 'realestate': {
365 'name': 'Real Estate/Construction',
366 'primary': '#795548',
367 'secondary': '#A1887F',
368 'accent': '#4CAF50'
369 },
370 'legal': {
371 'name': 'Legal/Compliance',
372 'primary': '#37474F',
373 'secondary': '#546E7A',
374 'accent': '#8D6E63'
375 },
376 'media': {
377 'name': 'Media/Entertainment',
378 'primary': '#7B1FA2',
379 'secondary': '#AB47BC',
380 'accent': '#FF4081'
381 },
382 'logistics': {
383 'name': 'Logistics/Supply Chain',
384 'primary': '#F57C00',
385 'secondary': '#FFB74D',
386 'accent': '#0288D1'
387 },
388 'agriculture': {
389 'name': 'Agriculture/Food',
390 'primary': '#558B2F',
391 'secondary': '#8BC34A',
392 'accent': '#FFCA28'
393 },
394 'tourism': {
395 'name': 'Tourism/Hospitality',
396 'primary': '#00ACC1',
397 'secondary': '#4DD0E1',
398 'accent': '#FF7043'
399 },
400 'automotive': {
401 'name': 'Automotive/Transportation',
402 'primary': '#263238',
403 'secondary': '#455A64',
404 'accent': '#D32F2F'
405 }
406 }
407
408
409 # ============================================================
410 # Font Configuration
411 # ============================================================
412
413 FONTS = {
414 'system_ui': "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
415 'sans_serif': "'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif",
416 'monospace': "'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace"
417 }
418
419 FONT_SIZES = {
420 'title_large': 48,
421 'title': 36,
422 'title_small': 28,
423 'heading': 24,
424 'subheading': 20,
425 'body': 18,
426 'body_small': 16,
427 'caption': 14,
428 'footnote': 12
429 }
430
431
432 # ============================================================
433 # Layout Configuration
434 # ============================================================
435
436 LAYOUT_MARGINS = {
437 'ppt169': {
438 'top': 60,
439 'right': 60,
440 'bottom': 60,
441 'left': 60,
442 'content_width': 1160,
443 'content_height': 600
444 },
445 'ppt43': {
446 'top': 50,
447 'right': 50,
448 'bottom': 50,
449 'left': 50,
450 'content_width': 924,
451 'content_height': 608
452 },
453 'xiaohongshu': {
454 'top': 80,
455 'right': 60,
456 'bottom': 80,
457 'left': 60,
458 'content_width': 1122,
459 'content_height': 1500
460 },
461 'moments': {
462 'top': 60,
463 'right': 60,
464 'bottom': 60,
465 'left': 60,
466 'content_width': 960,
467 'content_height': 960
468 },
469 'story': {
470 'top': 120,
471 'right': 60,
472 'bottom': 180,
473 'left': 60,
474 'content_width': 960,
475 'content_height': 1620
476 },
477 'wechat': {
478 'top': 40,
479 'right': 40,
480 'bottom': 40,
481 'left': 40,
482 'content_width': 820,
483 'content_height': 303
484 },
485 }
486
487
488 # ============================================================
489 # SVG Policy Reference
490 # ============================================================
491
492 # Do not mirror element/attribute rules here. The router selects the mandatory
493 # core and feature-triggered interfaces; the quality checker enforces them.
494 # Keep the exported authority key as the compatibility router for existing
495 # config consumers.
496 SVG_CONSTRAINTS = {
497 'authority': 'skills/ppt-master/references/shared-standards.md',
498 'core_authority': 'skills/ppt-master/references/shared-standards-core.md',
499 'conditional_authorities': {
500 'effects': 'skills/ppt-master/references/svg-effects.md',
501 'native_data': 'skills/ppt-master/references/native-data-interface.md',
502 'pptx_structure': 'skills/ppt-master/references/pptx-structure-interface.md',
503 },
504 'validator': 'skills/ppt-master/scripts/svg_quality_checker.py',
505 }
506
507
508 # ============================================================
509 # Configuration Manager Class
510 # ============================================================
511
512 class Config:
513 """Configuration manager."""
514
515 @staticmethod
516 def get_canvas_format(format_key: str) -> Optional[Dict]:
517 """
518 Get canvas format configuration.
519
520 Args:
521 format_key: Format key name (e.g. 'ppt169', 'xiaohongshu')
522
523 Returns:
524 Format configuration dict, or None if not found
525 """
526 return CANVAS_FORMATS.get(format_key)
527
528 @staticmethod
529 def get_all_canvas_formats() -> Dict:
530 """Get all canvas formats."""
531 return CANVAS_FORMATS.copy()
532
533 @staticmethod
534 def get_color_scheme(style: str) -> Optional[Dict]:
535 """
536 Get color scheme.
537
538 Args:
539 style: Style name (e.g. 'consulting', 'general', 'tech')
540
541 Returns:
542 Color scheme dict
543 """
544 return DESIGN_COLORS.get(style)
545
546 @staticmethod
547 def get_industry_colors(industry: str) -> Optional[Dict]:
548 """
549 Get industry color palette.
550
551 Args:
552 industry: Industry name (e.g. 'finance', 'healthcare')
553
554 Returns:
555 Industry color dict
556 """
557 return INDUSTRY_COLORS.get(industry)
558
559 @staticmethod
560 def get_all_industries() -> List[str]:
561 """Get list of all industries."""
562 return list(INDUSTRY_COLORS.keys())
563
564 @staticmethod
565 def get_layout_margins(format_key: str) -> Optional[Dict]:
566 """
567 Get layout margin configuration.
568
569 Args:
570 format_key: Format key name
571
572 Returns:
573 Margin configuration dict
574 """
575 return LAYOUT_MARGINS.get(format_key)
576
577 @staticmethod
578 def get_font(font_type: str = 'system_ui') -> str:
579 """
580 Get font declaration.
581
582 Args:
583 font_type: Font type ('system_ui', 'sans_serif', 'monospace')
584
585 Returns:
586 Font declaration string
587 """
588 return FONTS.get(font_type, FONTS['system_ui'])
589
590 @staticmethod
591 def get_font_size(size_name: str) -> int:
592 """
593 Get font size.
594
595 Args:
596 size_name: Size name (e.g. 'title', 'body', 'caption')
597
598 Returns:
599 Font size (pixels)
600 """
601 return FONT_SIZES.get(size_name, FONT_SIZES['body'])
602
603 @staticmethod
604 def get_project_path(subdir: str = '') -> Path:
605 """
606 Get project path.
607
608 Args:
609 subdir: Subdirectory name
610
611 Returns:
612 Full path
613 """
614 if subdir:
615 return PROJECT_ROOT / subdir
616 return PROJECT_ROOT
617
618 @staticmethod
619 def export_config(output_file: str = 'config_export.json'):
620 """
621 Export configuration to a JSON file.
622
623 Args:
624 output_file: Output file path
625 """
626 config_data = {
627 'canvas_formats': CANVAS_FORMATS,
628 'design_colors': DESIGN_COLORS,
629 'industry_colors': INDUSTRY_COLORS,
630 'fonts': FONTS,
631 'font_sizes': FONT_SIZES,
632 'svg_constraints': SVG_CONSTRAINTS
633 }
634
635 with open(output_file, 'w', encoding='utf-8') as f:
636 json.dump(config_data, f, ensure_ascii=False, indent=2)
637
638 print(f"Configuration exported to: {output_file}")
639
640
641 # ============================================================
642 # Command Line Interface
643 # ============================================================
644
645 def build_parser() -> argparse.ArgumentParser:
646 """Build the command-line parser."""
647 parser = argparse.ArgumentParser(
648 description="PPT Master configuration management tool.",
649 )
650 subparsers = parser.add_subparsers(dest="command", required=True)
651 subparsers.add_parser("list-formats", help="List all canvas formats")
652 subparsers.add_parser("list-colors", help="List all color schemes")
653 subparsers.add_parser("list-industries", help="List all industry colors")
654
655 export = subparsers.add_parser("export", help="Export configuration to JSON")
656 export.add_argument(
657 "output_path",
658 nargs="?",
659 help="Output JSON path (backward-compatible positional form)",
660 )
661 export.add_argument(
662 "-o",
663 "--output",
664 default=None,
665 help="Output JSON path (default: config_export.json)",
666 )
667
668 format_parser = subparsers.add_parser("format", help="View a specific canvas format")
669 format_parser.add_argument("key", choices=sorted(CANVAS_FORMATS), help="Canvas format key")
670 return parser
671
672
673 def main(argv: list[str] | None = None) -> int:
674 """Command line entry point."""
675 parser = build_parser()
676 args = parser.parse_args(argv)
677
678 if args.command == 'list-formats':
679 print("\nCanvas Format List:\n")
680 for key, info in CANVAS_FORMATS.items():
681 print(
682 f" {key:15} | {info['name']:15} | {info['dimensions']:12} | {info['use_case']}")
683
684 elif args.command == 'list-colors':
685 print("\nColor Scheme List:\n")
686 for key, info in DESIGN_COLORS.items():
687 print(f" {key:12} | {info['name']:15} | Primary: {info['primary']}")
688
689 elif args.command == 'list-industries':
690 print("\nIndustry Color List:\n")
691 for key, info in INDUSTRY_COLORS.items():
692 print(f" {key:15} | {info['name']:15} | Primary: {info['primary']}")
693
694 elif args.command == 'export':
695 Config.export_config(args.output or args.output_path or "config_export.json")
696
697 elif args.command == 'format':
698 info = Config.get_canvas_format(args.key)
699 print(f"\nCanvas Format: {args.key}\n")
700 for key, value in info.items():
701 print(f" {key}: {value}")
702
703 return 0
704
705
706 if __name__ == '__main__':
707 raise SystemExit(main())
708
708 lines PYTHON