ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

项目总结:桌宠(Desktop Pet)从 pose_classifier.py 到 desktop_pet.py 的完整落地与 TaoToken 接入

项目总结:桌宠(Desktop Pet)从 pose_classifier.py 到 desktop_pet.py 的完整落地与 TaoToken 接入 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, axis0) # 每列非白色像素的比例 # 找到内容密度低于阈值的位置作为列间空隙 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, threshold730): 从图片四边出发将与边缘连通且接近白色的像素设为透明 # 收集所有边缘的近白色像素作为起点 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 的魔棒 连续功能只去除与边缘物理连通的背景白色人物身体内部的高光、浅色皮肤不会受影响。threshold730意味着只有接近纯白255×3765的像素才会被纳入透明化。如果你的角色穿白衣服这个值就要往下调否则衣服会被吃掉。窗口属性设置决定了桌宠浮在桌面上的效果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.txtrun_pet.bat用pyw启动桌宠。但.bat方案即使使用pyw双击时仍会短暂闪现一个 CMD 窗口。要完全静默用.vbsCreateObject(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 URLhttps://taotoken.net/api注意 API 地址不带 UTM 参数API Key在控制台创建形如sk-开头的一串字符Model ID你要调用的具体模型标识获取入口分别是控制台在https://taotoken.net/consoleAPI 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: fBearer {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, headersheaders, jsonpayload, timeout15) 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 IDmessages是标准对话结构。timeout15防止请求卡死拖垮 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, headersheaders, jsonpayload, timeout15) 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, headersheaders, jsonpayload, timeout15)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解耦需要时再跑。把这几件事做完桌宠就从能跑变成好用了。剩下的就是换图、换姿态、换响应风格链路本身不用再动。
返回列表