Files
wehub-resource-sync 3a46cbf880
Site (mkdocs + mdBook) → GitHub Pages / Build mkdocs (root) + mdBook (/book/) (push) Has been cancelled
Site (mkdocs + mdBook) → GitHub Pages / Deploy to GitHub Pages (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:13:20 +08:00

266 lines
8.8 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.
"""Generate the Stage 5 Claude Code Stack diagram using PIL.
Produces resources/diagrams/stage5-stack{,en}.png at 1920×1080.
Why PIL not SVG: cairosvg's font fallback fails on Windows for CJK +
emoji. PIL takes explicit TTF paths so we control exactly what renders.
Why no emoji icons: rendering color emojis (CBDT / COLR fonts) is
unreliable across PIL versions. Colored circle bullets in the layer
stroke color work better and look more professional anyway.
Run:
python scripts/generate-stage5-stack.py [--lang zh-TW|en]
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
LAYERS = [
# name, sub, ability_zh-TW, ability_en, ability_zh-Hans, fill, stroke
(
"Plugins / Marketplaces",
"5.4 — packaging",
"把 Skills、hooks、commands、MCP 設定打包發佈",
"Package & ship Skills, hooks, commands, MCP configs as one unit",
"把 Skills、hooks、commands、MCP 设置打包发布",
"#fef3c7", "#b45309",
),
(
"Skills",
"5.3 — behavior",
"Claude Code 的行為包,可封裝 MCP tool",
"Behavior bundles for Claude Code that can wrap MCP tools",
"Claude Code 的行为包,可封装 MCP tool",
"#dbeafe", "#1e40af",
),
(
"MCP",
"5.2 — protocol",
"標準化協定,任何 LLM host 都能用任何 tool server",
"Standardized protocol — any LLM host can use any tool server",
"标准化协议,任何 LLM host 都能用任何 tool server",
"#dcfce7", "#166534",
),
(
"Tool Use / Function Calling",
"Stage 3",
"讓 LLM 呼叫你定義的 function",
"Let the LLM call functions you define",
"让 LLM 调用你定义的 function",
"#f3e8ff", "#6b21a8",
),
(
"Anthropic API + SDK",
"Stage 1, Stage 7",
"用程式存取 LLM",
"Programmatic access to the LLM",
"用程序访问 LLM",
"#fee2e2", "#991b1b",
),
(
"LLM (Claude)",
"foundation",
"底層基礎模型",
"Foundation model",
"底层基础模型",
"#e5e7eb", "#374151",
),
]
# Windows font paths
FONT_CJK_BOLD = r"C:\Windows\Fonts\msjhbd.ttc" # Microsoft JhengHei Bold
FONT_CJK_REG = r"C:\Windows\Fonts\msjh.ttc" # Microsoft JhengHei
FONT_LATIN_BOLD = r"C:\Windows\Fonts\seguisb.ttf" # Segoe UI Semibold
FONT_LATIN_REG = r"C:\Windows\Fonts\segoeui.ttf" # Segoe UI
def hex_to_rgb(h: str) -> tuple[int, int, int]:
h = h.lstrip("#")
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
def rounded_rect(draw, xy, radius, fill=None, outline=None, width=1):
"""Pillow's rounded_rectangle wrapper for older versions."""
if hasattr(draw, "rounded_rectangle"):
draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width)
else:
draw.rectangle(xy, fill=fill, outline=outline, width=width)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--lang", default="zh-TW", choices=["zh-TW", "en", "zh-Hans"])
args = parser.parse_args()
from PIL import Image, ImageDraw, ImageFont
W, H = 1920, 1080
lang = args.lang
title = "Claude Code Ecosystem Stack"
if lang == "en":
subtitle = "Each layer adds one capability · 4 sub-sections of Stage 5"
footer = ('Both tracks use this stack — Track A reads "how to use it", '
'Track B reads "how it works"')
elif lang == "zh-Hans":
subtitle = "Each layer adds one capability · Stage 5 的 4 个子章节"
footer = ("两条学习路径都会用到这个 stack — Track ACLI Power User"
"读“怎么用”,Track BAgent Builder)读“怎么运作”")
else:
subtitle = "Each layer adds one capability · Stage 5 之 4 個子章節"
footer = ("兩條學習路徑都會用到這個 stack — Track ACLI Power User"
"讀「怎麼用」,Track BAgent Builder)讀「怎麼運作」")
repo = "github.com/WenyuChiou/awesome-agentic-ai-zh"
# Pick fonts based on language. zh-Hans uses YaHei (msyh.ttc), zh-TW uses JhengHei (msjh.ttc)
if lang == "en":
f_title = ImageFont.truetype(FONT_LATIN_BOLD, 60)
f_sub = ImageFont.truetype(FONT_LATIN_REG, 26)
f_layer_name = ImageFont.truetype(FONT_LATIN_BOLD, 38)
f_layer_sub = ImageFont.truetype(FONT_LATIN_REG, 22)
f_ability = ImageFont.truetype(FONT_LATIN_REG, 22)
f_footer = ImageFont.truetype(FONT_LATIN_REG, 22)
else:
cjk_bold = FONT_CJK_BOLD if lang == "zh-TW" else r"C:\Windows\Fonts\msyhbd.ttc"
cjk_reg = FONT_CJK_REG if lang == "zh-TW" else r"C:\Windows\Fonts\msyh.ttc"
f_title = ImageFont.truetype(FONT_LATIN_BOLD, 60)
f_sub = ImageFont.truetype(cjk_reg, 26)
f_layer_name = ImageFont.truetype(cjk_bold, 36)
f_layer_sub = ImageFont.truetype(FONT_LATIN_REG, 22)
f_ability = ImageFont.truetype(cjk_reg, 22)
f_footer = ImageFont.truetype(cjk_reg, 22)
f_repo = ImageFont.truetype(FONT_LATIN_REG, 20)
img = Image.new("RGB", (W, H), color=(250, 250, 250))
d = ImageDraw.Draw(img)
# Title (top center)
title_y = 80
tw = d.textlength(title, font=f_title)
d.text((W/2 - tw/2, title_y), title, fill=(17, 17, 17), font=f_title)
# Subtitle
sub_y = 165
sw = d.textlength(subtitle, font=f_sub)
d.text((W/2 - sw/2, sub_y), subtitle, fill=(107, 114, 128), font=f_sub)
# Stack layout
stack_top = 230
stack_bottom = 920
layer_total = stack_bottom - stack_top
layer_h = layer_total / 6 # ~115
stack_x = 80
stack_w = 1080
ability_x = 1200
ability_w = 640
for i, (name, sub, abil_zh_tw, abil_en, abil_zh_hans, fill, stroke) in enumerate(LAYERS):
if lang == "en":
abil = abil_en
elif lang == "zh-Hans":
abil = abil_zh_hans
else:
abil = abil_zh_tw
y = stack_top + i * layer_h
# Slight pyramid widening for foundation feel
widen = i * 14
x = stack_x - widen / 2
w = stack_w + widen
rect_h = layer_h - 14
# Layer rect
rounded_rect(
d,
(x, y, x + w, y + rect_h),
radius=14,
fill=hex_to_rgb(fill),
outline=hex_to_rgb(stroke),
width=3,
)
# Colored disk bullet (left)
bullet_r = 16
bullet_x = x + 50
bullet_y = y + rect_h / 2
d.ellipse(
(bullet_x - bullet_r, bullet_y - bullet_r,
bullet_x + bullet_r, bullet_y + bullet_r),
fill=hex_to_rgb(stroke),
)
# Layer name + sub
text_x = bullet_x + 36
# Name vertical: slightly above midline
name_h = 38 if lang == "en" else 36
d.text(
(text_x, bullet_y - name_h + 4),
name,
fill=hex_to_rgb(stroke),
font=f_layer_name,
)
# Sub label (italic visual via gray + smaller)
d.text(
(text_x, bullet_y + 6),
sub,
fill=(107, 114, 128),
font=f_layer_sub,
)
# Ability column (right)
ay = y + 8
ah = rect_h - 16
rounded_rect(
d,
(ability_x, ay, ability_x + ability_w, ay + ah),
radius=10,
fill=(255, 255, 255),
outline=(229, 231, 235),
width=2,
)
# Ability text (vertically centered, wraps if needed)
# For now, single-line
d.text(
(ability_x + 24, ay + ah / 2 - 14),
abil,
fill=(55, 65, 81),
font=f_ability,
)
# Dotted divider between layers (visual hint of single stack)
if i < len(LAYERS) - 1:
sep_y = y + layer_h - 7
# Draw dotted line manually (PIL has no native dash)
x0 = stack_x + 40
x1 = stack_x + stack_w - 40
cur = x0
while cur < x1:
d.line(
[(cur, sep_y), (min(cur + 6, x1), sep_y)],
fill=(209, 213, 219),
width=1,
)
cur += 12
# Footer
footer_y = 1010
d.text((80, footer_y), footer, fill=(107, 114, 128), font=f_footer)
rw = d.textlength(repo, font=f_repo)
d.text((W - 80 - rw, footer_y + 2), repo, fill=(156, 163, 175), font=f_repo)
suffix = {"en": ".en", "zh-Hans": ".zh-Hans", "zh-TW": ""}[lang]
out = REPO_ROOT / "resources" / "diagrams" / f"stage5-stack{suffix}.png"
out.parent.mkdir(parents=True, exist_ok=True)
img.save(out, format="PNG", optimize=True)
print(f"[PIL] {out}")
return 0
if __name__ == "__main__":
sys.exit(main())