Files
EzVibe/scripts/generate_pet_sprites.py
e2hang 2a844e83a8 Initial commit: EzVibe AI 桌宠系统
- EmotionEngine: 5状态马尔可夫情绪机 + 蒙特卡洛转移
- VectorMemory: TF-IDF向量记忆 + SQLite持久化 + RAG检索
- AgentBrain: Ollama/OpenAI/Dummy三后端LLM
- BehaviorScheduler: 优先级/冷却/活跃度调度
- FastAPI服务器 + WebSocket实时推送
- perception: 键鼠监控 + 屏幕截图
- ui/pet_window: PySide6桌宠窗口 + 像素动画
- assets/pet: 5情绪各2帧像素艺术资源
2026-05-01 23:26:43 +08:00

271 lines
9.5 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
"""生成 EzVibe 桌宠像素艺术图片5 种情绪 × 2 帧)。"""
from PIL import Image
import os, math
# ── 画布参数 ────────────────────────────────────────────────────────────────
W, H = 64, 64
CX, CY, R = 32, 34, 22
# ── 调色板 ──────────────────────────────────────────────────────────────────
P = {
"bg": (0, 0, 0, 0),
"white": (255, 255, 255, 255),
"dark": (40, 40, 60, 255),
"pink": (255, 150, 180, 255),
"yellow": (255, 220, 100, 255),
"green": (80, 200, 120, 255),
"blue": (80, 160, 255, 255),
"red": (255, 80, 80, 255),
"black": (20, 20, 30, 255),
"orange": (255, 160, 60, 255),
"mouth": (255, 80, 80, 255),
"blush": (255, 120, 160, 180),
"blush_r": (255, 80, 80, 150),
"blush_y": (255, 120, 120, 150),
}
def px(x, y, key):
if 0 <= x < W and 0 <= y < H:
pixels[y, x] = P.get(key, key)
def fill(x, y, w, h, key):
for dy in range(h):
for dx in range(w):
px(x+dx, y+dy, key)
def circle(cx, cy, r, key):
for angle in range(360):
a = math.radians(angle)
px(int(cx + r*math.cos(a)), int(cy + r*math.sin(a)), key)
def fill_circle(cx, cy, r, key):
for dy in range(-r, r+1):
for dx in range(-r, r+1):
if dx*dx + dy*dy <= r*r:
px(cx+dx, cy+dy, key)
def line(x1, y1, x2, y2, key):
dx, dy = abs(x2-x1), abs(y2-y1)
sx = 1 if x1 < x2 else -1
sy = 1 if y1 < y2 else -1
err = dx - dy
while True:
px(x1, y1, key)
if x1 == x2 and y1 == y2: break
e2 = 2*err
if e2 > -dy: err -= dy; x1 += sx
if e2 < dx: err += dx; y1 += sy
# ── 基础猫脸 ────────────────────────────────────────────────────────────────
def cat_base():
fill_circle(CX, CY, R, "white")
fill_circle(CX-R+1, CY-R-2, 5, "pink")
fill_circle(CX+R-1, CY-R-2, 5, "pink")
# 耳朵轮廓
line(CX-R+4, CY-R, CX-R-3, CY-R-12, "dark")
line(CX-R-3, CY-R-12, CX-R+10, CY-R+1, "dark")
line(CX+R-4, CY-R, CX+R+3, CY-R-12, "dark")
line(CX+R+3, CY-R-12, CX+R-10, CY-R+1, "dark")
circle(CX, CY, R, "dark")
fill(CX-10, CY+R-4, 20, 8, "white")
# ── 帧绘制函数 ─────────────────────────────────────────────────────────────
def idle_0():
"""待机帧1正常眨眼。"""
cat_base()
fill_circle(CX-9, CY-4, 5, "green")
fill_circle(CX-10, CY-5, 2, "white")
fill_circle(CX-7, CY-3, 1, "black")
fill_circle(CX+9, CY-4, 5, "green")
fill_circle(CX+8, CY-5, 2, "white")
fill_circle(CX+11, CY-3, 1, "black")
fill(CX-2, CY+2, 4, 3, "pink")
line(CX-2, CY+5, CX-6, CY+8, "dark")
line(CX+2, CY+5, CX+6, CY+8, "dark")
fill_circle(CX-15, CY+2, 3, "blush")
fill_circle(CX+15, CY+2, 3, "blush")
line(CX-13, CY, CX-22, CY-2, "dark")
line(CX-13, CY+2, CX-22, CY+2, "dark")
line(CX+13, CY, CX+22, CY-2, "dark")
line(CX+13, CY+2, CX+22, CY+2, "dark")
def idle_1():
"""待机帧2闭眼。"""
cat_base()
fill(CX-13, CY-4, 9, 2, "dark")
fill(CX+4, CY-4, 9, 2, "dark")
fill(CX-2, CY+2, 4, 3, "pink")
line(CX-2, CY+5, CX-6, CY+8, "dark")
line(CX+2, CY+5, CX+6, CY+8, "dark")
fill_circle(CX-15, CY+2, 3, "blush")
fill_circle(CX+15, CY+2, 3, "blush")
line(CX-13, CY, CX-22, CY-2, "dark")
line(CX-13, CY+2, CX-22, CY+2, "dark")
line(CX+13, CY, CX+22, CY-2, "dark")
line(CX+13, CY+2, CX+22, CY+2, "dark")
def happy_0():
"""开心帧1眯眼大笑。"""
cat_base()
line(CX-13, CY-1, CX-6, CY-4, "dark")
line(CX-13, CY-4, CX-6, CY-1, "dark")
line(CX+6, CY-4, CX+13, CY-1, "dark")
line(CX+6, CY-1, CX+13, CY-4, "dark")
fill_circle(CX-15, CY+1, 4, "blush")
fill_circle(CX+15, CY+1, 4, "blush")
fill(CX-7, CY+4, 14, 6, "mouth")
line(CX-7, CY+4, CX+7, CY+4, "dark")
line(CX-7, CY+10, CX+7, CY+10, "dark")
fill(CX-3, CY+7, 6, 4, "pink")
def happy_1():
"""开心帧2星星眼。"""
cat_base()
for a in range(0, 360, 30):
rad = math.radians(a)
px(int(CX-9 + 5*math.cos(rad)), int(CY-4 + 5*math.sin(rad)), "yellow")
fill_circle(CX-9, CY-4, 2, "yellow")
for a in range(0, 360, 30):
rad = math.radians(a)
px(int(CX+9 + 5*math.cos(rad)), int(CY-4 + 5*math.sin(rad)), "yellow")
fill_circle(CX+9, CY-4, 2, "yellow")
fill_circle(CX-15, CY+1, 4, "blush")
fill_circle(CX+15, CY+1, 4, "blush")
fill(CX-6, CY+4, 12, 5, "mouth")
line(CX-6, CY+4, CX+6, CY+4, "dark")
fill(CX-2, CY+6, 4, 3, "pink")
def focused_0():
"""专注帧1眯眼盯视。"""
cat_base()
fill(CX-12, CY-5, 10, 4, "blue")
fill(CX+2, CY-5, 10, 4, "blue")
fill(CX-13, CY-6, 11, 1, "dark")
fill(CX-13, CY-2, 11, 1, "dark")
fill(CX+2, CY-6, 11, 1, "dark")
fill(CX+2, CY-2, 11, 1, "dark")
fill(CX-10, CY-3, 1, 1, "white")
fill(CX+5, CY-3, 1, 1, "white")
fill(CX-12, CY-10, 9, 2, "dark")
fill(CX+3, CY-10, 9, 2, "dark")
fill(CX-2, CY+2, 4, 3, "pink")
fill(CX-5, CY+5, 10, 2, "dark")
line(CX-13, CY, CX-22, CY-4, "dark")
line(CX-13, CY+2, CX-22, CY, "dark")
line(CX-13, CY+4, CX-22, CY+4, "dark")
line(CX+13, CY, CX+22, CY-4, "dark")
line(CX+13, CY+2, CX+22, CY, "dark")
line(CX+13, CY+4, CX+22, CY+4, "dark")
def focused_1():
"""专注帧2眨眼。"""
cat_base()
fill(CX-12, CY-4, 10, 5, "blue")
fill(CX+2, CY-4, 10, 5, "blue")
fill(CX-10, CY-2, 1, 1, "white")
fill(CX+4, CY-2, 1, 1, "white")
fill(CX-12, CY-10, 9, 2, "dark")
fill(CX+3, CY-10, 9, 2, "dark")
fill(CX-2, CY+2, 4, 3, "pink")
fill(CX-4, CY+5, 8, 2, "dark")
line(CX-13, CY, CX-22, CY-4, "dark")
line(CX-13, CY+2, CX-22, CY, "dark")
line(CX+13, CY, CX+22, CY-4, "dark")
line(CX+13, CY+2, CX+22, CY, "dark")
def annoyed_0():
"""烦躁帧1皱眉生气。"""
cat_base()
fill(CX-12, CY-3, 10, 5, "red")
fill(CX+2, CY-3, 10, 5, "red")
fill(CX-9, CY-2, 2, 2, "black")
fill(CX+5, CY-2, 2, 2, "black")
line(CX-13, CY-9, CX-4, CY-6, "dark")
line(CX-13, CY-6, CX-4, CY-9, "dark")
line(CX+4, CY-9, CX+13, CY-6, "dark")
line(CX+4, CY-6, CX+13, CY-9, "dark")
fill_circle(CX-15, CY+2, 3, "blush_r")
fill_circle(CX+15, CY+2, 3, "blush_r")
line(CX-6, CY+8, CX-2, CY+6, "dark")
line(CX+6, CY+8, CX+2, CY+6, "dark")
line(CX-R+4, CY-R, CX-R+10, CY-R-7, "dark")
line(CX+R-4, CY-R, CX+R-10, CY-R-7, "dark")
def annoyed_1():
"""烦躁帧2眯眼瞪视。"""
cat_base()
fill(CX-12, CY-3, 10, 2, "red")
fill(CX+2, CY-3, 10, 2, "red")
fill(CX-9, CY-3, 2, 2, "black")
fill(CX+5, CY-3, 2, 2, "black")
line(CX-13, CY-9, CX-4, CY-6, "dark")
line(CX-13, CY-6, CX-4, CY-9, "dark")
line(CX+4, CY-9, CX+13, CY-6, "dark")
line(CX+4, CY-6, CX+13, CY-9, "dark")
fill_circle(CX-15, CY+2, 3, "blush_r")
fill_circle(CX+15, CY+2, 3, "blush_r")
fill(CX-5, CY+6, 10, 2, "dark")
line(CX-R+4, CY-R, CX-R+10, CY-R-7, "dark")
line(CX+R-4, CY-R, CX+R-10, CY-R-7, "dark")
def sleepy_0():
"""困倦帧1半闭眼打哈欠。"""
cat_base()
fill_circle(CX-9, CY-1, 5, "orange")
fill_circle(CX+9, CY-1, 5, "orange")
fill(CX-14, CY-1, 10, 5, "white")
fill(CX+4, CY-1, 10, 5, "white")
fill_circle(CX-9, CY, 1, "black")
fill_circle(CX+9, CY, 1, "black")
fill_circle(CX-15, CY+2, 3, "blush_y")
fill_circle(CX+15, CY+2, 3, "blush_y")
fill(CX-4, CY+5, 8, 6, "mouth")
line(CX-4, CY+5, CX+4, CY+5, "dark")
fill(CX-2, CY+7, 4, 3, "pink")
line(CX-R+4, CY-R, CX-R-3, CY-R-7, "dark")
line(CX+R-4, CY-R, CX+R+3, CY-R-7, "dark")
def sleepy_1():
"""困倦帧2几乎闭眼 + ZZZ。"""
cat_base()
fill(CX-12, CY-2, 9, 2, "dark")
fill(CX+3, CY-2, 9, 2, "dark")
fill_circle(CX-15, CY+2, 3, "blush_y")
fill_circle(CX+15, CY+2, 3, "blush_y")
fill(CX-3, CY+5, 6, 3, "mouth")
line(CX-3, CY+5, CX+3, CY+5, "dark")
fill(CX-1, CY+6, 2, 2, "pink")
line(CX-R+4, CY-R, CX-R-3, CY-R-7, "dark")
line(CX+R-4, CY-R, CX+R+3, CY-R-7, "dark")
# ZZZ 符号
for zx, zy, sz in [(16, -18, 3), (21, -23, 3), (26, -28, 3)]:
circle(CX+zx, CY+zy, sz, "blue")
# ── 导出 ────────────────────────────────────────────────────────────────────
FRAMES = [
("idle", idle_0, idle_1),
("happy", happy_0, happy_1),
("focused", focused_0, focused_1),
("annoyed", annoyed_0, annoyed_1),
("sleepy", sleepy_0, sleepy_1),
]
ROOT = "/Users/e2hang/hermes/code/ezvibe/assets/pet"
os.makedirs(ROOT, exist_ok=True)
for emotion, f0, f1 in FRAMES:
out_dir = os.path.join(ROOT, emotion)
os.makedirs(out_dir, exist_ok=True)
for frame_idx, draw_fn in enumerate([f0, f1]):
img = Image.new("RGBA", (W, H), (0, 0, 0, 0))
pixels = img.load()
draw_fn()
out_path = os.path.join(out_dir, f"pet_{emotion}_{frame_idx}.png")
img.save(out_path)
print(f"{out_path}")
print(f"\n全部完成10 张 PNG 已保存到 {ROOT}")