English: ./README.en.md
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/ 的五个目录一一对应:
display.show_caption——屏幕字幕,字号 / 透明度在线参数(companion 的/captionendpoint 与「念出 + 字幕 + messenger」三路同步需要它)(#354);- IMU 读取工具——晃动感知层的数据源(#355);
- 队列看门狗动态 budget——慢速大传输不再被固定超时砍在半路(#356);
- 会话检测——
get_status暴露 session_id,脸组断电重推靠它(#357, 已合并进上游 main); - 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。
用你自己的设备验证:
- 起 stackchan-mcp gateway;确认设备连上。
- 起 companion;日志打印
companion on <bind>:<port>。 - 摸头——害羞/开心脸 + 轻摆头,然后逐级褪回。
- 在设备上触发聆听并说话——日志出现转写文本,回复被念出、口型跟动。
- 放着不动(默认 18 分钟)——它打瞌睡;摸一下——惊醒。
- 打一遍 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。 场景数据失联一律灭灯。 - **无声语汇。**七个「表情 + 头部动作」组合覆盖不说话的回应:
nodshakethinkackshyhappysad。任何后端都能通过 voice-turn 契约的face字段、或直接POST /face调用同一套语汇。 - 口头切换是实权。「模拟在餐厅」「不要亮灯」「可以说话了」——模型在
标签里回
setmode=/setled=持久键,gateway 写入场景存储,提出请求的 这一轮就按新档执行;companion 收到sceneChanged立即刷新缓存,不等 轮询周期。教学块注入到两个 surface 的每一个档位——缺块的档位会让 模型口头答应却无权落实。硬安全不动:silent 硬规则与 auto clamp 仍在 执行层,LED 切错也没有代价。
判定侧随 gateway 组件交付;强制侧今天已在 companion 里
(轮询 COMPANION_SCENE_URL 并强制执行;不配则走固定默认档)。
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/ 把 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/ 是随仓自带的大脑服务——自包含 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-turn、
VOICE_TURN_TOKEN = gateway 的 GATEWAY_TOKEN、
COMPANION_SCENE_URL=http://<host>:8791/scene、
PRESENCE_REPORT_URL=http://<host>:8791/events/presence、
OBSERVE_REPORT_URL=http://<host>:8791/events/observe、
MORNING_BRIEF_URL=http://<host>:8791/morning-brief。
- 端点——
POST /voice-turn、GET/POST /scene、POST /events/presence、POST /events/observe、GET /morning-brief、GET /health;除/health外都在GATEWAY_TOKENbearer 闸后。 - 上下文两档——
CONTEXT_BACKEND=bare(persona.toml + 本地滚动历史文件, 零外部服务)或kimi-core(对话历史、跨 surface 时间线与 presence 放在 一套 kimi-core 部署里:历史走chat_readMCP 工具、写入走带幂等键的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_ALIASES;LISTEN_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_writeupsert。端点默认跟对话引擎 同一个;配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 灯样式:off 或 ember(极低暖光呼吸) |
COMPANION_LED_TABLE |
空 | 逐表情 LED 动画的 JSON 覆写 |
| avatar 脸组 | ||
STACKCHAN_AVATAR_BIN |
空 | 每次重连后推回设备的脸组档案 |
STACKCHAN_AVATAR_FALLBACK_BIN |
空 | RAM 腾挪路径用的小型 layered 脸组 |
COMPANION_AVATAR_MATRIX_MIN_BYTES |
600000 |
超过此大小按 matrix 模式推送 |
| 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 turn —
POST 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}。 - 远端 STT —
POST WHISPER_REMOTE_URL,裸 Ogg/Opus 字节 →{"text": str, "language"?: str}。 - 晨间一眼 —
GET MORNING_BRIEF_URL→{"say": str}(自带 gateway 的/morning-brief;任何满足形状的服务都行)。 - 观察 sink —
POST 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"?}。
AGPL-3.0——见 LICENSE。