522 lines
16 KiB
Python
Executable File
522 lines
16 KiB
Python
Executable File
"""
|
||
xlsx skill — Palette System (Style-First Theme Engine)
|
||
=======================================================
|
||
12 visual styles × scene-based fallback. No domain-color binding.
|
||
|
||
Themes (12):
|
||
professional, warm, elegant, creative,
|
||
muji, aesop, kinfolk, celine, bottega, chanel, bloomberg, original_blue
|
||
|
||
Matching priority:
|
||
1. Explicit style keywords in prompt → direct match
|
||
2. Scene/content keywords → infer style
|
||
3. No match → professional (safe default)
|
||
|
||
Usage:
|
||
from templates.palettes import resolve_palette, get_palette
|
||
|
||
# Auto-detect from user prompt
|
||
palette = resolve_palette("帮我做一个温暖的销售月报") # Chinese prompt example
|
||
# → warm palette
|
||
|
||
# Manual selection
|
||
palette = get_palette("bottega")
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
from typing import Dict, Optional, Tuple
|
||
|
||
# ============================================================
|
||
# §1 Palette Data Structure
|
||
# ============================================================
|
||
|
||
_Palette = Dict[str, str | list]
|
||
|
||
|
||
def _make_palette(
|
||
*,
|
||
primary: str,
|
||
primary_light: str,
|
||
accent_positive: str = "1B7D46",
|
||
accent_negative: str = "C0392B",
|
||
accent_warning: str = "D4820A",
|
||
neutral_900: str = "37352F",
|
||
neutral_600: str = "8C8A84",
|
||
neutral_200: str = "E9E9E8",
|
||
neutral_100: str = "F7F7F5",
|
||
neutral_50: str = "FAFAF9",
|
||
neutral_0: str = "FFFFFF",
|
||
header_text: str = "FFFFFF",
|
||
cf_positive_bg: str = "E8F5E9",
|
||
cf_negative_bg: str = "FDEDEC",
|
||
cf_warning_bg: str = "FEF9E7",
|
||
) -> _Palette:
|
||
return {
|
||
"PRIMARY": primary,
|
||
"PRIMARY_LIGHT": primary_light,
|
||
"SECONDARY": primary_light,
|
||
"ACCENT_POSITIVE": accent_positive,
|
||
"ACCENT_NEGATIVE": accent_negative,
|
||
"ACCENT_WARNING": accent_warning,
|
||
"NEUTRAL_900": neutral_900,
|
||
"NEUTRAL_600": neutral_600,
|
||
"NEUTRAL_200": neutral_200,
|
||
"NEUTRAL_100": neutral_100,
|
||
"NEUTRAL_50": neutral_50,
|
||
"NEUTRAL_0": neutral_0,
|
||
"HEADER_TEXT": header_text,
|
||
"CF_POSITIVE_BG": cf_positive_bg,
|
||
"CF_NEGATIVE_BG": cf_negative_bg,
|
||
"CF_WARNING_BG": cf_warning_bg,
|
||
"CHART_COLORS": [primary, accent_positive, accent_warning, accent_negative, neutral_600],
|
||
}
|
||
|
||
|
||
# ============================================================
|
||
# §2 Legacy Palettes (6 original styles)
|
||
# ============================================================
|
||
|
||
# -- Professional: formal business, universal default --
|
||
PROFESSIONAL = _make_palette(
|
||
primary="1B2A4A",
|
||
primary_light="D6E4F0",
|
||
)
|
||
|
||
# -- Warm: warm and vibrant, high impact --
|
||
WARM = _make_palette(
|
||
primary="B85C1E",
|
||
primary_light="F5E6D5",
|
||
accent_positive="2E7D32",
|
||
accent_negative="C62828",
|
||
accent_warning="E65100",
|
||
neutral_900="3E2F1F",
|
||
neutral_600="9C8B78",
|
||
neutral_200="EAE0D5",
|
||
neutral_100="F7F2EC",
|
||
neutral_50="FBF8F5",
|
||
)
|
||
|
||
# -- Fresh: natural freshness, friendly and light --
|
||
FRESH = _make_palette(
|
||
primary="0E7C6B",
|
||
primary_light="D4F0EB",
|
||
accent_positive="2E9E5A",
|
||
accent_negative="D94F4F",
|
||
accent_warning="E6A023",
|
||
neutral_900="2F3735",
|
||
neutral_600="7A8C87",
|
||
neutral_200="DEE9E6",
|
||
neutral_100="F2F8F6",
|
||
neutral_50="F8FBFA",
|
||
)
|
||
|
||
# -- Elegant: premium restraint, minimalist black-white --
|
||
ELEGANT = _make_palette(
|
||
primary="2C2C2C",
|
||
primary_light="E5E5E5",
|
||
accent_positive="4A4A4A",
|
||
accent_negative="8B0000",
|
||
accent_warning="6B6B6B",
|
||
neutral_900="1A1A1A",
|
||
neutral_600="808080",
|
||
neutral_200="D4D4D4",
|
||
neutral_100="F0F0F0",
|
||
neutral_50="F8F8F8",
|
||
)
|
||
|
||
# -- Creative: artistic personality, distinctive --
|
||
CREATIVE = _make_palette(
|
||
primary="6C5B7B",
|
||
primary_light="E4DDE8",
|
||
accent_positive="6B9E78",
|
||
accent_negative="C06C7E",
|
||
accent_warning="C4A46A",
|
||
neutral_900="3E3A42",
|
||
neutral_600="9590A0",
|
||
neutral_200="E0DCE4",
|
||
neutral_100="F3F1F5",
|
||
neutral_50="F9F8FA",
|
||
)
|
||
|
||
# -- Vibrant: high-saturation multi-color, data display --
|
||
VIBRANT = _make_palette(
|
||
primary="2563EB",
|
||
primary_light="DBEAFE",
|
||
accent_positive="16A34A",
|
||
accent_negative="DC2626",
|
||
accent_warning="EA580C",
|
||
neutral_900="1E293B",
|
||
neutral_600="64748B",
|
||
neutral_200="E2E8F0",
|
||
neutral_100="F1F5F9",
|
||
neutral_50="F8FAFC",
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# §3 Premium Palettes (8 curated themes, "high-end feel" series)
|
||
# ============================================================
|
||
|
||
# -- A · MUJI breathing feel: restrained minimalism, pencil on paper --
|
||
MUJI = _make_palette(
|
||
primary="2C2C2C",
|
||
primary_light="F2F1EE",
|
||
accent_positive="5B8C5A",
|
||
accent_negative="C25450",
|
||
accent_warning="C9A84C",
|
||
neutral_900="2C2C2C",
|
||
neutral_600="999999",
|
||
neutral_200="E8E6E1",
|
||
neutral_100="F9F9F7",
|
||
neutral_50="FCFCFB",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- B · Aesop sandstone: earth tones, premium skincare packaging --
|
||
AESOP = _make_palette(
|
||
primary="3D3229",
|
||
primary_light="EDE8E0",
|
||
accent_positive="6B8F71",
|
||
accent_negative="B85C4A",
|
||
accent_warning="C4975A",
|
||
neutral_900="4A4038",
|
||
neutral_600="8C7B6B",
|
||
neutral_200="DDD5C9",
|
||
neutral_100="FAF8F5",
|
||
neutral_50="FDFCFA",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- C · Dieter Rams Industrial: Less but better --
|
||
DIETER_RAMS = _make_palette(
|
||
primary="1A1A1A",
|
||
primary_light="F7F7F7",
|
||
accent_positive="2D8C6F",
|
||
accent_negative="D44D3C",
|
||
accent_warning="D4920A",
|
||
neutral_900="1A1A1A",
|
||
neutral_600="787878",
|
||
neutral_200="E5E5E5",
|
||
neutral_100="F7F7F7",
|
||
neutral_50="FAFAFA",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- D · Kinfolk cream publication: independent magazine typography, slow-life aesthetic --
|
||
KINFOLK = _make_palette(
|
||
primary="5C524C",
|
||
primary_light="F0ECE7",
|
||
accent_positive="8DAA7F",
|
||
accent_negative="C9776A",
|
||
accent_warning="C9A96A",
|
||
neutral_900="5C524C",
|
||
neutral_600="BEB5AD",
|
||
neutral_200="EAE5DF",
|
||
neutral_100="FDFCFA",
|
||
neutral_50="FEFDFB",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- E · Céline pure black-white: monochrome, fashion house coldness --
|
||
CELINE = _make_palette(
|
||
primary="000000",
|
||
primary_light="FAFAFA",
|
||
accent_positive="4A7C59",
|
||
accent_negative="A63D2F",
|
||
accent_warning="8C7A3C",
|
||
neutral_900="000000",
|
||
neutral_600="ADADAD",
|
||
neutral_200="E0E0E0",
|
||
neutral_100="FAFAFA",
|
||
neutral_50="FDFDFD",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- F · Bottega dark green: Italian luxury, deep forest green --
|
||
BOTTEGA = _make_palette(
|
||
primary="2D4A3E",
|
||
primary_light="E8F0EB",
|
||
accent_positive="5FA67A",
|
||
accent_negative="C2694B",
|
||
accent_warning="B89B4A",
|
||
neutral_900="3B5249",
|
||
neutral_600="7A9B8C",
|
||
neutral_200="D4E3DB",
|
||
neutral_100="F6FAF8",
|
||
neutral_50="F9FCFA",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- G · Chanel champagne gold: Chanel elegance, beige + golden brown --
|
||
CHANEL = _make_palette(
|
||
primary="1C1917",
|
||
primary_light="E7DFD4",
|
||
accent_positive="A3845B",
|
||
accent_negative="B0413E",
|
||
accent_warning="C4975A",
|
||
neutral_900="1C1917",
|
||
neutral_600="A39888",
|
||
neutral_200="E7E0D5",
|
||
neutral_100="FDFBF7",
|
||
neutral_50="FEFDFB",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- H · Bloomberg deep blue: financial terminal, high-density data aesthetic --
|
||
BLOOMBERG = _make_palette(
|
||
primary="0D1B2A",
|
||
primary_light="D6E0EB",
|
||
accent_positive="10B981",
|
||
accent_negative="EF4444",
|
||
accent_warning="F59E0B",
|
||
neutral_900="0D1B2A",
|
||
neutral_600="708DA8",
|
||
neutral_200="D6E0EB",
|
||
neutral_100="F4F7FA",
|
||
neutral_50="F8FAFB",
|
||
header_text="FFFFFF",
|
||
)
|
||
|
||
# -- Original Blue/Black: original blue-black color scheme (Round 1 #1/#6 style) --
|
||
ORIGINAL_BLUE = _make_palette(
|
||
primary="1B2A4A",
|
||
primary_light="D6E4F0",
|
||
accent_positive="2E8B57",
|
||
accent_negative="EB5757",
|
||
accent_warning="F2994A",
|
||
neutral_900="333333",
|
||
neutral_600="666666",
|
||
neutral_200="E0E0E0",
|
||
neutral_100="F5F5F5",
|
||
neutral_50="FAFAFA",
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# §4 Registry
|
||
# ============================================================
|
||
|
||
PALETTE_REGISTRY: Dict[str, _Palette] = {
|
||
# Legacy (removed: fresh, vibrant)
|
||
"professional": PROFESSIONAL,
|
||
"warm": WARM,
|
||
"elegant": ELEGANT,
|
||
"creative": CREATIVE,
|
||
# Premium (high-end feel)
|
||
"muji": MUJI,
|
||
"aesop": AESOP,
|
||
# dieter_rams removed — header too dark, poor readability
|
||
"kinfolk": KINFOLK,
|
||
"celine": CELINE,
|
||
"bottega": BOTTEGA,
|
||
"chanel": CHANEL,
|
||
"bloomberg": BLOOMBERG,
|
||
"original_blue": ORIGINAL_BLUE,
|
||
}
|
||
|
||
# Aliases for convenience
|
||
PALETTE_REGISTRY["muji_breathing"] = MUJI
|
||
PALETTE_REGISTRY["sandstone"] = AESOP
|
||
PALETTE_REGISTRY["industrial"] = BLOOMBERG # was dieter_rams, redirected
|
||
PALETTE_REGISTRY["cream"] = KINFOLK
|
||
PALETTE_REGISTRY["monochrome"] = CELINE
|
||
PALETTE_REGISTRY["forest_green"] = BOTTEGA
|
||
PALETTE_REGISTRY["champagne"] = CHANEL
|
||
PALETTE_REGISTRY["terminal"] = BLOOMBERG
|
||
PALETTE_REGISTRY["classic_blue"] = ORIGINAL_BLUE
|
||
|
||
|
||
# ============================================================
|
||
# §5 Keyword Matching (three-step)
|
||
# ============================================================
|
||
|
||
# Step 1: Explicit style keywords (highest priority)
|
||
_STYLE_KEYWORDS: Dict[str, list[str]] = {
|
||
"professional": [
|
||
"正式", "商务", "专业", "沉稳", "稳重", "professional", "formal",
|
||
"corporate", "business",
|
||
],
|
||
"warm": [
|
||
"温暖", "活力", "热情", "热烈", "暖色", "温馨", "warm", "energetic",
|
||
"活跃", "热力",
|
||
],
|
||
"elegant": [
|
||
"极简", "简约", "elegant", "minimal",
|
||
"清新", "自然", "清爽", "淡雅", "浅色", "明亮", "fresh",
|
||
"natural", "clean", "light", "素雅",
|
||
"多彩", "丰富", "鲜艳", "vivid", "colorful", "明快",
|
||
"高饱和", "鲜明", "亮色",
|
||
],
|
||
"creative": [
|
||
"文艺", "个性", "紫色", "莫兰迪", "creative", "artistic",
|
||
"柔和", "雅致",
|
||
],
|
||
# Premium themes
|
||
"muji": [
|
||
"muji", "无印", "呼吸感", "白纸", "铅笔", "素净", "无印良品",
|
||
],
|
||
"aesop": [
|
||
"aesop", "沙岩", "大地色", "护肤", "泥土", "陶", "terracotta",
|
||
],
|
||
"bloomberg": [
|
||
"bloomberg", "终端", "深蓝", "terminal", "金融终端", "数据终端",
|
||
"rams", "dieter", "工业", "德系", "包豪斯", "bauhaus", "less but better",
|
||
"工业风",
|
||
],
|
||
"kinfolk": [
|
||
"kinfolk", "奶油", "刊物", "杂志", "慢生活", "latte", "拿铁",
|
||
],
|
||
"celine": [
|
||
"celine", "黑白", "时装", "冷冽", "mono", "纯黑", "monochrome",
|
||
],
|
||
"bottega": [
|
||
"bottega", "墨绿", "深绿", "森林", "橄榄", "绿色", "forest",
|
||
"贵气", "奢牌",
|
||
],
|
||
"chanel": [
|
||
"chanel", "米金", "金棕", "香奈儿", "champagne", "米色", "奶茶",
|
||
],
|
||
"original_blue": [
|
||
"原始", "经典蓝", "classic blue", "original", "传统蓝",
|
||
],
|
||
}
|
||
|
||
# Step 2: Scene keywords → infer style (lower priority)
|
||
_SCENE_TO_STYLE: Dict[str, str] = {
|
||
# Sales / Marketing / Ops → warm
|
||
"销售": "warm", "营销": "warm", "运营": "warm", "客户": "warm",
|
||
"业绩": "warm", "KPI": "warm", "GMV": "warm", "转化": "warm",
|
||
"漏斗": "warm", "签约": "warm", "提成": "warm", "电商": "warm",
|
||
"sales": "warm", "marketing": "warm", "campaign": "warm",
|
||
# Education / Medical → muji (was fresh, now removed)
|
||
"成绩": "muji", "考试": "muji", "学生": "muji", "课程": "muji",
|
||
"教育": "muji", "GPA": "muji", "学校": "muji", "班级": "muji",
|
||
"医疗": "muji", "健康": "muji", "患者": "muji", "体检": "muji",
|
||
"医院": "muji", "科室": "muji", "护理": "muji",
|
||
"环保": "muji",
|
||
"education": "muji", "medical": "muji", "health": "muji",
|
||
# Design / Brand → creative
|
||
"设计": "creative", "创意": "creative", "品牌": "creative",
|
||
"UI": "creative", "UX": "creative", "作品": "creative",
|
||
"视觉": "creative", "素材": "creative",
|
||
"design": "creative", "brand": "creative", "portfolio": "creative",
|
||
# Formal / Reporting → professional
|
||
"汇报": "professional", "提案": "professional", "会议": "professional",
|
||
"述职": "professional", "总结": "professional", "报告": "professional",
|
||
"年报": "professional", "季报": "professional", "月报": "professional",
|
||
"财务": "professional", "财报": "professional", "预算": "professional",
|
||
"审计": "professional", "咨询": "professional", "战略": "professional",
|
||
"finance": "professional", "budget": "professional", "report": "professional",
|
||
# Minimal / Premium → elegant
|
||
"premium": "elegant", "luxury": "elegant",
|
||
# Finance data → bloomberg
|
||
"股票": "bloomberg", "基金": "bloomberg", "投资": "bloomberg",
|
||
"交易": "bloomberg", "行情": "bloomberg", "K线": "bloomberg",
|
||
"stock": "bloomberg", "trading": "bloomberg", "portfolio_fin": "bloomberg",
|
||
# High-end / Luxury brand → chanel
|
||
"奢侈": "chanel", "高端": "chanel", "高级": "chanel",
|
||
}
|
||
|
||
|
||
def _match_style_keywords(text: str) -> Optional[str]:
|
||
"""Step 1: Match explicit style keywords. Returns style name or None."""
|
||
text_lower = text.lower()
|
||
best_match = None
|
||
best_score = 0
|
||
for style, keywords in _STYLE_KEYWORDS.items():
|
||
score = sum(1 for kw in keywords if kw.lower() in text_lower)
|
||
if score > best_score:
|
||
best_score = score
|
||
best_match = style
|
||
return best_match if best_score > 0 else None
|
||
|
||
|
||
def _infer_from_scene(text: str) -> Optional[str]:
|
||
"""Step 2: Infer style from scene/content keywords. Returns style name or None."""
|
||
text_lower = text.lower()
|
||
votes: Dict[str, int] = {}
|
||
for keyword, style in _SCENE_TO_STYLE.items():
|
||
if keyword.lower() in text_lower:
|
||
votes[style] = votes.get(style, 0) + 1
|
||
if not votes:
|
||
return None
|
||
return max(votes, key=votes.get)
|
||
|
||
|
||
# ============================================================
|
||
# §6 Public API
|
||
# ============================================================
|
||
|
||
def get_palette(style: str = "professional") -> _Palette:
|
||
"""Get a palette by style name. Falls back to professional."""
|
||
return PALETTE_REGISTRY.get(style, PROFESSIONAL)
|
||
|
||
|
||
def resolve_palette(prompt: str) -> _Palette:
|
||
"""
|
||
Auto-detect style from user prompt (three-step):
|
||
1. Explicit style keywords → direct match
|
||
2. Scene/content keywords → infer style
|
||
3. No match → professional (safe default)
|
||
"""
|
||
style = detect_style(prompt)
|
||
return get_palette(style)
|
||
|
||
|
||
def resolve_palette_with_info(prompt: str) -> Tuple[_Palette, str]:
|
||
"""Same as resolve_palette but also returns the detected style name."""
|
||
style = detect_style(prompt)
|
||
return get_palette(style), style
|
||
|
||
|
||
def detect_style(prompt: str) -> str:
|
||
"""
|
||
Detect style from prompt. Three-step priority:
|
||
1. Explicit style keywords
|
||
2. Scene keywords → infer style
|
||
3. Default: professional
|
||
"""
|
||
style = _match_style_keywords(prompt)
|
||
if style:
|
||
return style
|
||
style = _infer_from_scene(prompt)
|
||
if style:
|
||
return style
|
||
return "professional"
|
||
|
||
|
||
def list_available() -> list[str]:
|
||
"""Return list of available style names (no aliases)."""
|
||
# Return only canonical names, not aliases
|
||
canonical = [
|
||
"professional", "warm", "elegant", "creative",
|
||
"muji", "aesop", "kinfolk", "celine", "bottega",
|
||
"chanel", "bloomberg", "original_blue",
|
||
]
|
||
return canonical
|
||
|
||
|
||
def apply_palette(palette: _Palette, module_globals: dict):
|
||
"""
|
||
Inject palette tokens into a module's global namespace.
|
||
Designed to be called from base.py to override its color constants.
|
||
"""
|
||
key_map = {
|
||
"PRIMARY": "PRIMARY",
|
||
"PRIMARY_LIGHT": "PRIMARY_LIGHT",
|
||
"SECONDARY": "SECONDARY",
|
||
"ACCENT_POSITIVE": "ACCENT_POSITIVE",
|
||
"ACCENT_NEGATIVE": "ACCENT_NEGATIVE",
|
||
"ACCENT_WARNING": "ACCENT_WARNING",
|
||
"NEUTRAL_900": "NEUTRAL_900",
|
||
"NEUTRAL_600": "NEUTRAL_600",
|
||
"NEUTRAL_200": "NEUTRAL_200",
|
||
"NEUTRAL_100": "NEUTRAL_100",
|
||
"NEUTRAL_50": "NEUTRAL_50",
|
||
"NEUTRAL_0": "NEUTRAL_0",
|
||
"CHART_COLORS": "CHART_COLORS",
|
||
}
|
||
for palette_key, global_key in key_map.items():
|
||
if palette_key in palette:
|
||
module_globals[global_key] = palette[palette_key]
|