Dev Spec v2 · ← 返回文档首页
02 系统设计
1. 模块
products/talktale/
├── content/
│ ├── pipeline/ # 内容生产流水线(见 04)
│ ├── schema.py # 季、集、场景、台词、关卡、分支的数据结构
│ └── qa/ # 自动质检:难度、一致性、安全、配音回听、插画一致性
├── player/ # 剧集运行时:剧情状态机、分支变量、进度
├── checkpoint/ # 口语关卡:跟读 / 选择 / 自由回答 / 提示(见 05)
├── memory/ # 用户事实抽取与引用
├── messages/ # 角色消息(剧集之间)
├── freetalk/ # 自由对话(Pro)
├── learning/ # 词汇本、打卡、周报
├── safety/ # 年龄、危机处理、休息提醒(调用 core.compliance)
└── api/
2. 内容数据模型
一集的剧本是结构化 JSON(由流水线生成、校验后存储),播放器只解释执行,不在运行时生成剧情。
{
"episode_id": "ja_s1_e03",
"lang": "ja", "level": "beginner",
"goals": {"patterns": ["〜てもいいですか"], "vocab": ["レジ", "袋", "温めます"]},
"nodes": [
{"id": "n1", "type": "scene", "image": "img_ja_s1_e03_01", "lines": [
{"char": "yuki", "text": "いらっしゃいませ!あ、{user.name}さん!", "tr": {"en": "Welcome! Oh, {user.name}!"}, "audio": "aud_..."}
]},
{"id": "n2", "type": "checkpoint", "kind": "repeat", "target": "袋、いりますか?", "next": "n3"},
{"id": "n3", "type": "checkpoint", "kind": "choice", "options": [
{"say": "はい、お願いします", "set": {"bag": true}, "next": "n4a"},
{"say": "いいえ、大丈夫です", "set": {"bag": false}, "next": "n4b"}
]},
{"id": "n5", "type": "checkpoint", "kind": "free", "prompt_char": "yuki",
"question": "週末は何をしますか?", "expect": {"pattern": "〜ます", "topic": "weekend_plans"},
"memory_extract": ["hobby", "weekend_plan"], "fallback_line": "そうなんだ!いいね。", "next": "n6"},
{"id": "n6", "type": "scene", "if": "bag == true", "lines": [...]}
],
"recap": {"vocab_cards": [...], "cliffhanger": "..."}
}
- 台词中的占位符(
{user.name}、{memory.hobby})在运行时替换;带占位符的台词需要现场合成配音(很短,成本很低),其余台词使用预先生成的配音 if条件只能引用剧情变量和记忆字段,语法固定(简单的比较和与或),由播放器安全求值(不使用eval)
3. 数据库(schema talktale)
CREATE TABLE talktale.seasons (
id TEXT PRIMARY KEY, lang TEXT NOT NULL, ui_langs TEXT[] NOT NULL, region TEXT NOT NULL,
bible JSONB NOT NULL, -- 世界观、角色卡、故事线
status TEXT NOT NULL, -- generating|qa|live|retired
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE talktale.characters (
id TEXT PRIMARY KEY, season_id TEXT NOT NULL,
card JSONB NOT NULL, -- 性格、口头禅、关系(只能 friend / colleague)、边界
voice JSONB NOT NULL, -- 网关 tts 的 voice 标识(按区域)
ref_images TEXT[] NOT NULL -- 角色参考图(保持插画一致)
);
CREATE TABLE talktale.episodes (
id TEXT PRIMARY KEY, season_id TEXT NOT NULL, number INT NOT NULL,
version INT NOT NULL, -- 重新生成后版本加 1
script JSONB NOT NULL,
qa JSONB NOT NULL, -- 各项质检结果
assets_ready BOOLEAN NOT NULL,
free BOOLEAN NOT NULL,
rating_avg REAL, rating_n INT NOT NULL DEFAULT 0,
completion_rate REAL,
status TEXT NOT NULL, -- draft|qa_failed|live|regenerating|retired
content_cost_usd NUMERIC(8,4),
UNIQUE (season_id, number, version)
);
CREATE TABLE talktale.assets (
id TEXT PRIMARY KEY, episode_id TEXT NOT NULL, kind TEXT NOT NULL, -- image|audio|clip
object_key TEXT NOT NULL, meta JSONB
);
CREATE TABLE talktale.progress (
user_id TEXT NOT NULL, episode_id TEXT NOT NULL,
node_id TEXT NOT NULL, vars JSONB NOT NULL DEFAULT '{}',
started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, rating INT,
PRIMARY KEY (user_id, episode_id)
);
CREATE TABLE talktale.attempts ( -- 每次口语关卡
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, episode_id TEXT NOT NULL, node_id TEXT NOT NULL,
kind TEXT NOT NULL, transcript TEXT, score JSONB, hint_level INT NOT NULL DEFAULT 0,
audio_key TEXT, latency_ms INT, cost_usd NUMERIC(8,5),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE talktale.memories (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, key TEXT NOT NULL, -- 'name','hobby','goal'...
value TEXT NOT NULL, source_attempt TEXT, confidence REAL NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ
);
CREATE TABLE talktale.messages (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, character_id TEXT NOT NULL,
text TEXT NOT NULL, audio_key TEXT, scheduled_at TIMESTAMPTZ NOT NULL,
sent_at TIMESTAMPTZ, reply_attempt TEXT
);
CREATE TABLE talktale.vocab (
user_id TEXT NOT NULL, lang TEXT NOT NULL, term TEXT NOT NULL,
first_seen TEXT, strength REAL NOT NULL DEFAULT 0, due_at TIMESTAMPTZ,
PRIMARY KEY (user_id, lang, term)
);
CREATE TABLE talktale.usage (
user_id TEXT NOT NULL, day DATE NOT NULL,
freetalk_seconds INT NOT NULL DEFAULT 0, session_seconds INT NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, day)
);
4. 权益(core.entitlements)
| 键 | 免费 | 订阅 / 买断 |
|---|---|---|
season:{id} |
前 3 集 | 全部(买断只解锁这一季) |
daily_free_episode |
第一季每天 1 集 | — |
freetalk_seconds_day |
0 | 1800(仅订阅) |
messages |
每周 1 条 | 每集之间 2 条 |
5. 定时任务
| 任务 | 频率 | 说明 |
|---|---|---|
schedule_messages |
用户看完一集后触发 | 按剧情和记忆生成 1~2 条角色消息,安排在次日合适的时间 |
episode_health |
每天 | 评分 < 3.5(样本 ≥ 30)或完成率异常低的集 → 自动触发重新生成(见 04) |
weekly_report |
每周 | 学习周报 |
purge_audio |
每小时 | 删除超过 7 天的录音 |
session_timer |
实时(前端上报) | 连续使用达到阈值 → 休息提醒 |