linebot-image-combo
LINE Bot 題庫與筆記圖片製作的 Gemini + matplotlib 連續技。整合 AI 生成高品質底圖與程式化中文標籤,產出符合 LINE Bot 規格的圖片(< 300KB、手機可讀)。
SKILL.md
Full skill instructions
LINE Bot 圖片製作連續技
概述
結合 Gemini AI 生成高品質底圖 + matplotlib 加上正確中文標籤的連續技,專為 LINE Bot 題庫與筆記設計。
為什麼需要連續技?
| 方法 | 優點 | 缺點 |
|---|---|---|
| 純 matplotlib | 中文標籤正確 | 圖形簡陋、不夠精緻 |
| 純 Gemini | 圖片品質高 | 中文常出錯(亂碼) |
| 連續技 | 品質高 + 中文正確 | 需要兩步驟 |
LINE Bot 圖片規格(重要!)
硬性限制
| 項目 | 限制 | 說明 |
|---|---|---|
| 檔案大小 | < 300KB | 超過會載入緩慢或失敗 |
| 圖片格式 | JPEG, PNG | 不支援 GIF 動圖 |
| 圖片來源 | HTTPS | 必須是安全連線 |
| Hero 比例 | 4:3 或 16:9 | Flex Message 推薦 |
手機閱讀限制
LINE Bot 圖片在手機上無法放大! 必須確保:
- 字體夠大(手機上直接可讀)
- 圖片不要太寬(手機螢幕約 360-400px)
- 重要資訊不要太密集
優化後的圖片規格
尺寸標準(針對 LINE Bot 優化)
| 用途 | figsize | DPI | 輸出尺寸 | 檔案大小 |
|---|---|---|---|---|
| 題庫圖片 | (8, 6) | 120 | ~960×720px | < 150KB |
| 筆記圖片 | (8, 8) | 120 | ~960×960px | < 200KB |
| 橫向圖表 | (10, 6) | 120 | ~1200×720px | < 180KB |
| 大型圖(需壓縮) | (10, 8) | 100 | ~1000×800px | < 250KB |
字體大小標準(手機可讀)
# LINE Bot 專用字體大小(比一般用途更大)
FONT_TITLE = 24 # 圖片標題
FONT_LABEL = 18 # 標籤文字(主要內容)
FONT_SUBLABEL = 14 # 次要標籤
FONT_NOTE = 12 # 註解(最小值,謹慎使用)
對比原本的設定:
| 項目 | 原設定 | LINE Bot 優化 | 原因 |
|---|---|---|---|
| figsize | (14, 12) | (8, 6) | 原本太大,檔案超過 300KB |
| DPI | 150 | 120 | 降低以控制檔案大小 |
| 標籤字體 | 11-12 | 16-18 | 原本在手機上太小 |
連續技流程
Step 1:Gemini 生成無標籤底圖
cd ~/.claude/skills/fanpage-article-writer/scripts
# 生成無標籤的高品質圖片
python generate_image.py "圖片描述,WITHOUT any text labels" \
--output "底圖.png" \
--model gemini
Prompt 技巧:
✅ 正確:
"A detailed anatomical diagram of [主題].
Clean medical illustration style WITHOUT any text labels or annotations.
High quality educational diagram, white background."
❌ 錯誤:
"心臟解剖圖,標示中文說明" # 中文標籤會出錯
Step 2:matplotlib 加中文標籤
# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from PIL import Image
import io
# 中文字體設定(必須)
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
def add_labels_for_linebot(base_image_path, output_path, labels, title):
"""
為 Gemini 底圖加上中文標籤(LINE Bot 優化版)
Parameters:
- base_image_path: Gemini 生成的底圖路徑
- output_path: 輸出路徑
- labels: 標籤列表 [(x%, y%, 文字, 箭頭x%, 箭頭y%), ...]
- title: 圖片標題
"""
img = mpimg.imread(base_image_path)
# LINE Bot 優化尺寸
fig, ax = plt.subplots(figsize=(8, 6))
ax.imshow(img)
ax.axis('off')
h, w = img.shape[:2]
for lx, ly, text, ax_x, ax_y in labels:
x = lx * w
y = ly * h
arrow_x = ax_x * w
arrow_y = ax_y * h
bbox_props = dict(
boxstyle='round,pad=0.3',
facecolor='white',
edgecolor='#333333',
alpha=0.92,
linewidth=1.5
)
ax.annotate(
text,
xy=(arrow_x, arrow_y),
xytext=(x, y),
fontsize=16, # LINE Bot 優化:字體加大
fontweight='bold',
ha='center',
va='center',
bbox=bbox_props,
arrowprops=dict(
arrowstyle='-|>',
color='#333333',
lw=1.5,
connectionstyle='arc3,rad=0.1'
)
)
ax.set_title(title, fontsize=20, fontweight='bold', pad=10)
# 儲存為 LINE Bot 優化格式
save_for_linebot(fig, output_path)
plt.close()
def save_for_linebot(fig, output_path, max_size_kb=280):
"""
儲存圖片並確保 < 300KB
自動調整品質以符合 LINE Bot 限制
"""
# 先儲存為 PNG
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=120, bbox_inches='tight',
facecolor='white', pad_inches=0.05)
buf.seek(0)
# 檢查大小
size_kb = len(buf.getvalue()) / 1024
if size_kb <= max_size_kb:
# 大小 OK,直接儲存
with open(output_path, 'wb') as f:
f.write(buf.getvalue())
print(f"已儲存: {output_path} ({size_kb:.1f} KB)")
else:
# 太大,轉換為 JPEG 並壓縮
img = Image.open(buf)
if img.mode == 'RGBA':
img = img.convert('RGB')
# 嘗試不同品質
for quality in [85, 75, 65, 55]:
jpeg_buf = io.BytesIO()
img.save(jpeg_buf, format='JPEG', quality=quality, optimize=True)
jpeg_size = len(jpeg_buf.getvalue()) / 1024
if jpeg_size <= max_size_kb:
jpeg_path = output_path.rsplit('.', 1)[0] + '.jpg'
with open(jpeg_path, 'wb') as f:
f.write(jpeg_buf.getvalue())
print(f"已儲存: {jpeg_path} ({jpeg_size:.1f} KB, quality={quality})")
return
# 如果還是太大,縮小尺寸
img.thumbnail((800, 800), Image.LANCZOS)
jpeg_buf = io.BytesIO()
img.save(jpeg_buf, format='JPEG', quality=70, optimize=True)
jpeg_path = output_path.rsplit('.', 1)[0] + '.jpg'
with open(jpeg_path, 'wb') as f:
f.write(jpeg_buf.getvalue())
print(f"已儲存(縮小): {jpeg_path} ({len(jpeg_buf.getvalue())/1024:.1f} KB)")
完整範例:心臟解剖圖
1. 生成底圖
cd ~/.claude/skills/fanpage-article-writer/scripts
python generate_image.py \
"A detailed anatomical cross-section of the human heart showing four chambers, septum, valves, and major blood vessels. Clean medical illustration style WITHOUT any text labels. Blue for right side, red for left side. White background." \
--output "C:/Users/user/Documents/temp/heart_base.png" \
--model gemini
2. 加標籤腳本
# heart_linebot.py
# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from PIL import Image
import io
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False
def create_heart_diagram():
img = mpimg.imread('C:/Users/user/Documents/temp/heart_base.png')
# LINE Bot 優化尺寸
fig, ax = plt.subplots(figsize=(8, 6))
ax.imshow(img)
ax.axis('off')
h, w = img.shape[:2]
# 標籤:(標籤x%, 標籤y%, 文字, 箭頭x%, 箭頭y%)
labels = [
(0.08, 0.15, '上腔靜脈', 0.22, 0.20),
(0.50, 0.05, '主動脈', 0.42, 0.18),
(0.85, 0.10, '肺動脈', 0.60, 0.18),
(0.05, 0.40, '右心房', 0.25, 0.38),
(0.92, 0.35, '左心房', 0.72, 0.35),
(0.05, 0.65, '右心室', 0.35, 0.60),
(0.92, 0.65, '左心室', 0.65, 0.60),
(0.50, 0.90, '心室中隔', 0.50, 0.70),
]
for lx, ly, text, ax_x, ax_y in labels:
ax.annotate(
text,
xy=(ax_x * w, ax_y * h),
xytext=(lx * w, ly * h),
fontsize=16,
fontweight='bold',
ha='center',
va='center',
bbox=dict(boxstyle='round,pad=0.3', facecolor='white',
edgecolor='#333', alpha=0.9),
arrowprops=dict(arrowstyle='-|>', color='#333', lw=1.5)
)
# 圖例
ax.text(0.20 * w, 0.95 * h, '● 含氧血', fontsize=12, color='#C62828',
fontweight='bold', bbox=dict(facecolor='white', alpha=0.9))
ax.text(0.75 * w, 0.95 * h, '● 缺氧血', fontsize=12, color='#1565C0',
fontweight='bold', bbox=dict(facecolor='white', alpha=0.9))
ax.set_title('心臟結構解剖圖', fontsize=18, fontweight='bold', pad=8)
# 儲存(自動壓縮)
save_for_linebot(fig, 'heart_anatomy_linebot.png')
plt.close()
def save_for_linebot(fig, output_path, max_kb=280):
"""儲存並壓縮到 LINE Bot 規格"""
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=120, bbox_inches='tight',
facecolor='white', pad_inches=0.03)
buf.seek(0)
size_kb = len(buf.getvalue()) / 1024
if size_kb <= max_kb:
with open(output_path, 'wb') as f:
f.write(buf.getvalue())
print(f"已儲存: {output_path} ({size_kb:.1f} KB)")
else:
# 轉 JPEG 壓縮
img = Image.open(buf).convert('RGB')
for q in [85, 75, 65]:
jbuf = io.BytesIO()
img.save(jbuf, 'JPEG', quality=q, optimize=True)
if len(jbuf.getvalue())/1024 <= max_kb:
jpg_path = output_path.replace('.png', '.jpg')
with open(jpg_path, 'wb') as f:
f.write(jbuf.getvalue())
print(f"已儲存: {jpg_path} ({len(jbuf.getvalue())/1024:.1f} KB)")
return
print("警告:無法壓縮到目標大小")
if __name__ == '__main__':
create_heart_diagram()
API Key 管理
Gemini API Key 設定位置:
~/.claude/skills/fanpage-article-writer/scripts/api_keys.json
檢查 API Key 狀態:
cd ~/.claude/skills/fanpage-article-writer/scripts
python generate_image.py --status
圖片命名規範
題庫圖片
ch{章}-{節}-q{題號}-{描述}.png # 題目圖
ch{章}-{節}-a{題號}-{描述}.png # 答案解析圖
筆記圖片
{主題}_{內容}.png
例:heart_anatomy.png, heart_conduction.png
常見問題
圖片太大(> 300KB)
- 降低 DPI:
dpi=100或dpi=80 - 縮小 figsize:
figsize=(6, 4) - 轉 JPEG:
quality=75 - 縮小尺寸:
img.thumbnail((800, 800))
字體太小
- 標籤字體至少
fontsize=14 - 標題字體至少
fontsize=18 - 避免密集標籤,必要時分多張圖
Gemini 中文出錯
- Prompt 中加
WITHOUT any text labels - 使用 matplotlib 後製所有文字
LINE 圖片快取
更新圖片後加版本參數:
https://example.com/images/heart.png?v=2
相關 Skill
linebot-notebook-design:Flex Message 設計規範quiz-builder:題庫系統專用指南fanpage-article-writer:Gemini API 腳本位置
Noto Sans CJK TC 字體(罕見中文字支援)
問題
微軟正黑體不支援某些罕見中文字,例如超重元素 105-118 的名稱:
| 原子序 | 符號 | 中文名 | 微軟正黑體 |
|---|---|---|---|
| 105 | Db | 𨧀 | ⬜ 顯示為方框 |
| 106 | Sg | 𨭎 | ⬜ 顯示為方框 |
| 107 | Bh | 𨨏 | ⬜ 顯示為方框 |
| 108 | Hs | 𨭆 | ⬜ 顯示為方框 |
| 117 | Ts | 鿬 | ⬜ 顯示為方框 |
| 118 | Og | 鿫 | ⬜ 顯示為方框 |
解決方案:使用 Noto Sans CJK TC
Google 的 Noto Sans CJK TC 字體完整支援所有 CJK 字符。
下載字體:
# npm 安裝方式
npm install @aspect-build/aspect-fonts-noto-sans-cjk-tc
# 或直接下載 OTF 檔
# https://github.com/googlefonts/noto-cjk/releases
matplotlib 使用方式:
import matplotlib.font_manager as fm
import matplotlib.patheffects as path_effects
# 載入 Noto Sans CJK TC 字體
NOTO_FONT_PATH = "C:/Users/user/Documents/temp/NotoSansCJKtc-Regular.otf"
noto_font = fm.FontProperties(fname=NOTO_FONT_PATH)
# 使用 fontproperties 參數
ax.text(0.95, 0.08, "鿬", # 罕見字
fontsize=48, color='white', fontweight='bold',
fontproperties=noto_font, # 指定字體
path_effects=[
path_effects.withStroke(linewidth=3, foreground='black') # 黑邊
])
重點:
- 使用
fontproperties=參數,不是fontfamily= path_effects.withStroke()可加黑邊讓白字更清晰- 字體檔約 16MB,不要放進 Git repo
Breaking Bad 風格元素圖片生成
使用 Gemini API 直接生成 Breaking Bad 風格的元素週期表 hero 圖片。
API 直接呼叫方式
from google import genai
from google.genai import types
# 初始化 client
client = genai.Client(api_key=API_KEY)
# 生成圖片
response = client.models.generate_content(
model="gemini-2.0-flash-exp",
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=['image', 'text'] # 重要!
)
)
# 取得圖片資料
for part in response.candidates[0].content.parts:
if part.inline_data is not None:
image_data = part.inline_data.data
with open(output_path, 'wb') as f:
f.write(image_data)
Breaking Bad 風格 Prompt 範例
def generate_prompt(element, color_hex):
symbol = element['symbol'] # e.g., "H"
rest_name = "ydrogen" # 去掉符號後的名稱
atomic_num = 1
atomic_mass = "1.008"
return f"""Create a Breaking Bad TV series style periodic table element card.
CRITICAL REQUIREMENTS:
1. BACKGROUND: Dark navy blue (#1a1a2e) with {color_hex} colored smoke/vapor filling the ENTIRE image background.
2. ELEMENT BOX (center-left):
- Green square box (#00A300) with white border (3px)
- Large white letter '{symbol}' in the center (bold, sans-serif)
- Small white number '{atomic_num}' in top-right corner of the box
3. TEXT (to the right of the box):
- '{rest_name}' in white, same baseline as the symbol
- Below that: '{atomic_mass}' in smaller white text
4. LAYOUT:
- Design should be centered and fit a 2:1 aspect ratio (wide format)
- NO solid colored panels blocking the smoke background
5. STYLE:
- Cinematic, dramatic lighting
- Similar to Breaking Bad title card aesthetic
DO NOT add any other text, watermarks, or decorations."""
後處理:裁切 + 中文標籤 + 壓縮
def post_process_image(input_path, output_path, element, target_width=1200):
"""後處理:裁切成 2:1、加中文標籤、壓縮到 < 300KB"""
from PIL import Image
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
import matplotlib.font_manager as fm
# 載入 Noto 字體
noto_font = fm.FontProperties(fname=NOTO_FONT_PATH)
# 開啟圖片
img = Image.open(input_path)
width, height = img.size
# 裁切成 2:1 比例(從中心裁切)
target_height = width // 2
if height > target_height:
top = (height - target_height) // 2
img = img.crop((0, top, width, top + target_height))
# 調整大小
target_height = target_width // 2
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
# 轉換為 RGB
if img.mode != 'RGB':
background = Image.new('RGB', img.size, (26, 26, 46))
if img.mode == 'RGBA':
background.paste(img, mask=img.split()[3])
else:
background.paste(img)
img = background
# 用 matplotlib 加中文標籤
fig, ax = plt.subplots(figsize=(12, 6), dpi=100)
ax.imshow(img)
ax.axis('off')
# 加上中文名稱(右下角)
ax.text(0.95, 0.08, element['name_zh'],
transform=ax.transAxes,
fontsize=48, color='white', fontweight='bold',
ha='right', va='bottom',
fontproperties=noto_font,
path_effects=[
path_effects.withStroke(linewidth=3, foreground='black')
])
# 儲存並壓縮
plt.tight_layout(pad=0)
plt.savefig(output_path, dpi=100, bbox_inches='tight', pad_inches=0,
format='jpeg', quality=85)
plt.close()
生成結果
- 輸出尺寸:1200×600px(2:1 比例)
- 檔案大小:~80-150KB(符合 LINE Bot 限制)
- 風格:Breaking Bad 電視劇風格,深藍背景 + 彩色煙霧
Windows Console 編碼問題
在 Windows 上執行 Python 腳本時,若遇到 UnicodeEncodeError,在程式開頭加入:
import sys
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
變更記錄
| 日期 | 版本 | 變更內容 |
|---|---|---|
| 2026-01-18 | 1.0 | 初版制定,整合 Gemini + matplotlib 連續技 |
| 2026-01-20 | 1.1 | 新增 Noto Sans CJK TC 字體、Breaking Bad 風格生成、Windows 編碼修正 |
