HEL-879 R5 test package v2 (materials rework per manager's 4-item list; zero execution): (1) dependencies split into two tracks - R5 voice-only chain verified present (LiveKit 7880 plain ws / FunASR / agent venv livekit-agents 1.8.8 / CosyVoice weights, GPU 30520MiB free) vs FULL unified experience honestly listed as 41-item closure truly-missing (mem0ai fixed-source wheel offline-built sha256 698284b6 352586B, greenlet/pillow/truststore cache-reusable, 37 pending download authorization, hatchling resolved; existing .12 product venvs probed have NO mem0 and are system 3.12.3 - 'zero missing' does not hold for the full experience; fastembed models actually 190.4MB+0.05MB, hel895 doc's ~92MB flagged as erratum); (2) trusted HTTPS+WSS rewritten with evidence - retracted 'one self-signed click covers LiveKit WSS 7880' (7880 is plain ws shared by generic worker and 8090 demo, 7881 is not an HTTP endpoint, the 8090 https page itself hands browsers ws:// = mixed content with no proof it ever worked); proposed dedicated 8363 TLS bridge deploy/voice/wss_bridge.example.py (asyncio+ssl stdlib, zero new deps; local smoke TLS1.3 + byte transparency pass, wss-bridge-smoke.log), livekit_url=wss://...:8363, per-device per-port trust steps, ignore-cert switches explicitly banned, iOS/Android behavior marked pending real device; (3) budget per-category - acquire() is an atomic pre-send hold so failures/retries/interrupts/background all count (LLM call count != successful chat lines); LLM+embedding = code-enforced hard gates, ASR/TTS/session-creation honestly listed as ops-only caps (window<=2h, calls<=3, TTS<=200 segments; session-cap listed as engineering follow-up); proposed caps do not self-activate, caps stay 0; (4) engineering params decided by engineering - 8360/8361/8363/8082 and /opt/voice-agent-cybergril + /opt/cybergril-voice-certs all verified conflict-free read-only; D1 rewritten to the real 11 voice IDs with zero-synthesis reference-audio audition plan (8090 panel has NO audition and mutates shared state - v1 claim retracted; generic demo 'rain' is not this user's choice). .12 facts from read-only SSH probes, evidence sheet r5-facts-20261027.md (secrets redacted); web/src and src zero changes; 4x requirements checks pass (origin/main and f92a86f, with/without handoff); handoff appended; zero real LLM/embed/ASR/TTS/mic, zero download/install/deploy/merge, real caps stay 0
2026-10-27 06:44:42 +08:00
HEL-869: read-only remediation evidence per manager independent review of 81d50f2 - recheck869.py reproduces the upstream rolling 10-message window from the fixed mem0 storage source (SHA256 5f5ecdad, byte-identical to the isolated venv install) in an independent in-memory SQLite with zero LLM zero embedding zero network (single 15-message batch truncates to 10; the real 9,1,1,2,2 add pattern keeps batch1 first-4 + later-6, dropping batch1 last-5) and verifies read-only that this round's mem0 messages hold 10/15 missing exactly t3 user/companion, t4 user/companion, t5 user with the restored public copy identical at 10, while the baseline main archive chat_message keeps all 15 verbatim per-role rows and the original calls inputs still carry all 15 - SDK messages is a rolling window, not a permanent quote archive, and no real user data was lost; erratum_HEL-869.json records six report-corrections without touching any original JSON (E1 S1 frozen criterion sampled only mem0 t1 so full-15 retention cannot be claimed, E2 this round used 46004 tokens = 44735+1269 while 163041 is the cumulative 39-call total, E3 raw QA6-13 map in order to plan QA1-8, E4 calls headline 'answered blue' is a stale run2 note vs this round's honest unknown answer, E5 in-process llm_calls_real=0/embed 39 are not authoritative vs persistent ledgers' 13 sent/40 dense, E6 S9 re-verified only the public instance with full private+public recovery reused from gate2 P5); README HEL-868 record annotated in place at the three corrected conclusions plus a HEL-869 section; plan-next-HEL-869.md delivers the decision-only minimal follow-up (main archive stays the permanent quote layer with reread via turn_ids anchors, mem0 interfaces reused, no upstream fork; batch-extraction line-level provenance proven by input-side pre-grouping where each add's anchor equals its actual input set with gate-side sha256 recompute - no gate bypass, no anchor trimming, no prompt-only promises; S6 verified via explicit-status questioning under the same frozen bounded grammar with no wording amnesty; Chinese custom_instructions deferred; upstream/ours responsibilities, risks incl. embed budget 498/500 remaining 2 as the hard blocker, and a staged minimal retest: stage0 offline zero-cost audit then stage1 ~14 requests/~40-46 embeddings pending quota decision); all three ledgers byte-identical before/after (03f92557/c9871277/9716c722), every e06dcaf result blob unchanged, no criteria/fixtures/orchestration edits, no real requests, no embeddings, no merge, no deploy; dual-target requirement checks 4/4 pass vs origin/main and origin/agent/agent/de8874a3803b with/without handoff-HEL-869.json
2026-10-26 04:09:58 +08:00
HEL-879 R5 real-machine window executed (2026-10-27 15:26:54-15:58:10 UTC, 31min16s, within 2h/3-call/40req/500k-token/200-TTS-segment/0-embed authorization): real chain verified on .12 (isolated 8360/8361 dual-entry + 8363 WSS bridge + dedicated worker 8082 + shared TTS 9880 recovered for the window only) -- 3 conversational calls incl. 3 continuous rounds with receipts acked (4 sharing_attempt ack_at/judged), 3 real barge-in interrupts (turn_cancel interrupted, old turn fail-closed), hangup deletes room with ~20ms residual, abrupt-drop rooms TTL-compensated clean after API restart epoch 4->5, voice/text mutual cancel via same chat lock observed (voice-cancelled-text direction); usage ledger 14/40 requests, 42717/500000 tokens, 12/200 TTS segments, 0 embeddings, 0 new spend, budget sealed (real usage preserved) + consistency snapshot archived; two real-machine fixes with evidence: livekit_wire server token now carries SDK-parity video grants (room_admin/room_create; grantless bearer rejected 401 by real server) and Twirp calls switched to JSON encoding (protobuf body unparseable), offline fakes updated, 352/352 pytest + ruff(src/tests/scripts) clean; self-signed cert reissued critical CA:FALSE SAN=IP after rustls CaUsedAsEndEntity rejection; D1 audition README corrected to 'non-custom-function entry' per user feedback (comment 01a17de1); precise ~1s interrupt stop timing and text-cancels-voice direction honestly listed as not-obtained (follow-up window); all window-only processes torn down, protected services untouched, ComfyUI stays paused
2026-10-28 00:00:41 +08:00
HEL-879 R5 real-machine window executed (2026-10-27 15:26:54-15:58:10 UTC, 31min16s, within 2h/3-call/40req/500k-token/200-TTS-segment/0-embed authorization): real chain verified on .12 (isolated 8360/8361 dual-entry + 8363 WSS bridge + dedicated worker 8082 + shared TTS 9880 recovered for the window only) -- 3 conversational calls incl. 3 continuous rounds with receipts acked (4 sharing_attempt ack_at/judged), 3 real barge-in interrupts (turn_cancel interrupted, old turn fail-closed), hangup deletes room with ~20ms residual, abrupt-drop rooms TTL-compensated clean after API restart epoch 4->5, voice/text mutual cancel via same chat lock observed (voice-cancelled-text direction); usage ledger 14/40 requests, 42717/500000 tokens, 12/200 TTS segments, 0 embeddings, 0 new spend, budget sealed (real usage preserved) + consistency snapshot archived; two real-machine fixes with evidence: livekit_wire server token now carries SDK-parity video grants (room_admin/room_create; grantless bearer rejected 401 by real server) and Twirp calls switched to JSON encoding (protobuf body unparseable), offline fakes updated, 352/352 pytest + ruff(src/tests/scripts) clean; self-signed cert reissued critical CA:FALSE SAN=IP after rustls CaUsedAsEndEntity rejection; D1 audition README corrected to 'non-custom-function entry' per user feedback (comment 01a17de1); precise ~1s interrupt stop timing and text-cancels-voice direction honestly listed as not-obtained (follow-up window); all window-only processes torn down, protected services untouched, ComfyUI stays paused
2026-10-28 00:00:41 +08:00
HEL-879 rework: fix the four manager-review edges - (1) dual-port launcher owns the app lifecycle exactly once: uvicorn listeners run with lifespan=off, either listener ending stops the other BEFORE the shared store closes, cancellation cleans up listeners first, and create_app's lifespan gains a fail-closed double-entry guard (the old shape silently re-initialized Store/epoch/life/chat_lock on the second entry); (2) room ownership survives delete failures: end_session keeps the session in a bounded persisted cleanup ledger until the remote delete lands (retry/idempotent, LiveKit 404 = already gone = settled), expiry moves rooms into that ledger instead of silently dropping them (the manager measured zero DELETE attempts on expired ends), graceful shutdown does not hang up live calls, restart compensates only this instance's own recorded rooms bounded by attempts/TTL+grace with abandonment recorded, and the router answers 503 (not 404) while a room is still owned so the browser keeps its owed DELETE; (3) TurnLedger binds each speech handle to its own generation stream explicitly (oldest pending handle gets the stream the SDK starts; a newer speech retires an older generation-less handle) - a handle without a generation can no longer steal the next turn's stream/receipt, the worker discards interrupted/failed bindings fail-closed before consuming; (4) release snapshot v3: verify requires three-way count equality (manifest source = manifest snapshot = restored actual; rows_match is an extra constraint, never an OR bypass), the rollback drill actually restores by source path into a persistent isolated tree and opens the restored DBs there, same-basename DBs get collision-free archive names, duplicate sources are refused; new regression file tests/test_voice_rework_hel879.py 14/14 reproducing every edge first, full pytest 338/338
2026-10-27 04:10:23 +08:00
2026-10-19 20:08:52 +08:00
HEL-879 rework materials: hatchling cache correction with the offline mem0ai wheel actually built from the fixed source (sha256 698284b6..., zero downloads/installs, no 3.10 mixing, recorded in release-artifacts.json), hel895 release doc §0 now separates the historical HEL-895/896 candidate from the current hel-879-assembly candidate and §4/§5 carry the v3 snapshot semantics, budget §6.2.4 amended - a sealed library is never reopened by raising total_requests (contradiction removed), runbook §3.3/3.5/3.6/4.4/4.5 corrected (bounded cleanup ledger replaces the unproven rooms-die-naturally claim; assistant_interim kind; real-but-default-off proactive seam; single-owner dual launcher), one comment-only correction in web/src/voice/session.ts removing the unproven server-auto-cleanup claim (no logic/style change; CSS and JS bundle hashes byte-stable), CG-011 + her incremental subtitles now have a real minimal backend seam (voice/seam.py + worker/llm_chat_api wiring, proactive default off) with the precise frontend interface and acceptance list in docs/rework/hel879/voice-seam-interfaces.md, real-SDK check extended to 10 items (worker callback path on real SpeechHandle/SpeechCreatedEvent; interim seam through real LLMStream) and re-run on .12 ALL PASS, handoff-HEL-879.json appended with the rework scope; regression at this head: pytest 341/341, ruff clean except 1 pre-existing error identical at 7f0a739, web typecheck 0 errors / lint 0 errors 1 pre-existing warning / vitest 88/88 / build passes (CSS index-BpZaybfD.css unchanged), voice chain 8/8, budget split 10/10, release offline R1-R7, http pagination 7/7, real-service browser 9/9 with zero non-loopback outbound, pinned-SDK 16/16+12/12+6/6+7/7+12/12, requirements checks 4/4 (origin/main and c121c42, each with/without handoff)
2026-10-27 04:27:04 +08:00

CyberGril

一个有自己世界的赛博伴侣。

不是聊天机器人。区别在于:它在你不说话的时候也在运行。


先看一眼

晚晚 · 介绍动画

▶ 打开完整动画 — 22 秒,纯代码生成,无任何外部资源

docs/intro.html          单文件动画,双击即播
docs/preview.webp        渲染好的预览(800×450 / 10fps / 22s / 0.5MB)
tools/render_preview.py  重新渲染、换分辨率、导出静帧

动画里有一条贯穿的主线:强调色 = 当前效价 v。 情绪低落时画面整体转冷,而不是靠字幕告诉你"她现在很难过"。

幕 时间 讲什么
膜 0–6s 物质撞在膜上弹回,信息穿过去
心跳 6–12s 没有输入时它也在跳;难过恢复得慢
记忆 12–17s 记得的会沉下去,但沉不到底
名字 17–22s —

四张静帧:docs/01-membrane.png 02-heartbeat.png 03-memory.png 04-title.png

动画本身的技术取舍:时间轴是纯函数 render(t),不做 RAF 累加。 所以任意一帧都能独立渲染——这也是 render_preview.py 能逐帧截图、 而不需要真实录屏的原因。


它是什么

一个单用户、本地优先、常驻的存在,通过一层"膜"与你通信:

  • 有自己的时钟 —— 无输入时仍在演化、仍有生活
  • 情绪是动力学系统 —— 慢变量、有惯性、有代价(会心情不好)
  • 记忆有形状 —— 三层结构,会遗忘,且遗忘有下限
  • 关系是一等实体 —— 信任、债务、未修复的事
  • 本体不可谈判 —— 立场不因任何对话、情绪、施压而改变

三层不可混淆的东西

层 载体 变化速度 谁能改
命 bazi_core.derived 几乎不变 只有整合器,每次 ±0.02,且留痕
运 fortune_period 按周/月 排盘
状态 State (v/a/d/e) 按秒/分 tick 循环

混掉任何两层,角色就会失真:

  • 命被对话改 → 人设漂移,变成一个讨好你的东西
  • 状态被写成描述 → 模型自己编情绪,动力学失效

架构

contracts/   数据契约 —— 全项目唯一的共同语言
providers/   模型接入 —— 按能力建模
core/        状态核心 —— 独立时钟,不依赖用户输入
memory/      三层记忆 + 遗忘
dyad/        关系账本
think/       模型路由 + 预算 —— 四档分配,按关键时刻
config/      models.yaml(版本号) + character.yaml(世界法则)

设计判断(这一节比代码重要)

1. OpenAI 兼容是 adapter,不是标准

对 LLM 和 Embedding,OpenAI 的形状(/v1/chat/completions、/v1/embeddings) 已经是事实标准,内部契约就直接用它。

但对 ASR / TTS / 形象,OpenAI 的形状会砍掉这个项目最需要的能力:

能力 OpenAI 形状的问题 我们的做法
ASR /v1/audio/transcriptions 是「文件进→文本出」,没有 partial;而对话必须要 partial 才能做端点检测 内部契约是事件流(partial → final);OpenAI 适配器作为降级档,supports_partial=False
TTS /v1/audio/speech 只有 text/voice/speed,没有副语言标签;而 [laughter] [breath] 是"像活的"的核心 内部契约带 tags / emotion;不支持的 provider 在 plan() 里显式降级并上报
形象 没有事实标准 自定义 RenderCmd 协议,驱动源必须是 State,不是 LLM 文本

关键机制:静默降级是不允许的。

planned = tts.plan(request)
if planned.lost_paralinguistics:
    # 日志里会出现 [degraded] speech_tags: ...
    ...

静默降级会让你在调优时永远找不到"它为什么不像人了"。

2. 状态必须以数值注入,不能用散文描述

✗ "你现在有点难过"          → 模型会自己发挥,甚至编出更多情绪
✓ v=-0.62 a=0.41 d=0.30    → 模型知道边界
  + visible="低落"
  + constraint="不得声称与此不符的内在状态"

StateBlock 里那个 constraint 字段不是装饰,它是唯一能阻止 模型绕过状态机的机制(外加输出校验与重写)。

3. 遗忘有下限

mem_episodic.accessibility 的下限是 0.03,不是 0。

  • 正常检索拿不到 → 想不起来
  • 情绪线索命中时能捞出来 → 但还在

所有现成框架都是"要么记住要么删除",没有这个中间态。

4. 未修复的事变成行为,不变成台词

✗ "你上次伤害了我"          → 指责,而且廉价
✓ 主动性 -0.1,回复变短      → 它不说,但它不一样了

5. 缺席可以被提及,不能被使用

✗ "我一直在等你,等了好久"   → 制造负罪感,这是操控
✓ "这几天我这边发生了点事"   → 它有自己的世界,不是空转等你

这条写在 core/projection.py 的注释里,也有对应的测试。

6. 大模型按「关键时刻」分配,不按时间分配

先纠正一个常见误解:60 秒心跳不该调用任何 LLM。

心跳做的是 s = b + (s-b)*exp(-dt/τ) + I,四个 float 一次 exp, 微秒级。给它配模型是架构错误,不是资源问题。

真正的 token 消耗在两处:每轮对话的输入上下文,以及后台任务的频次。

所以用四档:

档 用什么 场景 成本
none 不调用 心跳、主动性判定、填充词、表情驱动 ≈0
low 本地小模型 生活事件、记忆压缩、事实抽取、输出初筛 电费
standard 中大模型 + 短上下文 投机响应的第二段 低
high 旗舰 主对话台词、主动联系、危机 按 token

路由判定必须便宜 —— 查表,不调模型。为了省 token 先调一次模型,是本末倒置。

关键约束:

  • 主对话的下限是 low,不是 none。 预算耗尽时可以变笨,不能没台词。
  • 危机场景永不降级。 这是产品约束,不是性能优化。
  • 降级要可预测。 单次请求放不下只降一档;到硬阈值才降到下限。

省钱的真正杠杆,按收益排序:

  1. Prompt 缓存 —— 本体/世界法则固定放前缀,命中后便宜一个数量级。杠杆最大。
  2. 上游压缩 —— 小模型先把 20 条记忆压成 3 条,大模型只收结果。输入从 4000 降到 800 tokens。
  3. 批处理 —— 记忆整合攒 30 分钟一次批量处理,不是 20 次单条调用。
  4. 换小模型 —— 收益反而最小,而且伤害人设。

还有一个漂亮的副作用:成本降级可以被叙事化。 大模型不可用时它"有点走神"—— 这既符合人设,又是真的(实现同构的虚构)。


快速开始

# 1. 安装
pip install -e ".[dev]"

# 2. 配置
cp .env.example .env
# 编辑 .env 填入 API key(不填也能跑,会回退到 echo provider)

# 3. 跑测试
pytest -q

# 4. 检查配置能被正确加载与校验
python -c "from cybergril import load_settings; print(load_settings('config').character.name)"

默认配置不需要任何 API key 就能跑通测试 —— 因为内置了 echo(LLM)/ scripted(ASR)/ silence(TTS)三个离线 provider。 它们的价值是把接口形状固定下来,让上层可以先跑通再换真后端。


换成真模型

只改 config/models.yaml,不动任何代码。

tiers:
  high:
    provider: openai_compat
    model: deepseek-v4.1-flash
    base_url: https://api.deepseek.com/v1
    api_key_env: DEEPSEEK_API_KEY
  low:
    provider: openai_compat
    model: qwen3.5-4b-instruct
    base_url: http://127.0.0.1:8000/v1

tts:
  provider: cosyvoice        # 需要自己实现这个 adapter,见下
  model: fun-cosyvoice3-0.5b

新增一个 provider 只需要三步:

# 1. 继承对应的基类
class MyTTS(TTSProvider):
    name = "mytts"

    # 2. 诚实地声明能力
    @property
    def capabilities(self) -> Capabilities:
        return Capabilities(
            modality=Modality.AUDIO,
            streaming=Streaming.CHUNKED,
            speech_tags=frozenset({"laughter", "breath"}),
        )

    # 3. 实现 stream()
    async def stream(self, request): ...

# 注册
registry.register("tts", "mytts", MyTTS)

能力必须诚实。 夸大能力会让上层做出错误的规划, 而 plan() 会据此剥掉你其实需要的东西。


配置分层

文件 放什么 什么时候改
config/models.yaml 唯一允许写模型版本号的地方 换模型(每季度复核)
config/character.yaml 世界法则、情绪参数、关系参数 换人格(几乎不动)

两个文件互不污染。这是"防止文档和代码一起过时"的机制。

关于版本号的一个约定:只有 models.yaml 允许写版本号。 代码里所有模型引用走配置,不硬编码。


测试

pytest -q                    # 全部
pytest tests/test_mood.py -v # 情绪动力学
pytest -k degradation -v     # 降级机制

测试守的是设计判断,不是实现细节:

文件 守什么
test_contracts.py 接口形状
test_providers.py 降级必须显式上报
test_mood.py 情绪有惯性;负向恢复更慢
test_bazi.py 命是数值;强弱影响敏感度
test_projection.py 未覆盖事件必须计数;投影不泄漏技术词
test_store.py 遗忘有下限;间隔效应

下一步

地基只包含:契约、provider 抽象、配置、情绪动力学、八字编译、投影、存储 schema、关系账本。

还没有的(按依赖顺序):

  1. core/tick.py —— 四个循环(tick / consolidate / life / reach_out)
  2. memory/retrieval.py —— mood-congruent 检索
  3. memory/consolidation.py —— 整合与身份提升
  4. think/ —— 上下文组装、生成、三道校验
  5. express/ —— TTS 管道、形象、barge-in
  6. api/ —— WebSocket 会话

规格

完整设计见 docs/SPEC.md。

许可

私有项目,未授权。

S1 前端演示

独立的 React / TypeScript 前端位于 web/,运行 cd web && npm ci && npm run dev 后从普通入口进入明确标识的本机演示。真实后台、语音、视觉和备份尚未接通;不自动连接模型或现有服务。安装、构建和测试命令见 web/README.md,实现与后续接线见 docs/frontend/IMPLEMENTATION.md。

S
Description
No description provided
Readme
144 MiB
Languages
Python 57.2%
TypeScript 22.7%
CSS 19.9%
JavaScript 0.1%
HTML 0.1%