Skip to content

Repository files navigation

English: ./README.en.md

kimito-stackchan

AI agent的身体层:让一台 M5Stack CoreS3 stackchan 活起来的全部机制—— 表情衰减链、自动困倦与惊醒、摸头反应、呼吸微摆、说话口型同步、摄像头人脸追踪、 受场景硬约束的 LED 底环、presence 状态机,以及把 你的生成图变成屏幕上那张脸的 构建管线。大脑可插拔:把 VOICE_TURN_URL 指向任何 实现三字段 JSON 契约的后端——你自己写的服务,作者的canon在 kimi-core 部署(获得跨 surface 的连续记忆)。

架构一句话:一份大脑,若干条桥(每个 surface 一条),加一个有自己反射的身体——大脑决定开心,怎么笑、笑多久、被摸了怎么办 是身体的事,都住在这里。

**状态:三大组件全部就位。**companion daemon(完整行为层)、avatar 构建 管线与 gateway 大脑服务今天都可用,从一套每天跑在真实硬件上的私有部署 clean-room 移植而来。固件侧五项增强已全部提交上游 PR、其一已合并 ——见固件一节。

仓库结构

路径 内容
companion/ 行为层 daemon(纯标准库)
avatar/ 脸组构建管线:crop → build → boot face
examples/layouts/ 示例 avatar layout,全注释
examples/stub_brain.py 接线测试用最小 voice-turn 后端
examples/launchd/ 服务模板
docs/ avatar 管线教程 + 出门教程 + 运行期 field patterns + 硬件层备忘 + 哨兵拓扑(双语)
gateway/ 随仓自带的大脑服务(voice-turn 后端、场景引擎、TTS shim、messenger 桥)
scripts/ 泄漏闸——本仓是 clean-room 移植,见 CONTRIBUTING

硬件

  • 一台 M5Stack CoreS3 stackchan(屏幕 + 麦克风 + 喇叭 + 摄像头 + 双轴 舵机头 + 可选 12 颗 LED 环),组装并供电完成。这个规格的套件——板载支架、 舵机底座、供电——在日本的 maker 电商有售(Switch Science、BOOTH 等有 stack-chan 套件,任何代购渠道都能买到),或照上游 stack-chan 硬件文档自组。
  • 同一网络里一台跑 companion 的机器(macOS 或 Linux;树莓派也行,重的 STT 可以卸载到远端)。

固件

设备侧不在本仓:上游 kisaragi-mochi/stackchan-mcp 项目——CoreS3 stackchan 的 MCP 原生固件 + 持有设备 WebSocket 的设备 gateway 进程。 本仓在 v1.13.x 线上测试。

为这里的行为层做过五项固件侧改动,已全部提交上游 PR,与 patches/ 的五个目录一一对应:

  1. display.show_caption——屏幕字幕,字号 / 透明度在线参数(companion 的 /caption endpoint 与「念出 + 字幕 + messenger」三路同步需要它)(#354);
  2. IMU 读取工具——晃动感知层的数据源(#355);
  3. 队列看门狗动态 budget——慢速大传输不再被固定超时砍在半路(#356);
  4. 会话检测——get_status 暴露 session_id,脸组断电重推靠它(#357, 已合并进上游 main);
  5. avatar 创作笔记——素材侧文档(#358)。

开机烧脸不需要固件补丁:python -m avatar.boot_face 生成 avatar_images.local.{h,cc},照 patches/ 里的说明链入你的构建——设备 开机第一帧就是你的角色,与运行时脸组逐字节一致,不再是出厂脸。

PR 全部合并前两条路任选:从 fork 构建 (marikagura/stackchan-mcp, PR 全部合并后退役),或把补丁系列自行 git am -3 到官方上游。打补丁时 代码通常干净落地,CHANGELOG.md 冲突是常态——上游和每组补丁都往 同一个 [Unreleased] 锚点插行:纯文档 commit 直接 git am --skip;混合 commit 只冲 CHANGELOG 时 git checkout --theirs CHANGELOG.md && git add -A && git am --continue, 代码不受影响。已合并的组(如 #357)会报「已应用」自动空转,不需要处理。 其余功能对原版上游即可运行——字幕与开机脸是增量能力,固件没有它们时 优雅降级。

隐私

设计原则一句话:数据不流向任何厂商——因为没有厂商服务器。这是与市售 摄像头/智能音箱的根本区别:那些产品把数据送进厂商云端,你只能信任承诺; 这里整套系统 self-hosted,数据流的每一跳都在你自己的机器之间,而且全部 开源可审计——「照片看完即删」不是隐私政策里的承诺,是一行 grep 得到的代码。

数据流分层

数据 去向 留存
反射层(人脸追踪 / 来客识别 / 在场感) 摄像头帧 本机内存,永不出局域网、不经任何模型 用完即删
理解层(对话看图 / 视觉观察) 单帧照片 你自己的服务器 → 你选择的模型推理一次:托管档送你选的 API,本地档只到你局域网里的服务器(见下) 两端不落盘,只留一句文字
语音 录音片段 本机转写(或你自己的 GPU / 你选的 STT API) 转写后即弃,只留文字
来客的脸 相似度计算的中间量 本机 永不存储——来客的特征算完即弃,系统只记「有客人在」的起止时间
你的底库 注册照片 + 特征向量 本机文件 你自己管理,不进 repo
你的表情基线 八个通道的均值与方差(无图像、无时间线) 本机文件 你自己管理,不进 repo;删掉即从头重攒

已实现的保护

  • 被看见永远可感知——视觉观察拍照前底环亮深蓝:他在看你的时候,你知道。
  • 录音永远显式——麦克风只在你拍屏之后才开,录音时屏幕右上角有指示; 没有任何常开监听。拍照是离散动作,不是视频流。
  • 来客即隐私模式——认出画面里不是你 → 停拍照、停视觉观察、停在场记录。 他对隐私的尊重不止对你,也对走进你家的每一个人。
  • 视觉两档,本地档照片不出局域网——理解层的模型端点可以单独指定: 托管档(默认)把单帧送去你选的 API;本地档指向你自己跑的 OpenAI 兼容服务(Ollama、vLLM 等),照片物理上不离开局域网,而对话 照旧用原来的引擎。两档的隐私边界不同——托管档是「对方不留存」, 本地档是「根本没发出去」。配法见下方视觉两档一节。
  • 出门链路全程 TLS 加密到你自己的服务器。
  • 视觉功能全部可独立关闭(env 开关),关掉后其余功能照常。

视觉两档

摄像头帧拍的是你家里的实时画面,比一般的图更该由你决定它去哪。理解层 因此和对话解耦:

托管档(默认) 本地档
配置 不配 OBSERVE_API_BASE_URL OBSERVE_API_BASE_URL + OBSERVE_MODEL
帧去哪 你选的模型 API 只到你局域网里的服务器
门槛 无(有 API 或订阅即可) 一张约 8 GB 显存的卡(7B 视觉模型够写一句白描)
质量 一句白描用 haiku 级足够 同级——这个任务不吃模型上限
# 本地档:Ollama(先 ollama pull qwen2.5-vl:7b)
OBSERVE_API_BASE_URL=http://127.0.0.1:11434/v1
OBSERVE_MODEL=qwen2.5-vl:7b

对话引擎不受影响:VOICE_TURN_ENGINE 保持原样,所以「对话走订阅、 视觉只在本地」是可配的组合。不限 Ollama——任何 OpenAI 兼容服务都行, OBSERVE_API_FORMAT=anthropic 可切到 Messages API 线格式。托管档的 key 刻意不会发往这个端点;服务器要 bearer 就单独配 OBSERVE_API_KEY。 配了端点没配模型名会在启动时报错——不让一帧发出去之后才失败。

硬件层

不放心就贴镜头盖——所有非视觉功能不受影响。

计划中

  • 运行时隐私开关:一条消息暂停全部摄像头与麦克风(LED 示意),再一条恢复
  • 出门相册的保留策略(自动清理可选)

安装

git clone https://github.com/marikagura/kimito-stackchan
cd kimito-stackchan
pip install .                  # 核心 daemon:纯标准库
pip install '.[stt]'           # + 本地语音转写(另需 PATH 里有 ffmpeg)
pip install '.[tracking]'      # + 摄像头人脸追踪 / presence
pip install '.[guest]'         # + 来客识别(InsightFace;numpy 钉 <2)
pip install '.[emotion]'       # + 表情基线(HSEmotion;首跑下载约 16 MB 模型)
pip install '.[avatar]'        # + 脸组构建管线

行为与感知层

常开的基础层(表情衰减、困倦、呼吸微摆、口型、LED 环)之外,六组可选层 ——各自几个 env,不配即静默关闭:

  • **找人的凝视(追踪升级)。**追踪是状态机而不是固定频率轮询:你不动时 settled 降频省舵机,你移动时一步到位的大幅转头,跟丢后按可配置的位置表 巡视——找的时候 thinking 脸、找到瞬间明显的开心、连续扫空后带自动恢复 冷却的歇息。正脸检测器看不见伏案低头的人时,来客引擎的检测头兜底。 大幅转头的观感灵感来自 zziying/stackchan-openapi 的 face tracker;本仓实现是独立完成的。
  • **趣味池。**头顶拍两下 → 一张它视角的照片进本地相册。messenger 上 /listen 预备一次长段收音(听店员念菜单、一整段话),回一句悄悄短评 + 完整文字回复。IMU 判「持续晃动」(地铁/车上):呼吸让路、头锁回中、 偶发晕脸,晃动信号随轮报给大脑当场景信号。当天第一次摸头(晨间窗口内) 取一句简报——不是闹钟。LED 颜色在状态间平滑过渡。FESTIVE_DATES 里的 日子有粉色底光、更容易脸红的摸头反应、开机小庆祝。
  • **来客模式。**对着你注册照片的本地人脸识别;摄像头确证画面里只有别人 时,全部亲昵行为收起、装普通玩具(见隐私一节)。注册: python companion/companion.py enroll <照片>。注意 [guest] extra 把 numpy 钉在 2 以下——onnxruntime/insightface 的轮子还在 1.x ABI 上。首次 注册或首次识别会下载 buffalo_l 模型包:压缩包约 289 MB,解压后连同压缩 包一起留在 ~/.insightface,合计约 630 MB——快网上一到两分钟,期间有 进度条。
  • **视觉观察。**每半小时、你在且无来客时认真看一眼——快门开着时底环深蓝 ——小视觉模型写恰好一句事实白描;只有这句话留存(完整数据流表见隐私 一节)。
  • **表情基线。**本地小模型在追踪已经拍下的帧上打八通道表情分,每一路对着 你自己的基线算 z-score——问的不是「这是不是难过的脸」,而是「这张脸有没有 偏离它自己的基线」。这样问有两个后果。一是相机糊也能用:基线与样本出自 同一颗镜头,糊是常量,在 z-score 里被基线吸收掉(0.3 MP 的机器人相机上 实证可行)。二是基线是你的:由你自己的脸在你自己的机器上攒出来,不随本仓 分发,热身约 100 帧之后 z 才有意义,删掉基线文件即从头重攒。追踪层的载荷 规矩在这里照旧——本层不额外拍任何一张照片,只吃已有的帧,设备侧成本为零, 只有主机为每帧多花几十毫秒 CPU。第一期只把偏离写进日志:不出信号,不改 行为。默认关闭,COMPANION_EMOTION=1 打开;首次运行从 GitHub 下载约 16 MB 模型到 ~/.hsemotion(用户目录,重装 venv 不丢——慢线路要几分钟, 离线机器需要先手动把文件放好)。方法改进自 zziying/emotion-sentry(MIT) ——这里不另开采集循环,只吃追踪已有的帧,并且用追踪与来客的既有状态当 gate;本仓实现是独立完成的。
  • **Claude Code 联动。**agent 干活的那几分钟有了身体:生命周期事件成为 灯的底色,命令失败当场变脸,一轮收尾时按回复的尾段派一个表情。零 API 调用,回路里没有模型——固定的事件映射加一张词表,而词表是配置不是代码: 你的语言、你的用词,自己写。装法见 examples/cc-hook/

摆放与找人校准

stackchan 是仰望构造:摄像头光轴自带一个上仰角,头部俯仰行程压到底 也只能把视线压到接近水平——水平线以下是物理盲区。这决定了摆放法则:

  • 放置高度不要高于你的脸。桌面是原生几何(他矮你高,正好在仰望区); 放到架子等高处后你会落进他的盲区,人脸追踪、在场感、视觉观察都会失效 或退化成「只在你站起/走近时可见」。
  • 出门场景(餐桌、柜台)通常就是桌面几何,不需要重新校准。
  • 换了常驻位置后校准三步:停掉 companion → 用设备工具做网格拍照 (move_head 指一个角度、take_photo 看画面里有没有你,扫几个 yaw/pitch 组合)→ 把你所在的扇区写进 SEARCH_POSITIONS 巡视表与 扫空回中位。找到你一次之后,记忆点机制会自动把「上次见到你的位置」 排到每轮巡视的第一位。
  • pitch 数值的视线方向按你的固件实拍确认,不要凭直觉假设——我们在这上面 花过一整晚。

找人排查表

症状 原因 修法
巡视只往上/只往一边转 巡视表的 pitch/yaw 档不覆盖你实际所在的方位 网格拍照找到你的甜点角度,写进 SEARCH_POSITIONS
「捕捉到什么」就停下,盯着你的头顶或家具 低质量检出过闸;高概率点排在扫描序后面 你最常见姿态的点排最前;found 日志带置信度便于验尸
找到之后头反而甩向天花板 pitch 修正方向按直觉写、写反了 停机双帧标定语义(数值增大 = 视线更高还是更低)再定方向
伏案/低头时检不出人 mediapipe 是正脸检测器,头顶无解 InsightFace SCRFD 检测头兜底(对大角度鲁棒得多)
网格标定结果自相矛盾 标定时 companion 还在跑,巡视循环把头挪走了——拍的不是你指定的角度 停掉 companion 再标定,只信停机干净帧
扫空几轮后永远不找了 挂起闸没有恢复路径 冷却期自动恢复 + 摸头/见脸即时唤醒
settled 深情凝视窗帘/空调 检测置信度阈值太低,静物临界误检 置信度下限提到 0.6 起步
人明明在,presence 却记离开 同「伏案检不出」——物理盲区 接受语义(真的看到才算在),在场判定由消费端与其他信号汇合

快速开始

cp .env.example .env           # 读一遍——每个旋钮都有注释
# 最小配置:设 VOICE_TURN_URL;或者先用接线测试 stub:
python examples/stub_brain.py &
VOICE_TURN_URL=http://127.0.0.1:9000/voice-turn python companion/companion.py

唯一必配是 VOICE_TURN_URL——没配它 daemon 拒绝启动, 而不是悄悄退回到一个你没选过的 endpoint。

用你自己的设备验证

  1. 起 stackchan-mcp gateway;确认设备连上。
  2. 起 companion;日志打印 companion on <bind>:<port>
  3. 摸头——害羞/开心脸 + 轻摆头,然后逐级褪回。
  4. 在设备上触发聆听并说话——日志出现转写文本,回复被念出、口型跟动。
  5. 放着不动(默认 18 分钟)——它打瞌睡;摸一下——惊醒。
  6. 打一遍 HTTP 面: curl -X POST localhost:8770/say -d '{"text":"hello"}' -H 'Content-Type: application/json'

部署三档

三种形态,严格增量——从第一档起步,想要了再往上叠。

**第一档——最小。**一台机器,无记忆后端。persona 写在配置文件里;模型走 API key 或 Claude 订阅(见 voice-turn 引擎一节)。开箱即得:语音对话 + 全部行为层。

[stackchan CoreS3] ⇆ WebSocket ⇆ [stackchan-mcp gateway] ⇆ [companion]
                                                               │ voice turn
                                                               ▼
                                             [裸大脑:gateway 组件,
                                              或任何实现契约的小服务——
                                              examples/stub_brain.py]

**第二档——+ kimi-core。**voice-turn 后端把上下文与记忆放进一套 kimi-core 部署:设备与你的其他 surface(chat、PWA)共享同一份连续记忆,而不是一只独立的玩具。

[companion] → voice turn → [gateway 组件] → 上下文/记忆 → [kimi-core /chat]
                                 │
                                 └→ LLM → 回复 + 通道标签 → 念出 / 字幕 / 表情

**第三档——+ 出门 / + GPU。**一台 VPS 承担公网路径,设备离家可用 (手机热点 → 公网 WSS → VPS 侧设备 gateway + companion,STT 走 API, TTS 经 shim);家里局域网可以加一台 GPU 机器服务 WHISPER_REMOTE_URL 做快速转写。两者都是可选叠加、静默回退——拔掉 GPU,STT 落回本地 whisper;没有 VPS,在家拓扑照常工作。出门的完整操作——热点怎么 存进设备、到了地方怎么用、回家怎么自动回主——见 docs/outing.md

出门:  设备(热点)⇆ 公网 WSS ⇆ [VPS: 设备 gateway + companion + STT API + TTS shim] → 大脑
在家:  设备 ⇆ 局域网 gateway ⇆ companion ── STT → [局域网 GPU 机]──╮
                                          └─ 回退:本地 whisper ◀──╯

场景引擎

设备在哪,决定它可以怎么回应。判定住在大脑里;强制住在执行层。

档位 语义
voice 出声说话;LED 允许
quiet 以悄悄音量说完再恢复;LED 允许
silent 恒不出声——回复渲染成无声表情动作,完整文字落到 messenger 桥
off 无主动输出;纯摆件模式
auto 大脑按轮选择,受下方执行层钳制
  • **大脑判定,边缘渲染。**模型在每条回复末尾输出通道标签—— [[ch: mode/face/speech]]——gateway 剥离后分发:念什么、字幕出什么、 演哪个无声动作。档位切换同时是普通聊天指令,且每次判档都写入事件 记录,判错可以回看、沉淀成规则。
  • 硬安全在执行层,不在 prompt 里。silent 档下 companion 不出任何 声音,无论模型输出了什么——两种判错的代价不对称(地铁上出声是事故, 餐厅里没出声只是少说一句)。auto 档下,设备不在家网时上限 quiet。 场景数据失联一律灭灯。
  • **无声语汇。**七个「表情 + 头部动作」组合覆盖不说话的回应: nod shake think ack shy happy sad。任何后端都能通过 voice-turn 契约的 face 字段、或直接 POST /face 调用同一套语汇。
  • 口头切换是实权。「模拟在餐厅」「不要亮灯」「可以说话了」——模型在 标签里回 setmode=/setled= 持久键,gateway 写入场景存储,提出请求的 这一轮就按新档执行;companion 收到 sceneChanged 立即刷新缓存,不等 轮询周期。教学块注入到两个 surface 的每一个档位——缺块的档位会让 模型口头答应却无权落实。硬安全不动:silent 硬规则与 auto clamp 仍在 执行层,LED 切错也没有代价。

判定侧随 gateway 组件交付;强制侧今天已在 companion 里 (轮询 COMPANION_SCENE_URL 并强制执行;不配则走固定默认档)。

Voice-turn 引擎:API 或 Claude 订阅

voice loop 的「调模型」一步支持两种引擎,VOICE_TURN_ENGINE 切换 (gateway 组件的设置):

  • api(默认):OpenRouter API(需 ENGINE_API_KEY)。按 token 计费(大模型每轮约 $0.06–0.5,取决于缓存命中),延迟较低:热缓存 4–5 秒、冷缓存约 8 秒。适合对响应速度有要求的场景。
  • subscription:Claude 订阅(Agent SDK 启动 Claude Code CLI)。不产生 API 计费,使用已有的 Claude Pro/Max 订阅额度。延迟较高:热缓存 15–17 秒、 冷缓存约 30 秒。适合没有 API 预算、对延迟不敏感的桌面场景。

订阅引擎失败时(超时 VOICE_TURN_SUB_TIMEOUT_MS,默认 45 秒;5 小时用量 窗口打满;CLI 不可用)自动回退 api 路径(需 ENGINE_API_KEY 在场); 两者都不可用时返回错误。

订阅认证两种方式:桌面场景用本机 claude 登录态;headless/VPS 用 claude setup-token 生成 CLAUDE_CODE_OAUTH_TOKEN。环境里的 ANTHROPIC_API_KEY 会覆盖订阅计费——引擎已在子进程级移除该变量,宿主 进程环境不受影响。

结构差异(订阅路径):对话历史以文本拼进 prompt(SDK 单轮语义);prompt cache 由 CLI 管理(5 分钟 TTL,对话间隔长于 5 分钟时命中率低于 api 路径 的 1 小时 TTL)。

参考实测(私有部署,system 上下文约 87 KB + 54 轮历史,大模型, 2026-07):api 冷约 8 秒 / 热 3.7–4.6 秒;subscription 冷约 31 秒 / 热 15–17 秒。

Avatar 管线

avatar/ 把 AI 生成的排版图变成设备上的那张脸:瞳孔锚定工作平面上的 分层合成、带接缝搜索的羽化窗贴、编辑级眨眼差分合成、程序化腮红与特效 角标——输出 14 帧 layered 组、90 帧全表情 matrix、以及固件开机脸源码。 两条素材定律决定一套脸能不能在实机上成立:一次合成只用一种比例系统可动局部必须用编辑级素材

每套脸的专属数字都在一个 layout TOML 里 (示例);零素材、零图像依赖即可 校验你的配置:

python -m avatar.build --layout examples/layouts/sd-face-128.toml --check-layout

完整教程——排版图、定律、量锚点、故障排查: docs/avatar-pipeline.md。素材是你的; 本仓不含任何素材。

跑起来之后才会撞上的坑(设备半死的前兆、脸组超时的三层、USB 抖动与 真重启的判别、依赖被 uv 抹掉): docs/field-patterns.md

硬件本体在几周尺度上的表现(i2c 总线的连坐结构、最先老化的相机 排线、线材与热、什么时候才轮到换模块): docs/hardware-notes.md

主机侧谁来看着看门人(第三视角原则、探测矩阵的形状、没有树莓派时 从 dead-man's switch 起步的替代阶梯): docs/sentinel.md

Gateway 组件

gateway/ 是随仓自带的大脑服务——自包含 TypeScript 服务(Node ≥ 20), 实现 companion 全部契约的后端侧,不用自己写后端就能跑通完整回路。

cd gateway && npm install
cp .env.example .env   # 编辑后:
set -a; source .env; set +a
npm start              # gateway 在 127.0.0.1:8791
npm run tts-shim       # 可选 TTS shim 在 127.0.0.1:8769

把 companion 指向它:VOICE_TURN_URL=http://<host>:8791/voice-turnVOICE_TURN_TOKEN = gateway 的 GATEWAY_TOKENCOMPANION_SCENE_URL=http://<host>:8791/scenePRESENCE_REPORT_URL=http://<host>:8791/events/presenceOBSERVE_REPORT_URL=http://<host>:8791/events/observeMORNING_BRIEF_URL=http://<host>:8791/morning-brief

  • 端点——POST /voice-turnGET/POST /scenePOST /events/presencePOST /events/observeGET /morning-briefGET /health;除 /health 外都在 GATEWAY_TOKEN bearer 闸后。
  • 上下文两档——CONTEXT_BACKEND=bare(persona.toml + 本地滚动历史文件, 零外部服务)或 kimi-core(对话历史、跨 surface 时间线与 presence 放在 一套 kimi-core 部署里:历史走 chat_read MCP 工具、写入走带幂等键的 POST /chat、presence 转发到 /events)。kimi-core 不可达时语音回路照常 应答(空窗口降级)——记忆服务宕机不会让设备哑掉。
  • 引擎——VOICE_TURN_ENGINE=api(默认;OpenRouter 兼容的 OpenAI 格式, 或 ENGINE_API_FORMAT=anthropic 直连 Anthropic key)| subscription (Claude 订阅走 Agent SDK——见上文引擎一节)| stub(离线确定性回显: 干跑与 CI 完全不需要 key)。
  • 场景引擎判定侧——五档与 LED 三态存本地、供 companion 轮询 (FESTIVE_DATES 的纪念日标志捎带在响应里);改档走 POST /scene、 口头/打字的持久标签(setmode=/setled=)、或 messenger 指令 (/voice /off /quiet /silent /auto/led on|off|auto/status/demo/listen——任何语言的自定口令加进 COMMAND_ALIASESLISTEN_KEYWORDS 让短消息含关键词就预备长录)。硬规则同样落在这个 执行层:silent 档不管模型写什么,reply 恒为空串(文字走 messenger + 屏幕字幕);auto 档在外判出 voice 会被压到 quiet;每一轮都往设备屏幕 出字幕。
  • 晨间一眼与观察 sink——GET /morning-brief 确定性拼一句(天气仅在 配了 OPENWEATHER_API_KEY 时有;本仓没有日历源,如实说明)。 POST /events/observe 把一帧交给小视觉模型(OBSERVE_MODEL,默认 haiku 级),只留恰好一句事实白描——本地写 observations.jsonl, kimi-core 档另加一次 observation_write upsert。端点默认跟对话引擎 同一个;配 OBSERVE_API_BASE_URL 则单独走本地视觉服务、帧不出局域网 (见隐私一节的视觉两档)。
  • Messenger 桥(可选)——配 TELEGRAM_BOT_TOKEN 即起,长轮询(不需要 公网地址与 webhook);打字轮走同一管线、按生效档驱动身体。不配桥时 silent 档的语音回复仍会以字幕到达设备屏幕,但没有文字出口——常用 silent 档建议配上。
  • TTS shim——npm run tts-shim 实现设备网关 TTS 中继期望的 mp3-URL JSON 协议,后端 TTS_PROVIDER=elevenlabs | openai | custom(自定 POST {text, voice} → 音频 端点)。TTS_VOICE 刻意无默认值。
  • 干跑——examples/gateway_dry_run.sh 用 stub 引擎起全套、curl 走一遍 所有契约:不要模型、不要 key。

配置(全表)

全部走环境变量;.env.example 有同一张表的长注释版。只有 VOICE_TURN_URL 必配。

变量 默认 含义
大脑
VOICE_TURN_URL —(必配) 对话后端 endpoint
VOICE_TURN_TOKEN 后端 Bearer(scene 轮询与 presence 上报共用)
COMPANION_VOICE_TURN_TIMEOUT_S 90 单轮对话等待秒数
COMPANION_VIA local 随每轮发送的通道标签(网络指纹)
设备 gateway
STACKCHAN_MCP_URL http://127.0.0.1:8767/mcp 设备 gateway 的 MCP endpoint
STACKCHAN_EVENTS_JSONL ~/.stackchan/events.jsonl gateway 的 JSONL 事件流(触摸事件来源)
STACKCHAN_DEVICE_ID 随对话发送的自由格式设备 id
HTTP 面
COMPANION_BIND 127.0.0.1 监听地址(要放宽先配 token)
COMPANION_PORT 8770 监听端口
COMPANION_TOKEN /say /demo /caption /task-status /face 的 Bearer
语音转写
COMPANION_WHISPER_MODEL small 本地 faster-whisper 模型
WHISPER_REMOTE_URL 远端 STT 优先,失败静默回退本地
WHISPER_REMOTE_TOKEN 远端 STT 的 Bearer
WHISPER_REMOTE_TIMEOUT_S 4 远端 STT 超时
COMPANION_STT_LANGUAGES 预期语言表;检出表外语言按第一项强制重转(长录豁免)
COMPANION_STT_HALLUCINATIONS 水印词表 含任一子串整条丢弃(whisper 近静音的字幕水印幻觉)
语音输出
COMPANION_SAY_VOICE say 工具的音色名(空 = 设备默认)
COMPANION_SAY_TIMEOUT_S 120 say 调用超时
COMPANION_VOL_QUIET 35 悄悄档音量
COMPANION_VOL_NORMAL 70 悄悄档说完恢复到的音量
行为
COMPANION_STROKE_COOLDOWN_S 20 摸头反应冷却(冷却内持续摸 = 表情续时不重播)
COMPANION_SLEEP_IDLE_MIN 18 无交互多少分钟打瞌睡
COMPANION_SLEEP_NIGHT_FACTOR 0.5 夜间窗口内困倦阈值的缩放
COMPANION_NIGHT_START_HOUR 0 夜间窗口起点(两值相等 = 关闭窗口)
COMPANION_NIGHT_END_HOUR 6 夜间窗口终点
COMPANION_LED_NIGHT_DIM 0.5 夜间 LED 亮度系数
COMPANION_BREATH 1 呼吸微摆开关
COMPANION_BREATH_MIN_S / _MAX_S 3.5 / 6.5 两次漂移之间的间隔范围
COMPANION_BREATH_REST_S 10 任何其他动作后的歇息
COMPANION_BREATH_YAW_DEG / _PITCH_DEG 2.2 / 1.2 漂移幅度
COMPANION_DECAY_SCALE 1.0 所有表情停留时长的倍率
COMPANION_DECAY_CHAIN 整张衰减表的 JSON 覆写
人脸追踪(可选依赖)
COMPANION_FACE_TRACK 1 开关(依赖没装则该 loop 静默不起)
COMPANION_TRACK_MODE lazy 预设:lazy(大位移才动)或 active(紧跟)
COMPANION_TRACK_INTERVAL 预设 轮询秒数覆写(lazy 3 / active 1)
COMPANION_TRACK_SETTLED_S 预设 settled 降频轮询(lazy 10 / active 1)
COMPANION_TRACK_GAIN 预设 修正增益(lazy 0.95 / active 1.0——一步到位)
COMPANION_TRACK_DEADZONE_DEG 预设 小于此偏差留给呼吸微摆(lazy 12 / active 5)
COMPANION_TRACK_SPEED 预设 修正舵速 dps(lazy 150 / active 300)
COMPANION_TRACK_FLIP 1 摄像头镜像方向(1-1
COMPANION_TRACK_FLIP_Y 1 pitch 修正方向(停机标定,见上文校准节)
COMPANION_TRACK_YAW_LIMIT 85 修正 clamp(接近全舵机域)
COMPANION_TRACK_PITCH_MIN / _MAX 8 / 70 pitch 修正窗口
COMPANION_TRACK_MIN_CONF 0.6 检测置信度下限(0.5 会凝视家具)
COMPANION_TRACK_SEARCH_POSITIONS 通用桌面扫描表 JSON [[yaw,pitch],...] 巡视表——摆放几何,务必校准
COMPANION_TRACK_HOME_POSITION 0,45 扫空后头停在哪("yaw,pitch")
COMPANION_TRACK_SEARCH_AFTER 预设 连续几拍没脸进巡视(8)
COMPANION_TRACK_SEARCH_SPEED / _PAUSE 预设 巡视舵速 / 每点停留
COMPANION_TRACK_SEARCH_TRIES 3 连续扫空几轮后巡视歇息
COMPANION_TRACK_SEARCH_COOL_S 600 歇息冷却;摸头或见到脸提前解除
COMPANION_CAM_FOV_X 66.0 摄像头水平视场角(度)
COMPANION_CAPTURE_DIR ~/.stackchan/captures gateway 落帧目录(用完即删)
presence
COMPANION_PRESENCE_ABSENT_MIN 5 连续多少分钟没见到脸翻转为离开
PRESENCE_REPORT_URL 翻转上报 endpoint(空 = 只本地 log)
趣味池
COMPANION_OUTING_DIR ~/Pictures/stackchan-outings 出门相册目录
COMPANION_TAP_DOUBLE_WINDOW / _OUTING_COOLDOWN 2.5 / 8 拍两下判定窗 / 两张最小间隔
OUTING_REPORT_URL 每张照片的可选通知 endpoint
COMPANION_LISTEN_ARM_TTL_MIN 10 长录预备有效期(分钟)
COMPANION_LONG_LISTEN_MAX_S 50 长录转写上限(Ogg 页级裁剪)
COMPANION_IMU 1 IMU 晃动层(固件无此工具则自动退避)
COMPANION_IMU_POLL_S 1.5 IMU 轮询间隔
COMPANION_IMU_SHAKE_ON / _OFF 0.02 / 0.008 方差阈值(g²,滞回)
COMPANION_IMU_DIZZY_S 60 晕脸平均间隔(秒)
MORNING_BRIEF_URL 晨间一眼 endpoint(空 = 关)
COMPANION_MORNING 1 晨间一眼开关
COMPANION_MORNING_START / _END 5 / 11 触发窗口小时
COMPANION_LED_FADE 0.35 LED 渐变系数(0 = 直切)
来客模式(可选依赖)
COMPANION_GUEST 1 开关(引擎/底库缺时自动停用)
COMPANION_GUEST_DB ~/.stackchan/face_db.npz 注册底库(companion.py enroll 建)
COMPANION_GUEST_MODEL buffalo_l insightface 模型包
COMPANION_GUEST_CHECK_S 15 正常态验证间隔(来客态每帧验)
COMPANION_GUEST_ENTER_SIM / _EXIT_SIM 0.18 / 0.40 陌生人 / 机主相似度阈值(之间 = 不表态)
COMPANION_GUEST_ENTER_N / _EXIT_N 2 / 2 收起 / 恢复所需连续判定次数
COMPANION_GUEST_MIN_DET 0.60 检测分下限——糊/暗的脸不表态
GUEST_REPORT_URL 进出上报的可选 endpoint
视觉观察
OBSERVE_REPORT_URL 帧 sink endpoint(空 = 层关闭)
COMPANION_OBSERVE 1 观察层开关
COMPANION_OBSERVE_INTERVAL_MIN 30 两次观察间隔(分钟)
表情基线(可选依赖)
COMPANION_EMOTION 0 开关(默认关;引擎缺时自动停用)
COMPANION_EMOTION_MODEL enet_b0_8_best_afew HSEmotion 模型名
COMPANION_EMOTION_STATE ~/.stackchan-emotion.json 基线文件(删掉即重攒)
COMPANION_EMOTION_BASELINE_N 2000 EMA 窗口(帧)——「日常底色」回溯多远
COMPANION_EMOTION_MIN_FACE 48 脸短边下限(px)——更小的脸跳过不猜
COMPANION_EMOTION_Z_LOG 2.5 负向通道记一行日志的 z 阈值
场景策略
COMPANION_SCENE_URL 策略 endpoint;轮询并在执行层强制,stale → 灯灭
COMPANION_SCENE_DEFAULT_MODE voice 没配 endpoint 时的固定档位
COMPANION_SCENE_POLL_S / _STALE_S 20 / 600 轮询周期 / 失联判定
任务状态
COMPANION_TASK_STATUS 1 agent 任务状态外化开关
COMPANION_TASK_TTL_S 1800 没发 done 就崩掉的 session 的清扫时限
LED
COMPANION_LED_IDLE off idle 灯样式:offember(极低暖光呼吸)
COMPANION_LED_TABLE 逐表情 LED 动画的 JSON 覆写
avatar 脸组
STACKCHAN_AVATAR_BIN 每次重连后推回设备的脸组档案
STACKCHAN_AVATAR_FALLBACK_BIN RAM 腾挪路径用的小型 layered 脸组
COMPANION_AVATAR_MATRIX_MIN_BYTES 600000 超过此大小按 matrix 模式推送

HTTP 面

endpoint 鉴权 用途
POST /audio 无(保持 loopback) 本机 gateway 发来的 Ogg/Opus 语音 → 转写 → 对话 → 渲染
POST /say {text, quiet?} token 从设备喇叭念文本(设备不在本 gateway 时 409)
POST /face {act} token 无声语汇:nod shake think ack shy happy sad
POST /caption {text, duration_ms?} token 屏幕字幕
POST /demo {kind?} token 表情轮巡(灯 + 头 + 口型)
POST /task-status {state, sid} token agent 生命周期 → 灯底色 / 闪烁
POST /listen-mode {armed} token 长录预备/收回(TTL 内一次性)
POST /observe-now token 手动观察一轮(放行睡着/离席;来客 gate 永不放行)
POST /track-search token 手动巡视一轮(与自动跟丢同一代码路径)
POST /shake-test {seconds?} token 注入模拟晃动走 IMU 同一状态机
GET /status {connected, presence, guest, fun} 探针(供上游路由判断设备在哪)

契约(自己写后端用)

  • Voice turnPOST VOICE_TURN_URL,请求 {"text": str, "deviceId": str, "via": str, "longForm"?: true, "shaking"?: true} → 响应 {"reply": str, "face"?: str, "mode"?: "voice"|"quiet"|"silent", "sceneChanged"?: true}reply 永远等于该念出的文本(silent 档为空串);face 指定用无声语汇代替 说话;longForm 标记长录轮;shaking 是移动中场景信号;sceneChanged 要求 companion 立即刷新场景缓存。
  • 场景策略GET COMPANION_SCENE_URL{"mode": "off"|"voice"|"quiet"|"silent"|"auto", "led": "auto"|"on"|"off", "festive"?: {"active": true, "title": str}}。 在本执行层强制:只有 voice/quiet 亮灯(auto 在设备连本地时视同 voice);数据失联一律灭。
  • presence 上报POST PRESENCE_REPORT_URL,仅在翻转时发 {"present": bool, "sinceTs": ms, "gapMin"?: number}
  • 远端 STTPOST WHISPER_REMOTE_URL,裸 Ogg/Opus 字节 → {"text": str, "language"?: str}
  • 晨间一眼GET MORNING_BRIEF_URL{"say": str}(自带 gateway 的 /morning-brief;任何满足形状的服务都行)。
  • 观察 sinkPOST OBSERVE_REPORT_URL{"imageB64": str, "mediaType"?: str}{"ok": true, "observation"?: str, "skipped"?: str}(自带: /events/observe)。sink 侧不得留存帧。
  • 出门 / 来客上报(可选)POST OUTING_REPORT_URL 每张照片 {"path", "via"}POST GUEST_REPORT_URL 收起/恢复时 {"state": "enter"|"exit", "sinceTs", "minutes"?}

License

AGPL-3.0——见 LICENSE

About

不只是桌面,更是出门的身体。A body for an AI companion — the full behavior layer for an M5Stack CoreS3 stackchan: expression decay, sleepiness, face tracking, guest mode, expression baseline, avatar pipeline.

Topics

Resources

Contributing

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages