Files
2026-06-06 05:21:10 +00:00

522 lines
16 KiB
Python
Executable File
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.
"""
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]