苍梧疑冢 · 九章互动叙事

陌生人交友产品的单人互动叙事:第一季九章完整故事 + 初步相处线索报告 + 分享卡,不含真实匹配和聊天。

完整可玩剧本:九章全文;时间线、选择点地图与伏笔回收:整季说明。 全季 49 个主线交互点、122 条按钮出口,保留第一幕十条自适应支线。前三章原文大体保留,补齐第四至九章并接通正式游玩入口。 行为只能提供待验证的关系线索,不是已验证的人格量表或择偶预测模型。

产品共识见 CONTEXT.md,架构决策见 docs/adr/0001~0004,剧本母本见 docs/story-framework.md(第 4 节是 Act 1 全部选择点地图)。

怎么跑

pnpm install
pnpm dev          # 浏览器开发(无 API key 时自动降级 Mock,见下)
pnpm build        # 类型检查 + 产物构建
pnpm verify       # 无头校验:选择点地图对照母本 + 引擎三章流程 + 重玩语义(Mock)
pnpm verify:season # 九章、全部新增选项、三种见证×三种治理结局、隐私隔离
pnpm fetch:asr     # 下载端侧 ASR 资产(sherpa-onnx WASM + SenseVoiceSmall 模型,约 250MB,进 public/asr/,已 gitignore)
pnpm script:season # 从章节 JSON 生成完整剧本文档
pnpm script:review # 更新全季评审包
pnpm verify:live  # 真 key 冒烟:C1-01 抽取 + C1-02 情绪签名 + 7 维 score(读 .env)
pnpm lint         # oxlint
pnpm tauri dev    # Tauri 桌面壳(Rust 工具链需先 source ~/.cargo/env)

Jev(TypeSafe AI System One)配置

复制 .env.example 为 .env,填入 key:

VITE_TYPESAFE_API_KEY=<your key>
  • 有 key:前端直连 POST https://api.typesafe.ai/v1/systemone(HttpJevClient)。
  • 无 key:自动降级 MockJevClient(确定性伪随机返分,console 有提示),开发联调不受影响。
  • .env 已在 .gitignore 中,绝不能入库;scripts/verify-live.ts 手动解析 .env (不引 dotenv),任何输出都不打印 key。
  • 请求/响应 schema 已按真接口 curl 实测对齐(2026-10-03):Question 文本字段为 instructions;choice 的 criteria 是 {选项: 描述} map;score 的 criteria 为 2-10 级有序数组(索引 0 起),答案是概率加权浮点值 + legend + probabilities。

项目结构

content/chapters/        第一季九章剧情数据(JSON,原三章 + 新增六章)
src/engine/              数据驱动剧情引擎(纯 TS,与 UI 解耦)
  types.ts               章节 schema:dialogue / choice / narration / cutscene / identity / chapterEnd
  engine.ts              StoryEngine 状态机:节点推进、分支、情绪签名→场景变体、
                         身份抽取(extractIdentity)与确认(confirmIdentity)、重玩
  eventStore.ts          EventStore 接口(append-only,只有 append/list)+ localStorage 实现
src/services/jev.ts      JevClient 接口 + HttpJevClient / MockJevClient
src/asr/                 端侧转写:ondevice.ts 引擎(Worker 内跑 sherpa-onnx WASM)、
                         ondevice-asr.worker.ts、capture.ts 16k PCM 采集(AudioWorklet)、
                         segment.ts 长音频切分(纯函数,verify 直测)
src/ui/                  最简影游界面(对话框/选项/VoiceInput 语音输入/身份登记/场景变体渐变)
src/ui/report/           画像报告(Jev 7 维 score)、勋章映射、canvas 分享卡
scripts/verify.ts        内容完整性与流程校验(Mock 回归)
scripts/verify-live.ts   真 key 冒烟
src-tauri/               Tauri 2 壳(iOS/Android/桌面共用)

架构要点

  • append-only 事件存储(ADR-0004):EventStore 类型层面只有 append/list, 没有 update/delete 路径。重玩 = 新 sessionId 的新轨迹,旧事件原样保留。
  • 三通道边界(ADR-0003):主线全部预写在 JSON 里;Jev 在讲述点只做 "情绪签名 → 预写场景变体" 的选择题(通道 1);C3-02 问心幻境通过 variantFrom: "C1-02" 复用第一章的记忆解析。
  • 语音输入:VoiceInput 组件优先用 Web Speech API(zh-CN,continuous + interimResults,实时转写),不支持的环境(如 Tauri WKWebView)优雅降级为文本框。 只保留转写文本、不保存任何音频,天然符合 ADR-0004"特征提取后即焚"。
  • 端侧小模型转写(第一优先通路):pnpm fetch:asr 下载 sherpa-onnx WASM 运行时 (k2-fsa 官方 vad-asr space 构建)+ SenseVoiceSmall int8(中英日韩粤,自带情感/事件 标签)到 public/asr/(不进 git);首次使用懒加载进 CacheStorage(有进度提示), 在 Web Worker 里本地解码——音频不出设备,ADR-0004"即焚"在最强意义上成立。模型未就绪/ 失败/浏览器不支持时静默退回 Web Speech 或录音上传通路。VITE_ONDEVICE_ASR=0 可关闭。 资产获取脚本会把运行时 glue 内嵌的 Emscripten 打包清单改写为只含本项目的模型两个文件 (普通 wasm/asr 运行时只导出流式 API,SenseVoice 必须用 vad-asr 构建的 Offline API)。
  • C1-01 语音化身份登记:馆长邀请自由介绍(称呼/年龄/城市/职业/学历), Jev 一次调用并行抽取:gender(choice)、age(score 分档)、occupation(choice)、 education(choice)、city(noul 判断是否提及 + 城市 choice 兜底); 抽不出的字段留空,进确认页由用户修改补填,确认后 append profile 事件 (身份字段是匹配硬过滤字段,用户有权直接改,不进行为信号流)。
  • 画像报告:Act 1 结束后把全部行为轨迹(不含 profile 字段)汇总为 state, 一次 Jev 调用并行问 7 个维度的 score 问题(10 级 criteria),API 返回的 0-9 浮点分数 +1 映射为 1-10 级展示;勋章取置信度最高的维度。

已知缺口

以下保留早期 MVP 的部署备注;九章内容与自适应支线路由现已接入。七维标量评分仍属试验性接口,不能将置信度当心理效度;真实偏好确认、匹配与聊天仍未实现。

  • CORS:浏览器直连 api.typesafe.ai 可能有跨域限制,正式环境需加后端代理转发。
  • Web Speech API 覆盖:Chrome/Edge 可用,Tauri WKWebView 与 Firefox 不支持 (自动降级文本框);原生端的正式 ASR 管线未接。
  • 端侧 WASM 需真机验证:端侧转写(sherpa-onnx WASM + SenseVoiceSmall)的 模型完整性、glue 打包清单改写、Offline 解码链路已在 node 垫片环境实跑通过 (官方 zh 测试音频转出正确文本 + 情感/事件标签),但浏览器/WKWebView 里的 完整链路(CacheStorage 下载、Worker blob URL、麦克风采集)只能真机验证; 长音频(>28s)按静音谷切分是能量启发式,无 VAD 精确(vad-asr 运行时自带 VAD API, 留作后续升级);SenseVoice 的 emotion/event 标签已透传但尚未接进讲述事件。
  • Tauri SQLite 未实现:EventStore 目前只有 localStorage 实现;原生端应用 tauri-plugin-sql 实现同一接口(表只建 INSERT/SELECT,不建 UPDATE/DELETE 路径), 预留位置见 src/engine/eventStore.ts 末尾注释。
  • Tauri 移动端(iOS/Android)推送与构建未验证:目前只验证了 Rust 壳 cargo check 与 tauri info,未跑 tauri android/ios build。
  • 追问补全未接:讲述点 schema 里有 followUp 策略占位(Jev 缺失维度检测 + NPC 口吻追问,至多 1-2 句),MVP 未实现。
  • 美术全部占位:CSS 色块 + 渐变,无图片/视频素材;cutscene 节点是占位渲染。
  • 自适应支线路由未实现:story-framework 第 6 节的低置信度维度插支线属于后续章节工作。

后端代理

server/ 是独立子包(Node 22 + Hono,自己的 lockfile,与前端互不干扰):

pnpm --dir server install
cp server/.env.example server/.env   # 填 TYPESAFE_API_KEY(key 只在服务端)
pnpm --dir server dev                # 默认 http://localhost:8787
pnpm --dir server smoke              # 冒烟:代理调真实 Jev + /api/asr 自测
  • POST /api/jev 透传 systemone(状态码原样返回),解决浏览器直连的 CORS 问题; 前端在根 .env 设 VITE_JEV_PROXY_URL=http://localhost:8787 即切代理模式, 此时前端不需要 VITE_TYPESAFE_API_KEY,未配置时回退直连(见 src/services/jev.ts)。
  • POST /api/asr 为移动端 ASR 预留:provider 可插拔(当前 Mock 占位),音频遵守 ADR-0004"特征提取后即焚"不落盘,返回 { transcript, prosody }。
  • 详见 server/README.md。
S
Description
No description provided
Readme
3.1 MiB
Languages
TypeScript 96.3%
CSS 3.4%
Rust 0.2%
HTML 0.1%