codepet 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/adapters/skill/SKILL.md +697 -0
- package/bin/codepet.js +384 -0
- package/bin/pet-render.sh +43 -0
- package/core/index.js +206 -0
- package/package.json +45 -0
- package/scripts/animate.py +207 -0
- package/scripts/auto_exp.js +91 -0
- package/scripts/crop_all.py +46 -0
- package/scripts/crop_expressions.py +73 -0
- package/scripts/gen_expressions.py +95 -0
- package/scripts/gen_variants.py +107 -0
- package/scripts/img2ascii.py +41 -0
- package/scripts/img2emoji.py +61 -0
- package/scripts/img2terminal.py +88 -0
- package/scripts/pet_bubble.js +136 -0
- package/scripts/pet_card.py +472 -0
- package/scripts/polaroid.py +103 -0
- package/scripts/popup_pet.sh +29 -0
- package/scripts/render_sprite.py +558 -0
- package/scripts/render_to_image.py +44 -0
- package/scripts/show_all.py +45 -0
- package/scripts/show_all_expressions.py +41 -0
- package/scripts/sprites.sh +184 -0
- package/sprites/bababoyi.png +0 -0
- package/sprites/bagayalu.png +0 -0
- package/sprites/bibilabu.png +0 -0
- package/sprites/gugugaga.png +0 -0
- package/sprites/waibibabu.png +0 -0
- package/sprites/wodedaodun.png +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""将图片转为 emoji 色块画 — 体积极小,CC 不会折叠"""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
# 用 Unicode 全角方块,不用 ANSI 颜色码
|
|
8
|
+
# 将颜色量化到有限调色板,用对应的 emoji/unicode 字符表示
|
|
9
|
+
PALETTE = [
|
|
10
|
+
((255,255,255), ' '), # 白/透明
|
|
11
|
+
((240,230,210), '🟨'), # 浅黄/米色 → 黄方块
|
|
12
|
+
((210,180,120), '🟧'), # 棕黄 → 橙方块
|
|
13
|
+
((180,130,70), '🟫'), # 棕色
|
|
14
|
+
((140,100,50), '🟫'), # 深棕
|
|
15
|
+
((100,70,35), '⬛'), # 很深棕 → 黑
|
|
16
|
+
((50,50,50), '⬛'), # 黑
|
|
17
|
+
((200,200,200), '⬜'), # 灰白
|
|
18
|
+
((160,160,160), '🔲'), # 灰
|
|
19
|
+
((120,120,120), '🔳'), # 深灰
|
|
20
|
+
((220,150,150), '🟥'), # 粉红
|
|
21
|
+
((200,60,60), '🟥'), # 红
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def closest_char(r, g, b, a):
|
|
25
|
+
if a < 128 or (r > 245 and g > 245 and b > 245):
|
|
26
|
+
return ' '
|
|
27
|
+
best = ' '
|
|
28
|
+
best_dist = float('inf')
|
|
29
|
+
for (pr, pg, pb), ch in PALETTE:
|
|
30
|
+
d = (r-pr)**2 + (g-pg)**2 + (b-pb)**2
|
|
31
|
+
if d < best_dist:
|
|
32
|
+
best_dist = d
|
|
33
|
+
best = ch
|
|
34
|
+
return best
|
|
35
|
+
|
|
36
|
+
def render(img_path, width=15):
|
|
37
|
+
img = Image.open(img_path).convert("RGBA")
|
|
38
|
+
aspect = img.height / img.width
|
|
39
|
+
height = int(width * aspect * 0.55) # emoji 是正方形,补偿比例
|
|
40
|
+
img = img.resize((width, height), Image.LANCZOS)
|
|
41
|
+
|
|
42
|
+
lines = []
|
|
43
|
+
for y in range(height):
|
|
44
|
+
line = ""
|
|
45
|
+
for x in range(width):
|
|
46
|
+
r, g, b, a = img.getpixel((x, y))
|
|
47
|
+
line += closest_char(r, g, b, a)
|
|
48
|
+
lines.append(line.rstrip())
|
|
49
|
+
|
|
50
|
+
# 去掉首尾空行
|
|
51
|
+
while lines and not lines[0].strip():
|
|
52
|
+
lines.pop(0)
|
|
53
|
+
while lines and not lines[-1].strip():
|
|
54
|
+
lines.pop()
|
|
55
|
+
|
|
56
|
+
return '\n'.join(lines)
|
|
57
|
+
|
|
58
|
+
if __name__ == '__main__':
|
|
59
|
+
path = sys.argv[1]
|
|
60
|
+
width = int(sys.argv[2]) if len(sys.argv) > 2 else 15
|
|
61
|
+
print(render(path, width))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
将图片转为终端像素画(半块字符 ▀▄█ + RGB 真彩色)
|
|
4
|
+
用法:
|
|
5
|
+
python3 img2terminal.py <图片路径> [宽度] [--no-bg]
|
|
6
|
+
python3 img2terminal.py crop <图片路径> <x> <y> <w> <h> [终端宽度]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from PIL import Image
|
|
11
|
+
|
|
12
|
+
UPPER_HALF = "▀"
|
|
13
|
+
LOWER_HALF = "▄"
|
|
14
|
+
FULL_BLOCK = "█"
|
|
15
|
+
|
|
16
|
+
def fg(r, g, b):
|
|
17
|
+
return f"\033[38;2;{r};{g};{b}m"
|
|
18
|
+
|
|
19
|
+
def bg(r, g, b):
|
|
20
|
+
return f"\033[48;2;{r};{g};{b}m"
|
|
21
|
+
|
|
22
|
+
RESET = "\033[0m"
|
|
23
|
+
|
|
24
|
+
def is_transparent_or_white(pixel, threshold=245):
|
|
25
|
+
"""判断像素是否接近白色/透明(当作背景)"""
|
|
26
|
+
if len(pixel) == 4 and pixel[3] < 128:
|
|
27
|
+
return True
|
|
28
|
+
return pixel[0] > threshold and pixel[1] > threshold and pixel[2] > threshold
|
|
29
|
+
|
|
30
|
+
def render_image(img, term_width=30, remove_bg=True):
|
|
31
|
+
"""将 PIL Image 渲染为终端像素画"""
|
|
32
|
+
# 调整大小:宽度=term_width字符,高度按比例(每字符=2像素高)
|
|
33
|
+
aspect = img.height / img.width
|
|
34
|
+
pixel_w = term_width
|
|
35
|
+
pixel_h = int(term_width * aspect * 1.7) # 1.7 补偿字符高宽比,比2更胖
|
|
36
|
+
if pixel_h % 2 == 1:
|
|
37
|
+
pixel_h += 1
|
|
38
|
+
|
|
39
|
+
img = img.convert("RGBA").resize((pixel_w, pixel_h), Image.LANCZOS)
|
|
40
|
+
|
|
41
|
+
lines = []
|
|
42
|
+
for y in range(0, pixel_h, 2):
|
|
43
|
+
line = ""
|
|
44
|
+
for x in range(pixel_w):
|
|
45
|
+
top = img.getpixel((x, y))
|
|
46
|
+
bot = img.getpixel((x, y + 1)) if y + 1 < pixel_h else (255, 255, 255, 0)
|
|
47
|
+
|
|
48
|
+
top_bg = remove_bg and is_transparent_or_white(top)
|
|
49
|
+
bot_bg = remove_bg and is_transparent_or_white(bot)
|
|
50
|
+
|
|
51
|
+
if top_bg and bot_bg:
|
|
52
|
+
line += " "
|
|
53
|
+
elif top_bg:
|
|
54
|
+
line += f"{fg(bot[0], bot[1], bot[2])}{LOWER_HALF}{RESET}"
|
|
55
|
+
elif bot_bg:
|
|
56
|
+
line += f"{fg(top[0], top[1], top[2])}{UPPER_HALF}{RESET}"
|
|
57
|
+
elif top[:3] == bot[:3]:
|
|
58
|
+
line += f"{fg(top[0], top[1], top[2])}{FULL_BLOCK}{RESET}"
|
|
59
|
+
else:
|
|
60
|
+
line += f"{fg(top[0], top[1], top[2])}{bg(bot[0], bot[1], bot[2])}{UPPER_HALF}{RESET}"
|
|
61
|
+
lines.append(line)
|
|
62
|
+
|
|
63
|
+
return "\n".join(lines)
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
if len(sys.argv) < 2:
|
|
67
|
+
print("用法:")
|
|
68
|
+
print(" python3 img2terminal.py <图片> [宽度]")
|
|
69
|
+
print(" python3 img2terminal.py crop <图片> <x> <y> <w> <h> [宽度]")
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
if sys.argv[1] == "crop":
|
|
73
|
+
# 裁切模式
|
|
74
|
+
img_path = sys.argv[2]
|
|
75
|
+
x, y, w, h = int(sys.argv[3]), int(sys.argv[4]), int(sys.argv[5]), int(sys.argv[6])
|
|
76
|
+
term_width = int(sys.argv[7]) if len(sys.argv) > 7 else 30
|
|
77
|
+
img = Image.open(img_path)
|
|
78
|
+
img = img.crop((x, y, x + w, y + h))
|
|
79
|
+
print(render_image(img, term_width))
|
|
80
|
+
else:
|
|
81
|
+
# 整图模式
|
|
82
|
+
img_path = sys.argv[1]
|
|
83
|
+
term_width = int(sys.argv[2]) if len(sys.argv) > 2 else 40
|
|
84
|
+
img = Image.open(img_path)
|
|
85
|
+
print(render_image(img, term_width))
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 宠物冒泡 — 被 hooks 调用,30% 概率输出一句宠物的话
|
|
4
|
+
* 输出到 stderr 让 Claude 看到(作为 hook 反馈)
|
|
5
|
+
*
|
|
6
|
+
* 增强:根据 lastInteraction 自动更新 mood(sleep / worry),
|
|
7
|
+
* 台词根据 mood 上下文选择。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const PET_FILE = path.join(os.homedir(), '.codepet', 'pet.json');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(PET_FILE)) process.exit(0);
|
|
17
|
+
|
|
18
|
+
let pet;
|
|
19
|
+
try {
|
|
20
|
+
pet = JSON.parse(fs.readFileSync(PET_FILE, 'utf-8'));
|
|
21
|
+
} catch {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
if (pet.muted) process.exit(0);
|
|
25
|
+
|
|
26
|
+
// ── 根据 lastInteraction 更新 mood ──
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const last = pet.lastInteraction ? new Date(pet.lastInteraction).getTime() : now;
|
|
29
|
+
const hoursAgo = (now - last) / (1000 * 60 * 60);
|
|
30
|
+
|
|
31
|
+
let moodChanged = false;
|
|
32
|
+
if (hoursAgo > 6 && pet.mood !== 'worry') {
|
|
33
|
+
pet.mood = 'worry';
|
|
34
|
+
moodChanged = true;
|
|
35
|
+
} else if (hoursAgo > 2 && hoursAgo <= 6 && pet.mood !== 'sleep') {
|
|
36
|
+
pet.mood = 'sleep';
|
|
37
|
+
moodChanged = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 如果刚回来(< 2h),恢复清醒
|
|
41
|
+
if (hoursAgo <= 2 && (pet.mood === 'sleep' || pet.mood === 'worry')) {
|
|
42
|
+
pet.mood = '清醒';
|
|
43
|
+
moodChanged = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 更新 lastInteraction(每次冒泡脚本被调用 = 有互动)
|
|
47
|
+
pet.lastInteraction = new Date().toISOString();
|
|
48
|
+
|
|
49
|
+
if (moodChanged || !pet.lastInteraction) {
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(PET_FILE, JSON.stringify(pet, null, 2), 'utf-8');
|
|
52
|
+
} catch {
|
|
53
|
+
// 写入失败不影响冒泡
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 30% 概率冒泡
|
|
58
|
+
if (Math.random() > 0.3) process.exit(0);
|
|
59
|
+
|
|
60
|
+
// ── 心情专属台词 ──
|
|
61
|
+
const SLEEP_LINES = {
|
|
62
|
+
bibilabu: ['zzZ……香蕉皮盖好了……', '(梦见了一根巨大的香蕉)', '别吵……让我再睡五分钟……'],
|
|
63
|
+
bagayalu: ['zzZ……', '(安静地打盹中)', '……水……到渠成……zzZ'],
|
|
64
|
+
wodedaodun: ['zzZ……zzZ……zzZ……', '(翻了个身继续睡)', '(呼噜声)……少个分号……'],
|
|
65
|
+
bababoyi: ['(闭着大眼睛睡觉)', 'zzZ……咕……', '(眼皮在抖,估计在做梦)'],
|
|
66
|
+
waibibabu: ['哥先眯一会儿……', 'zzZ……年轻人别急……zzZ', '(打鼾中)……先写测试……'],
|
|
67
|
+
gugugaga: ['嘎……zzZ……', '(蜷成一团睡着了)', '……嘎嘎……zzZ'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const WORRY_LINES = {
|
|
71
|
+
bibilabu: ['你去哪了?我的香蕉皮都干了……', '是不是不要我了……', '(焦虑地搓香蕉皮)'],
|
|
72
|
+
bagayalu: ['……你还在吗?', '等了好久……没事,我不急。(其实很急)', '水到渠成……但水呢?'],
|
|
73
|
+
wodedaodun: ['醒了好久了……你怎么还不来?', '是不是出什么事了?', '(担心地看着门口)'],
|
|
74
|
+
bababoyi: ['(大眼睛里有泪光)', '咕……?你回来了吗……', '一直在等你……'],
|
|
75
|
+
waibibabu: ['哥等你好久了,知道吗?', '以为你把哥忘了……', '回来就好……回来就好。'],
|
|
76
|
+
gugugaga: ['嘎……嘎嘎?(你在哪里)', '(不安地来回踱步)', '嘎!(终于回来了!)'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── 普通台词(清醒) ──
|
|
80
|
+
const NORMAL_LINES = {
|
|
81
|
+
bibilabu: [
|
|
82
|
+
'我穿这身不是因为我想穿。',
|
|
83
|
+
'香蕉直觉告诉我,你写得不错。',
|
|
84
|
+
'没事,我穿成这样都没放弃生活。',
|
|
85
|
+
'今天穿香蕉皮,明天穿西瓜皮。',
|
|
86
|
+
],
|
|
87
|
+
bagayalu: [
|
|
88
|
+
'没事。再来一次就好。',
|
|
89
|
+
'你急什么?',
|
|
90
|
+
'嗯……我虽然不动,但我看到了。',
|
|
91
|
+
'水到渠成。',
|
|
92
|
+
],
|
|
93
|
+
wodedaodun: [
|
|
94
|
+
'zzZ……啊?还在写?',
|
|
95
|
+
'(梦话)……少个分号……zzZ',
|
|
96
|
+
'躺平不代表不思考。',
|
|
97
|
+
'你的代码比我还困。',
|
|
98
|
+
],
|
|
99
|
+
bababoyi: [
|
|
100
|
+
'(一动不动地盯着你写代码)',
|
|
101
|
+
'咕?',
|
|
102
|
+
'我的大眼睛不是摆设。',
|
|
103
|
+
'从架构层面来看——算了。',
|
|
104
|
+
],
|
|
105
|
+
waibibabu: [
|
|
106
|
+
'这代码谁写的?哦,你写的。',
|
|
107
|
+
'慌什么。哥在呢。',
|
|
108
|
+
'年轻人,听哥一句,先写测试。',
|
|
109
|
+
'哥以前也写过这种 bug。',
|
|
110
|
+
],
|
|
111
|
+
gugugaga: [
|
|
112
|
+
'嘎?你认真的?',
|
|
113
|
+
'嘎嘎嘎嘎嘎!',
|
|
114
|
+
'……嘎。(继续吧)',
|
|
115
|
+
'我的刘海下面藏着 debug 之眼。',
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ── 根据 mood 选台词库 ──
|
|
120
|
+
let linesMap;
|
|
121
|
+
if (pet.mood === 'sleep') {
|
|
122
|
+
linesMap = SLEEP_LINES;
|
|
123
|
+
} else if (pet.mood === 'worry') {
|
|
124
|
+
linesMap = WORRY_LINES;
|
|
125
|
+
} else {
|
|
126
|
+
linesMap = NORMAL_LINES;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const lines = linesMap[pet.character] || linesMap.gugugaga;
|
|
130
|
+
const line = lines[Math.floor(Math.random() * lines.length)];
|
|
131
|
+
const name = pet.nickname || pet.name;
|
|
132
|
+
|
|
133
|
+
// 输出气泡到 stderr(hook 反馈格式)
|
|
134
|
+
console.error(`\n┌${'─'.repeat(name.length * 2 + line.length + 6)}┐`);
|
|
135
|
+
console.error(`│ 💬 ${name}:${line} │`);
|
|
136
|
+
console.error(`└${'─'.repeat(name.length * 2 + line.length + 6)}┘`);
|