Files
wehub-resource-sync 8ef9db2513
Deploy to GitHub Pages / deploy (push) Has been cancelled
Deploy to GitHub Pages / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:56:49 +08:00

701 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
PPT Master - Unified Configuration Management Module
Centrally manages all project configuration items to ensure consistency and maintainability.
Usage:
from config import Config, CANVAS_FORMATS, DESIGN_COLORS
# Get canvas format
ppt169 = Config.get_canvas_format('ppt169')
# Get color scheme
colors = Config.get_color_scheme('consulting')
"""
import argparse
import json
import os
from pathlib import Path
from typing import Dict, List, Optional, Any
from console_encoding import configure_utf8_stdio
configure_utf8_stdio()
# ============================================================
# Path Configuration
# ============================================================
# Project root directory
PROJECT_ROOT = Path(__file__).parent.parent
# Core directories
SCRIPTS_DIR = PROJECT_ROOT / 'scripts'
REFERENCES_DIR = PROJECT_ROOT / 'references'
TEMPLATES_DIR = PROJECT_ROOT / 'templates'
WORKFLOWS_DIR = PROJECT_ROOT / 'workflows'
# Repository root directory
REPO_ROOT = PROJECT_ROOT.parent.parent
EXAMPLES_DIR = REPO_ROOT / 'examples'
PROJECTS_DIR = REPO_ROOT / 'projects'
# Template subdirectories
CHART_TEMPLATES_DIR = TEMPLATES_DIR / 'charts'
# ============================================================
# Environment Configuration
# ============================================================
USER_CONFIG_DIR = Path.home() / '.ppt-master'
USER_ENV_FILE = USER_CONFIG_DIR / '.env'
def get_env_candidates() -> list[Path]:
"""Return the supported .env lookup order."""
return [
Path.cwd() / '.env',
PROJECT_ROOT / '.env',
REPO_ROOT / '.env',
USER_ENV_FILE,
]
def resolve_env_path() -> Path:
"""
Return the first existing .env path.
If no candidate exists, return the CWD .env path so callers can no-op
consistently while still showing a useful default location in messages.
"""
candidates = get_env_candidates()
for candidate in candidates:
if candidate.exists():
return candidate
return candidates[0]
def strip_env_quotes(value: str) -> str:
"""Strip matching surrounding quotes from a .env value."""
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
return value[1:-1]
return value
def strip_inline_env_comment(value: str) -> str:
"""Strip an unquoted inline ``#`` comment from a .env value.
Matches standard dotenv behavior: a ``#`` outside surrounding quotes
starts a comment and is dropped along with the rest of the line. To keep
a literal ``#`` in the value, wrap it in single or double quotes.
"""
stripped = value.lstrip()
if stripped.startswith(('"', "'")):
quote = stripped[0]
end = stripped.find(quote, 1)
if end != -1:
head = value[: len(value) - len(stripped) + end + 1]
tail = value[len(head):]
hash_pos = tail.find('#')
if hash_pos == -1:
return value
return head + tail[:hash_pos]
return value
hash_pos = value.find('#')
if hash_pos == -1:
return value
return value[:hash_pos]
def load_prefixed_env_file(
prefixes: tuple[str, ...],
*,
deprecated_keys: Optional[dict[str, str]] = None,
) -> Optional[Path]:
"""
Load matching keys from the first supported .env file.
Existing process environment variables always win. Keys outside the
requested prefixes are ignored so one shared .env can hold image, search,
and narration credentials without leaking unrelated values into the
process.
"""
env_path = resolve_env_path()
if not env_path.exists():
return None
deprecated_keys = deprecated_keys or {}
with env_path.open('r', encoding='utf-8') as fh:
for lineno, raw_line in enumerate(fh, start=1):
line = raw_line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('export '):
line = line[7:].lstrip()
if '=' not in line:
raise ValueError(
f"Invalid line in {env_path}:{lineno}. Expected KEY=VALUE."
)
key, value = line.split('=', 1)
key = key.strip()
if not key:
raise ValueError(
f"Invalid line in {env_path}:{lineno}. Missing variable name."
)
if not any(key.startswith(prefix) for prefix in prefixes):
continue
if key in deprecated_keys:
raise ValueError(
f"Unsupported key in {env_path}:{lineno}: {key}\n"
f"{deprecated_keys[key]}"
)
cleaned = strip_inline_env_comment(value).strip()
os.environ.setdefault(key, strip_env_quotes(cleaned))
return env_path
# ============================================================
# Canvas Format Configuration
# ============================================================
CANVAS_FORMATS = {
'ppt169': {
'name': 'PPT 16:9',
'dimensions': '1280×720',
'viewbox': '0 0 1280 720',
'width': 1280,
'height': 720,
'aspect_ratio': '16:9',
'use_case': 'Modern projectors, online presentations'
},
'ppt43': {
'name': 'PPT 4:3',
'dimensions': '1024×768',
'viewbox': '0 0 1024 768',
'width': 1024,
'height': 768,
'aspect_ratio': '4:3',
'use_case': 'Traditional projectors'
},
'wechat': {
'name': 'WeChat Article Header',
'dimensions': '900×383',
'viewbox': '0 0 900 383',
'width': 900,
'height': 383,
'aspect_ratio': '2.35:1',
'use_case': 'WeChat article cover images'
},
'xiaohongshu': {
'name': '小红书',
'dimensions': '1242×1660',
'viewbox': '0 0 1242 1660',
'width': 1242,
'height': 1660,
'aspect_ratio': '3:4',
'use_case': 'Knowledge sharing, product reviews'
},
'moments': {
'name': 'Moments/Instagram',
'dimensions': '1080×1080',
'viewbox': '0 0 1080 1080',
'width': 1080,
'height': 1080,
'aspect_ratio': '1:1',
'use_case': 'Social media square images'
},
'story': {
'name': 'Story/Vertical',
'dimensions': '1080×1920',
'viewbox': '0 0 1080 1920',
'width': 1080,
'height': 1920,
'aspect_ratio': '9:16',
'use_case': 'Short video covers, stories'
},
'banner': {
'name': 'Horizontal Banner',
'dimensions': '1920×1080',
'viewbox': '0 0 1920 1080',
'width': 1920,
'height': 1080,
'aspect_ratio': '16:9',
'use_case': 'Web banners, large screen displays'
},
'a4': {
'name': 'A4 Print',
'dimensions': '1240×1754',
'viewbox': '0 0 1240 1754',
'width': 1240,
'height': 1754,
'aspect_ratio': '√2:1',
'use_case': 'Print documents, PDF export'
}
}
# ============================================================
# Design Color Configuration
# ============================================================
DESIGN_COLORS = {
'consulting': {
'name': 'Consulting Style',
'primary': '#005587',
'secondary': '#0076A8',
'accent': '#F5A623',
'success': '#27AE60',
'warning': '#E74C3C',
'text_dark': '#1A252F',
'text_light': '#FFFFFF',
'text_muted': '#7F8C8D',
'background': '#FFFFFF',
'background_alt': '#F8F9FA'
},
'general': {
'name': 'General Flexible Style',
'primary': '#2196F3',
'secondary': '#4CAF50',
'accent': '#FF9800',
'purple': '#9C27B0',
'success': '#27AE60',
'warning': '#E74C3C',
'text_dark': '#2C3E50',
'text_light': '#FFFFFF',
'text_muted': '#7F8C8D',
'background': '#FFFFFF',
'background_alt': '#F8F9FA'
},
'tech': {
'name': 'Tech Style',
'primary': '#00D1FF',
'secondary': '#7B61FF',
'accent': '#00FF88',
'success': '#00FF88',
'warning': '#FF6B6B',
'text_dark': '#0A0E17',
'text_light': '#FFFFFF',
'text_muted': '#8892A0',
'background': '#0A0E17',
'background_alt': '#1A1F2E'
},
'academic': {
'name': 'Academic Style',
'primary': '#8B0000',
'secondary': '#1E3A5F',
'accent': '#C9B037',
'success': '#2E7D32',
'warning': '#D32F2F',
'text_dark': '#1A1A1A',
'text_light': '#FFFFFF',
'text_muted': '#666666',
'background': '#FFFFFF',
'background_alt': '#F5F5F5'
},
'government': {
'name': 'Government Style',
'primary': '#C41E3A',
'secondary': '#1E3A5F',
'accent': '#D4AF37',
'success': '#2E7D32',
'warning': '#B71C1C',
'text_dark': '#1A1A1A',
'text_light': '#FFFFFF',
'text_muted': '#555555',
'background': '#FFFFFF',
'background_alt': '#FFF8E1'
}
}
# ============================================================
# Industry Color Templates
# ============================================================
INDUSTRY_COLORS = {
'finance': {
'name': 'Finance/Banking',
'primary': '#003366',
'secondary': '#4A90D9',
'accent': '#D4AF37'
},
'healthcare': {
'name': 'Healthcare/Medical',
'primary': '#00796B',
'secondary': '#4DB6AC',
'accent': '#FF7043'
},
'technology': {
'name': 'Technology/Internet',
'primary': '#1565C0',
'secondary': '#42A5F5',
'accent': '#00E676'
},
'education': {
'name': 'Education/Training',
'primary': '#5E35B1',
'secondary': '#7E57C2',
'accent': '#FFD54F'
},
'retail': {
'name': 'Retail/Consumer',
'primary': '#E53935',
'secondary': '#EF5350',
'accent': '#FFB300'
},
'manufacturing': {
'name': 'Manufacturing/Industrial',
'primary': '#455A64',
'secondary': '#78909C',
'accent': '#FF6F00'
},
'energy': {
'name': 'Energy/Environmental',
'primary': '#2E7D32',
'secondary': '#66BB6A',
'accent': '#FDD835'
},
'realestate': {
'name': 'Real Estate/Construction',
'primary': '#795548',
'secondary': '#A1887F',
'accent': '#4CAF50'
},
'legal': {
'name': 'Legal/Compliance',
'primary': '#37474F',
'secondary': '#546E7A',
'accent': '#8D6E63'
},
'media': {
'name': 'Media/Entertainment',
'primary': '#7B1FA2',
'secondary': '#AB47BC',
'accent': '#FF4081'
},
'logistics': {
'name': 'Logistics/Supply Chain',
'primary': '#F57C00',
'secondary': '#FFB74D',
'accent': '#0288D1'
},
'agriculture': {
'name': 'Agriculture/Food',
'primary': '#558B2F',
'secondary': '#8BC34A',
'accent': '#FFCA28'
},
'tourism': {
'name': 'Tourism/Hospitality',
'primary': '#00ACC1',
'secondary': '#4DD0E1',
'accent': '#FF7043'
},
'automotive': {
'name': 'Automotive/Transportation',
'primary': '#263238',
'secondary': '#455A64',
'accent': '#D32F2F'
}
}
# ============================================================
# Font Configuration
# ============================================================
FONTS = {
'system_ui': "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
'sans_serif': "'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif",
'monospace': "'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace"
}
FONT_SIZES = {
'title_large': 48,
'title': 36,
'title_small': 28,
'heading': 24,
'subheading': 20,
'body': 18,
'body_small': 16,
'caption': 14,
'footnote': 12
}
# ============================================================
# Layout Configuration
# ============================================================
LAYOUT_MARGINS = {
'ppt169': {
'top': 60,
'right': 60,
'bottom': 60,
'left': 60,
'content_width': 1160,
'content_height': 600
},
'ppt43': {
'top': 50,
'right': 50,
'bottom': 50,
'left': 50,
'content_width': 924,
'content_height': 608
},
'xiaohongshu': {
'top': 80,
'right': 60,
'bottom': 80,
'left': 60,
'content_width': 1122,
'content_height': 1500
},
'moments': {
'top': 60,
'right': 60,
'bottom': 60,
'left': 60,
'content_width': 960,
'content_height': 960
},
'story': {
'top': 120,
'right': 60,
'bottom': 180,
'left': 60,
'content_width': 960,
'content_height': 1620
},
'wechat': {
'top': 40,
'right': 40,
'bottom': 40,
'left': 40,
'content_width': 820,
'content_height': 303
},
}
# ============================================================
# SVG Policy Reference
# ============================================================
# Do not mirror element/attribute rules here. The shared standard is the
# authoring authority and the quality checker enforces it.
# Keep the exported key for compatibility with existing config consumers.
SVG_CONSTRAINTS = {
'authority': 'skills/ppt-master/references/shared-standards.md',
'validator': 'skills/ppt-master/scripts/svg_quality_checker.py',
}
# ============================================================
# Configuration Manager Class
# ============================================================
class Config:
"""Configuration manager."""
@staticmethod
def get_canvas_format(format_key: str) -> Optional[Dict]:
"""
Get canvas format configuration.
Args:
format_key: Format key name (e.g. 'ppt169', 'xiaohongshu')
Returns:
Format configuration dict, or None if not found
"""
return CANVAS_FORMATS.get(format_key)
@staticmethod
def get_all_canvas_formats() -> Dict:
"""Get all canvas formats."""
return CANVAS_FORMATS.copy()
@staticmethod
def get_color_scheme(style: str) -> Optional[Dict]:
"""
Get color scheme.
Args:
style: Style name (e.g. 'consulting', 'general', 'tech')
Returns:
Color scheme dict
"""
return DESIGN_COLORS.get(style)
@staticmethod
def get_industry_colors(industry: str) -> Optional[Dict]:
"""
Get industry color palette.
Args:
industry: Industry name (e.g. 'finance', 'healthcare')
Returns:
Industry color dict
"""
return INDUSTRY_COLORS.get(industry)
@staticmethod
def get_all_industries() -> List[str]:
"""Get list of all industries."""
return list(INDUSTRY_COLORS.keys())
@staticmethod
def get_layout_margins(format_key: str) -> Optional[Dict]:
"""
Get layout margin configuration.
Args:
format_key: Format key name
Returns:
Margin configuration dict
"""
return LAYOUT_MARGINS.get(format_key)
@staticmethod
def get_font(font_type: str = 'system_ui') -> str:
"""
Get font declaration.
Args:
font_type: Font type ('system_ui', 'sans_serif', 'monospace')
Returns:
Font declaration string
"""
return FONTS.get(font_type, FONTS['system_ui'])
@staticmethod
def get_font_size(size_name: str) -> int:
"""
Get font size.
Args:
size_name: Size name (e.g. 'title', 'body', 'caption')
Returns:
Font size (pixels)
"""
return FONT_SIZES.get(size_name, FONT_SIZES['body'])
@staticmethod
def get_project_path(subdir: str = '') -> Path:
"""
Get project path.
Args:
subdir: Subdirectory name
Returns:
Full path
"""
if subdir:
return PROJECT_ROOT / subdir
return PROJECT_ROOT
@staticmethod
def export_config(output_file: str = 'config_export.json'):
"""
Export configuration to a JSON file.
Args:
output_file: Output file path
"""
config_data = {
'canvas_formats': CANVAS_FORMATS,
'design_colors': DESIGN_COLORS,
'industry_colors': INDUSTRY_COLORS,
'fonts': FONTS,
'font_sizes': FONT_SIZES,
'svg_constraints': SVG_CONSTRAINTS
}
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(config_data, f, ensure_ascii=False, indent=2)
print(f"Configuration exported to: {output_file}")
# ============================================================
# Command Line Interface
# ============================================================
def build_parser() -> argparse.ArgumentParser:
"""Build the command-line parser."""
parser = argparse.ArgumentParser(
description="PPT Master configuration management tool.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("list-formats", help="List all canvas formats")
subparsers.add_parser("list-colors", help="List all color schemes")
subparsers.add_parser("list-industries", help="List all industry colors")
export = subparsers.add_parser("export", help="Export configuration to JSON")
export.add_argument(
"output_path",
nargs="?",
help="Output JSON path (backward-compatible positional form)",
)
export.add_argument(
"-o",
"--output",
default=None,
help="Output JSON path (default: config_export.json)",
)
format_parser = subparsers.add_parser("format", help="View a specific canvas format")
format_parser.add_argument("key", choices=sorted(CANVAS_FORMATS), help="Canvas format key")
return parser
def main(argv: list[str] | None = None) -> int:
"""Command line entry point."""
parser = build_parser()
args = parser.parse_args(argv)
if args.command == 'list-formats':
print("\nCanvas Format List:\n")
for key, info in CANVAS_FORMATS.items():
print(
f" {key:15} | {info['name']:15} | {info['dimensions']:12} | {info['use_case']}")
elif args.command == 'list-colors':
print("\nColor Scheme List:\n")
for key, info in DESIGN_COLORS.items():
print(f" {key:12} | {info['name']:15} | Primary: {info['primary']}")
elif args.command == 'list-industries':
print("\nIndustry Color List:\n")
for key, info in INDUSTRY_COLORS.items():
print(f" {key:15} | {info['name']:15} | Primary: {info['primary']}")
elif args.command == 'export':
Config.export_config(args.output or args.output_path or "config_export.json")
elif args.command == 'format':
info = Config.get_canvas_format(args.key)
print(f"\nCanvas Format: {args.key}\n")
for key, value in info.items():
print(f" {key}: {value}")
return 0
if __name__ == '__main__':
raise SystemExit(main())