1. 桌宠项目从姿态切图到桌面交互的完整链路
桌宠(Desktop Pet)这个项目,说白了就是把一张包含多姿态人物的大图,切成一张张独立的小人图,再让它们在桌面上以无边框、置顶、透明背景的方式循环播放,形成会动、能点、能拖的桌面宠物。它适合两类人:一类是想复刻一个属于自己的桌面挂件、又不想从零写 GUI 的开发者;另一类是手里已经有一批 AI 生成的角色图,想把它们变成可交互桌面程序的玩家。整条链路其实只有两个核心文件:pose_classifier.py负责把src/里的大图按姿态切分到output/,desktop_pet.py负责把这些图加载成桌宠窗口。中间再加.bat和.vbs做启动封装,最后把模型调用 endpoint 统一改到 TaoToken 的 Key/API 通道,让桌宠的识别与响应链路一次跑通。
我先把整体数据流画清楚,后面每一步都围绕它展开:
src/大图 │ ▼ pose_classifier.py ──▶ output/睡觉状态/、output/跑步状态/ ... 共 6 个姿态文件夹 │ ▼ desktop_pet.py ──▶ 无边框 + 置顶 + 透明窗口,每姿态 5 帧循环播放 │ ▼ 点击切换姿态 / 拖拽移动 / 右键菜单这里的关键认知是:切图和显示是两个完全解耦的阶段。pose_classifier.py只关心怎么把一张图切干净,desktop_pet.py只关心怎么把切好的图显示好。你完全可以在切图阶段反复调参,而不影响桌宠程序;也可以换一套图,只要目录结构一致,桌宠程序一行都不用改。这种解耦是后面能顺利接入 TaoToken 通道的前提——因为模型调用只发生在需要"识别/响应"的环节,跟切图、显示互不干扰。
先看pose_classifier.py的核心思路。它读取src/中的大图,把图中 6 行人物按行切分,每行内部再按投影法切出单个人物,存入output/下按姿态命名的 6 个文件夹。姿态与行的对应关系是固定的:
ROW_POSES = [ "睡觉状态", # 第1行(最上) "跑步状态", # 第2行 "思考发呆状态", # 第3行 "等待站立状态", # 第4行 "开心卖萌状态", # 第5行 "摸鱼慵懒状态", # 第6行(最下) ] NUM_ROWS = 6投影法的原理是把图像沿垂直方向投影,统计每一列的"内容密度"。列与列之间的人物间隙处内容密度接近 0,由此定位切割线:
# 将图像沿垂直方向投影,统计每列的"内容密度" gray = cv2.cvtColor(row_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 245, 255, cv2.THRESH_BINARY_INV) v_proj = np.mean(binary > 0, axis=0) # 每列非白色像素的比例 # 找到内容密度低于阈值的位置作为列间空隙 is_gap = v_proj < 0.03 # 3% 以下视为空白列这段代码里有两个参数值得你亲手调:阈值245决定什么算"白",0.03决定多空才算"间隙"。如果你的原图背景不是纯白,或者人物之间有轻微重叠,这两个值就要动。我试过把0.03调到0.05,切分更保守,不容易把人物切碎,但可能漏切;调到0.01则更激进,容易把一个人切成两半。建议先用默认值跑一遍,看output/里的结果再微调。
desktop_pet.py这边,最容易被忽略但最影响观感的是背景透明化。它用的是 Flood Fill(洪水填充)算法,从图片四边出发,把与边缘连通且接近白色的像素设为透明:
def _make_transparent(self, qimg, threshold=730): """从图片四边出发,将与边缘连通且接近白色的像素设为透明""" # 收集所有边缘的"近白色"像素作为起点 for x in range(w): for y in [0, h - 1]: # 上下边缘 c = qimg.pixelColor(x, y) if c.red() + c.green() + c.blue() >= threshold: stack.append((x, y)) # 四方向 Flood Fill:从起点向相邻像素蔓延 while stack: x, y = stack.pop() if (x, y) in visited: continue visited.add((x, y)) qimg.setPixelColor(x, y, QColor(0, 0, 0, 0)) # 设为透明 for dx, dy in [(0,1),(0,-1),(1,0),(-1,0)]: # 四方向 nx, ny = x + dx, y + dy if 0 <= nx < w and 0 <= ny < h: c = qimg.pixelColor(nx, ny) if c.red() + c.green() + c.blue() >= threshold: stack.append((nx, ny))它类似 Photoshop 的"魔棒 + 连续"功能:只去除与边缘物理连通的背景白色,人物身体内部的高光、浅色皮肤不会受影响。threshold=730意味着只有接近纯白(255×3=765)的像素才会被纳入透明化。如果你的角色穿白衣服,这个值就要往下调,否则衣服会被"吃掉"。
窗口属性设置决定了桌宠"浮在桌面上"的效果:
def _setup_window(self): self.setWindowFlags( Qt.FramelessWindowHint # 无边框 | Qt.WindowStaysOnTopHint # 始终置顶 | Qt.Tool # 不显示在任务栏 ) self.setAttribute(Qt.WA_TranslucentBackground) # 透明背景四个标志各司其职:FramelessWindowHint去掉标题栏和边框,WindowStaysOnTopHint让窗口始终在所有窗口之上,Tool让它不在任务栏显示图标,WA_TranslucentBackground允许窗口背景透明,配合透明图片实现"浮在桌面上"的效果。这四个缺一不可,少一个都会露馅。
帧动画循环靠QTimer每 200ms 触发一次:
ANIM_SPEED_MS = 200 # 每 200ms 切换一帧 def _advance_frame(self): name = list(self.frames.keys())[self.pose_idx] fl = self.frames[name] if fl: self.frame_idx = (self.frame_idx + 1) % len(fl) # 循环到下一帧 self.update() # 触发重绘% len(fl)取模实现循环轮播,self.update()触发paintEvent重绘当前帧。200ms 是节奏感的关键,太快像抽搐,太慢像卡顿,5 帧的话 200ms 刚好一秒一轮。
点击切换姿态要区分点击与拖拽,靠的是位移判断:
def mouseReleaseEvent(self, e): if e.button() == Qt.LeftButton and self.drag_offset is not None: delta = e.globalPos() - self.drag_offset - self.frameGeometry().topLeft() if abs(delta.x()) < 5 and abs(delta.y()) < 5: # 移动 < 5px 视为点击 self._switch_pose() self.drag_offset = None记录鼠标按下和释放时的全局坐标差,位移小于 5 像素判定为"点击",否则为"拖拽移动"。这样同一个左键同时支持点击切换和拖拽移动,交互不冲突。右键菜单则用setData(i)把每个菜单项与姿态索引绑定,选中后直接跳转,当前姿态前显示●标记。
到这里,切图和显示两条链路都通了。接下来要解决的是"启动"和"模型调用"两件事——前者决定你能不能双击就跑,后者决定桌宠能不能接入统一的模型通道。
2. TaoToken 前置准备与 pyw 静默启动配置
在把模型调用 endpoint 改到 TaoToken 之前,先把启动方式理顺。很多人卡在"双击.bat闪一下黑框"或者"py和pyw分不清"上,这些细节不解决,后面接通道时会误以为是网络问题。
先说py和pyw的区别。py是 Python 启动器,带控制台窗口;pyw是 Python Windows 启动器,无控制台窗口。用pyw运行 GUI 程序不会弹出黑框。桌宠是 GUI 程序,所以启动脚本里应该用pyw而不是py。
.bat是 Windows 批处理文件,本质是一个包含多条 CMD 命令的文本文件,双击后系统逐行执行。项目里有两个:
@echo off :: 不显示命令本身 cd /d D:\MyworkSpace_Java\zhuo-chong :: 切换到项目目录 start "" pyw desktop_pet.py :: 用 pyw 后台启动桌宠,start "" 使 cmd 不等待run_py.bat运行pose_classifier.py分割图片,输出重定向到result.txt;run_pet.bat用pyw启动桌宠。但.bat方案即使使用pyw,双击时仍会短暂闪现一个 CMD 窗口。要完全静默,用.vbs:
CreateObject("WScript.Shell").Run "pyw D:\MyworkSpace_Java\zhuo-chong\desktop_pet.py", 0, False.vbs是 VBScript 脚本文件,由 Windows Script Host 引擎解释执行,无需额外安装。参数详解:
| 参数值 | 含义 |
|---|---|
"pyw ...desktop_pet.py" | 要执行的程序 |
0 | 隐藏窗口,不在屏幕上显示任何窗口(连一闪而过的黑框都没有) |
False | 不等待,启动后立即返回,不阻塞调用者 |
.vbs通过WScript.Shell.Run的第二个参数0完全隐藏启动过程,实现真正静默启动。推荐用.vbs,把快捷方式丢到启动目录,开机就自动出现桌宠。
启动理顺后,进入 TaoToken 前置准备。TaoToken 是一个统一的模型调用通道,把不同模型的 endpoint 收敛成一套 Base URL + Key + Model ID 的组合。对桌宠项目来说,它的价值在于:当你想让桌宠具备"识别当前状态并做出响应"的能力时,不需要为每个模型单独维护一套鉴权和地址,改一处配置即可。
你需要准备三样东西:
- Base URL:
https://taotoken.net/api(注意 API 地址不带 UTM 参数) - API Key:在控制台创建,形如
sk-开头的一串字符 - Model ID:你要调用的具体模型标识
获取入口分别是:控制台在https://taotoken.net/console,API Keys 管理在https://taotoken.net/api-keys,接入文档在https://taotoken.net/doc。如果你用的是 Claude Code 这类编码工具,还有专门的接入页https://taotoken.net/ClaudeCodeAnthropic;想先验证模型通不通,可以用模型对话页https://taotoken.net/model-chat;长期跑编码或 Agent 任务,看 Coding Plan 页https://taotoken.net/coding-plan。
这里要强调一个原则:Base URL、Key、Model ID 三件套必须成套出现。只改 Base URL 不改 Key,会 401;只改 Key 不改 Model ID,可能报模型不存在;三者都改但 Base URL 写错路径,会连接失败。后面排障章节会逐一对照真实报错。
前置准备做完,就可以进入可复制配置环节了。
3. 可复制配置:settings.json 与 desktop_pet.py 接入片段
这一节给出可以直接复制粘贴的配置。桌宠项目本身是 Python + PyQt,模型调用部分通常走 OpenAI 兼容的 HTTP 接口,所以配置分两块:一块是给编码工具/CLI 用的settings.json,一块是desktop_pet.py里发起请求的代码片段。
先看settings.json。如果你用 Claude Code 或类似工具来辅助开发这个项目,配置文件路径和原文保持一致,内容如下:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-你的Key", "ANTHROPIC_MODEL": "你的ModelID" } }注意ANTHROPIC_BASE_URL填的是https://taotoken.net/api,不要带末尾斜杠,也不要带 UTM 参数。ANTHROPIC_AUTH_TOKEN就是你在 API Keys 页面创建的 Key。ANTHROPIC_MODEL填你要用的模型 ID。
如果你用的是 Codex 系工具,配置落在auth.json,同样三件套:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "你的ModelID" }再看desktop_pet.py里发起模型请求的片段。桌宠的"识别与响应"通常是这样:定时截取当前姿态或用户操作,发给模型,拿回一句响应文本,显示在气泡里。请求部分可以这样写:
import requests TAOTOKEN_BASE_URL = "https://taotoken.net/api" TAOTOKEN_API_KEY = "sk-你的Key" TAOTOKEN_MODEL = "你的ModelID" def ask_model(prompt: str) -> str: url = f"{TAOTOKEN_BASE_URL}/v1/chat/completions" headers = { "Authorization": f"Bearer {TAOTOKEN_API_KEY}", "Content-Type": "application/json", } payload = { "model": TAOTOKEN_MODEL, "messages": [ {"role": "system", "content": "你是一只桌面宠物,用简短可爱的一句话回应。"}, {"role": "user", "content": prompt}, ], "max_tokens": 64, } resp = requests.post(url, headers=headers, json=payload, timeout=15) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]这段代码里,url拼接的是https://taotoken.net/api/v1/chat/completions,这是 OpenAI 兼容的标准路径。Authorization用Bearer加 Key。payload里model就是你的 Model ID,messages是标准对话结构。timeout=15防止请求卡死拖垮 GUI 主线程——这一点很重要,桌宠是单线程 GUI,网络请求必须设超时,否则界面会假死。
如果你用 Cline 或带 MCP 的工具,配置里同样要写全三件套。以 Cline 的 MCP 配置为例:
{ "mcpServers": { "taotoken": { "command": "npx", "args": ["-y", "your-mcp-server"], "env": { "BASE_URL": "https://taotoken.net/api", "API_KEY": "sk-你的Key", "MODEL_ID": "你的ModelID" } } } }这里再次强调:Base URL + Key + Model ID 三件套缺一不可。任何一处缺失或写错,都会在验证阶段暴露出来。
配置写完后,建议把ask_model的调用放到QThread或concurrent.futures里执行,避免阻塞paintEvent。桌宠的动画是 200ms 一帧,如果模型请求在主线程里跑,动画会明显卡顿。一个简单的做法是用QThreadPool提交任务,回调里更新气泡文本。
配置就绪后,进入验证环节。
4. 验证请求与成功结果:一次跑通识别与响应链路
配置写完不代表通了,必须实际发一次请求看返回。验证分两步:先用命令行验证通道,再在桌宠里验证完整链路。
命令行验证最直接。用curl发一个最小请求:
curl -X POST "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "你的ModelID", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 32 }'如果通道正常,你会看到类似这样的返回:
{ "id": "chatcmpl-xxxx", "object": "chat.completion", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "你好呀,我是你的桌面宠物。" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 8, "completion_tokens": 12, "total_tokens": 20 } }关键看三个地方:choices[0].message.content有内容,说明模型正常返回;finish_reason是stop,说明正常结束;usage里有 token 统计,说明计费链路也通了。如果content为空但finish_reason是length,说明max_tokens太小,调大即可。
命令行通了之后,在桌宠里验证。把ask_model接到一个测试按钮或定时器上,触发后看气泡是否显示返回文本。完整链路是:
用户点击/定时触发 │ ▼ desktop_pet.py 组装 prompt │ ▼ POST https://taotoken.net/api/v1/chat/completions │ ▼ 解析 choices[0].message.content │ ▼ 气泡显示响应文本成功的结果是:桌宠在保持 200ms 帧动画不卡顿的同时,气泡里出现模型返回的一句话。如果动画卡顿,说明请求在主线程;如果气泡一直空白,说明请求失败或解析出错,需要看日志。
验证时建议打开日志,把resp.status_code和resp.text打出来:
resp = requests.post(url, headers=headers, json=payload, timeout=15) print("status:", resp.status_code) print("body:", resp.text[:500]) resp.raise_for_status()这样任何异常都能第一时间定位。实测下来,大部分问题在status和body里就能看出来。
验证通过后,把ask_model的调用频率控制好。桌宠不需要每秒都问模型,可以按姿态切换或用户点击触发,避免无谓的 token 消耗。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节对照真实报错逐一排查。这些错误在接入 TaoToken 通道时最容易遇到,按出现频率排序。
401 Unauthorized。返回体通常是:
{"error": {"message": "Invalid API key", "type": "invalid_request_error"}}原因有三:Key 写错、Key 前后有空格、Key 已失效。排查方法是把 Key 复制到命令行用curl单独测,排除代码里的拼接问题。注意Authorization: Bearer sk-xxx里Bearer和 Key 之间是一个空格,多一个少一个都会 401。
local proxy failed / connection refused。报错形如:
requests.exceptions.ProxyError: HTTPConnectionPool(host='...', port=...): Max retries exceeded这通常是本地环境变量里残留了代理设置,导致请求被转发到不存在的本地端口。排查方法是检查HTTP_PROXY、HTTPS_PROXY环境变量,在代码里显式禁用:
session = requests.Session() session.trust_env = False # 忽略环境变量里的代理设置 resp = session.post(url, headers=headers, json=payload, timeout=15)trust_env = False让 requests 不读取系统代理环境变量,直接连目标地址。
reading 'choices' / KeyError: 'choices'。报错形如:
KeyError: 'choices'或者:
TypeError: 'NoneType' object is not subscriptable这说明返回体里没有choices字段,通常是请求失败但代码没检查状态码就直接取data["choices"]。修复方法是先判断状态码,再解析:
if resp.status_code != 200: print("请求失败:", resp.status_code, resp.text) return "(模型暂时没回应)" data = resp.json() if "choices" not in data: print("返回体异常:", data) return "(返回格式不对)" return data["choices"][0]["message"]["content"]OAuth 相关报错。如果你用的是 Claude Code 类工具,可能遇到:
OAuth token expired or invalid这类工具默认走 OAuth 登录,接入统一 Key 通道时要把鉴权方式切成 API Key。检查settings.json里是否同时存在 OAuth 配置和ANTHROPIC_AUTH_TOKEN,两者冲突时以 OAuth 优先,导致 Key 不生效。解决方法是清掉 OAuth 相关字段,只保留三件套。
模型不存在 / model not found。报错形如:
{"error": {"message": "The model `xxx` does not exist"}}这是 Model ID 写错。回到控制台确认模型标识,注意大小写和连字符。三件套里 Model ID 最容易写错,建议直接从文档复制。
超时 / Read timed out。桌宠 GUI 卡死常见原因。修复方法是设timeout并把请求放到子线程。如果超时频繁,检查网络到https://taotoken.net/api的连通性,用curl -v看握手耗时。
把这几类错误对照排查一遍,基本能覆盖接入过程中的绝大多数问题。排查顺序建议是:先命令行curl验证通道,再检查代码里的三件套,最后看线程和超时。
6. 继续把桌宠做下去:从能跑到好用
项目跑通之后,真正决定体验的是细节。切图阶段的投影阈值、透明化的threshold、动画的ANIM_SPEED_MS、点击判定的 5 像素,这四个参数值得反复调。我的经验是先把ANIM_SPEED_MS定在 200ms,再调透明阈值,最后微调切图参数,因为前两个直接影响观感,切图问题可以后期重跑。
启动方式上,.vbs静默启动是最终形态。把.vbs的快捷方式放进shell:startup目录,开机自动出现桌宠,没有任何黑框。.bat只留作调试用,因为能看到报错输出。
模型调用这块,建议把ask_model做成可开关的。桌宠不一定时刻需要模型,按姿态切换或用户点击触发就够了,既省 token 又省电。请求一定要放子线程,timeout设 15 秒以内,失败时返回一句兜底文案,不要让气泡空着。
最后是图片质量。原文提到"AI 写提示词 → AI 生成图"的短板叠加,这个观察很准。要提升稳定性,可以在切图前加一道预处理:统一背景为纯白、统一人物尺寸、统一行间距。这样投影法的阈值就不用为每张图单独调,切图成功率会明显上升。预处理脚本可以单独写一个preprocess.py,跟pose_classifier.py解耦,需要时再跑。
把这几件事做完,桌宠就从"能跑"变成"好用"了。剩下的就是换图、换姿态、换响应风格,链路本身不用再动。