- EmotionEngine: 5状态马尔可夫情绪机 + 蒙特卡洛转移 - VectorMemory: TF-IDF向量记忆 + SQLite持久化 + RAG检索 - AgentBrain: Ollama/OpenAI/Dummy三后端LLM - BehaviorScheduler: 优先级/冷却/活跃度调度 - FastAPI服务器 + WebSocket实时推送 - perception: 键鼠监控 + 屏幕截图 - ui/pet_window: PySide6桌宠窗口 + 像素动画 - assets/pet: 5情绪各2帧像素艺术资源
271 lines
9.5 KiB
Python
271 lines
9.5 KiB
Python
#!/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}")
|