Files

130 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 苍梧疑冢 · 九章互动叙事
陌生人交友产品的单人互动叙事:第一季九章完整故事 + 初步相处线索报告 + 分享卡,**不含真实匹配和聊天**。
完整可玩剧本:[九章全文](docs/script/season1-full.md);时间线、选择点地图与伏笔回收:[整季说明](docs/script/season-design.md)。
全季 49 个主线交互点、122 条按钮出口,保留第一幕十条自适应支线。前三章原文大体保留,补齐第四至九章并接通正式游玩入口。
行为只能提供待验证的关系线索,不是已验证的人格量表或择偶预测模型。
产品共识见 `CONTEXT.md`,架构决策见 `docs/adr/0001`~`0004`,剧本母本见
`docs/story-framework.md`(第 4 节是 Act 1 全部选择点地图)。
## 怎么跑
```bash
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,与前端互不干扰):
```bash
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`。