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 实时(前端上报) 连续使用达到阈值 → 休息提醒