Files
English/Doc/COURSE-PACK-JSON.md
T

280 lines
14 KiB
Markdown
Raw 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.
# 课程内容包 JSON 格式
> 对应《学习引擎规格》第 5 节。本文说明 A0–B1 单元内容包的文件位置、字段和程序校验规则。所有等级共用这一套结构,由 `CourseRepository` 用同一套规则转换成学习和复习数据;A0 的内容标准仍以《A0 初始核心课程包》为准(见 4.10)。
## 1. 文件与工具
| 路径 | 内容 |
|---|---|
| `kouyu_english/assets/courses/course-map.json` | 主题地图:等级、单元顺序、每级的数量范围、名称白名单 |
| `kouyu_english/assets/courses/<等级>/<单元ID>.json` | 单元内容包,一个单元一个文件 |
| `kouyu_english/assets/courses/A0/assessment.json` | A0 阶段测评题(见 4.11 |
| `kouyu_english/tool/courses/lexicon/` | 锁定的 CEFR-J 数据与 `SOURCES.md` |
| `kouyu_english/tool/courses/pools/` | 等级词池(`build_word_pools.py` 生成) |
| `kouyu_english/tool/courses/validate.dart` | 校验全部内容包,报告词池覆盖率 |
| `kouyu_english/test/course_content_test.dart` | 在 `flutter test` 中要求校验零错误 |
```sh
cd kouyu_english
dart run tool/courses/validate.dart # 错误和覆盖率
dart run tool/courses/validate.dart --uncovered B1 # 按主题列出未覆盖的词池词
dart run tool/courses/validate.dart --levels B1-U14 # 打印条目的计算等级
```
运行时由 `lib/core/courses/course_repository.dart` 读取:`main` 启动前先加载 A0`AppState.load` 再加载其余等级。学习(教学段、对话、写作、独立表达)和复习(产出核心的 `match``review`)都只读这些 JSON,代码里不再写死课程数据。`test/course_self_consistency_test.dart` 要求每个示范答案都能通过它自己的判定规则。
## 2. 当前内容(2026-09-18A1B1 为 `draft`
| 等级 | 单元 | 产出核心 | 理解词 | 听读材料 | 词池覆盖 |
|---|---|---|---|---|---|
| A0 | 10 | 见 A0 标准 | — | — | —(`approved`,不参与词池校验) |
| A1 | 21 | 265 | 598 | 42 | 840/93390.0% |
| A2 | 15 | 217 | 862 | 44 | 1040/107896.5% |
| B1 | 14 | 206 | 1121 | 42 | 1345/155286.7% |
A1–B1 全部由 AI 起草、程序校验通过,尚未经过独立审核和用户确认,不能冻结。
## 3. course-map.json
```jsonc
{
"schemaVersion": 1,
"goal": "日常生活与旅行",
"sources": ["CEFR-J Vocabulary Profile 1.5(…)", "CEFR-J Grammar Profile 20180315(…)"],
"minimumPoolCoverage": 0.85, // 等级词池覆盖下限
"alwaysAllowedWords": ["oh", "ok", ], // 任何文本都可用的语气词
"names": ["tom", "london", "mr", ], // 全局专名、缩写
"levels": [{
"id": "B1",
"title": "独立应对大多数情况",
"coreRange": [10, 15],
"receptiveRange": [70, 110],
"materials": {"count": [2, 3], "listeningWords": [250, 550], "readingWords": [300, 500]},
"assessment": "A0/assessment.json", // 可选:本级阶段测评题,目前只有 A0 有
// A1 的材料较短,且可选 "unknownTolerance"(默认 0.02)放宽超纲词占比
"units": [{"id": "B1-U01", "file": "B1/B1-U01.json", "title": "…", "categories": ["旅行"], "canDo": "…"}]
}]
}
```
各等级范围:
| 等级 | 产出核心 | 理解词 | 听读材料 | 听力词数 | 阅读词数 |
|---|---|---|---|---|---|
| A1 | 1015 | 2035 | 2 篇 | 60120 | 80150 |
| A2 | 1015 | 4065 | 23 篇 | 120280 | 150250 |
| B1 | 1015 | 70110 | 23 篇 | 250550 | 300500 |
A1 材料短、面向零基础,`unknownTolerance` 设为 0.08(听读材料默认 0.02):允许更多“课程尚未正式教、但零基础者需要”的常用词,前提是每个都写进 `glosses` 给出中文,问题选项用中文以免受“选项须在原文出现”的限制。
`units` 的顺序就是推荐学习顺序,校验“已教的词”时按此顺序累积。
## 4. 单元文件
### 4.1 ID 规则
所有 ID 都以单元 ID 开头,序号从 1 连续编号:
| 前缀 | 对象 | 例子 |
|---|---|---|
| `W` | 产出核心:单词 | `B1-U14-W01` |
| `P` | 产出核心:词块或句型 | `B1-U14-P01` |
| `R` | 理解词 | `B1-U14-R01` |
| `S` | 教学段 | `B1-U14-S1` |
| `C` | 场景 | `B1-U14-C1` |
| `T` | 任务 | `B1-U14-T1` |
| `M` | 听读材料 | `B1-U14-M1` |
### 4.2 顶层字段
```jsonc
{
"schemaVersion": 1,
"id": "B1-U14",
"revision": 1,
"level": "B1",
"title": "写说明与投诉", // title/categories/canDo 必须与 course-map 一致
"categories": ["日常", "旅行"],
"canDo": "写邮件说明经过并提出要求",
"status": "draft", // draft / reviewed / frozen
"meta": {
"draftedBy": "claude-opus-5",
"draftedAt": "2026-09-17",
"sources": ["CEFR-J Vocabulary Profile 1.5", "CEFR-J Grammar Profile 20180315"],
"reviews": [], // 审核记录
"frozenAt": null
},
"names": ["seaview", "palace"], // 本单元专名,小写
"grammar": [{"id": "142", "use": "active"}, {"id": "215", "use": "receptive", "reason": "…"}],
"coreItems": [], "receptiveWords": [], "segments": [], "scenes": [], "tasks": [],
"materials": [], // A1 起(A1 为短材料)
"rubric": {"dimensions": []}, // A2 起(A1 无开放任务评分标准)
"excluded": [{"en": "…", "reason": "…"}]
}
```
### 4.3 coreItems(产出核心)
```json
{
"id": "B1-U14-P08",
"type": "phrase",
"en": "I would like to request a full refund.",
"zh": "我想申请全额退款。",
"sceneWord": false,
"match": [["request a full refund", "request a refund"]],
"example": {"en": "I would like to request a refund for the second night.", "zh": "我想申请退还第二晚的费用。"},
"review": {"prompt": "在邮件里提出退款要求。"}
}
```
- `type``word` / `phrase` / `pattern`
- `match`:判定用户是否用到本项。外层数组之间为“且”,内层为“或”;每个词条按连续短语匹配(缩写展开、忽略大小写和标点)。`en``example.en` 都必须满足。
- `sceneWord`:高于本等级或 CEFR-J 未收录、但场景必需的词句设为 `true`,并写 `sceneReason`;每单元不超过产出核心的 1/3。
- `review.prompt`:复习时的中文任务,不能直接包含英文答案。
### 4.4 receptiveWords(理解词)
```json
{"id": "B1-U14-R01", "en": "furthermore", "zh": "此外"}
```
- 不高于本等级加一级(B1 单元最高 B2)。
- 不能是 A0 或之前单元已教的词(按词形还原比较)。
- 必须出现在本单元的教学段、场景或听读材料文本中。
### 4.5 segments(教学段)
每单元 2–4 段,每段 8–15 分钟、最多 6 个新产出核心:
```jsonc
{
"id": "B1-U14-S1", "title": "说明写信目的", "minutes": 12,
"itemIds": ["B1-U14-P01", "B1-U14-P02"],
"listening": {"text": "…", "question": "…", "options": ["正确", "干扰", "干扰"]},
"speaking": [{"text": "…", "tip": "可选的发音提示"}],
"reading": {"text": "多行用 \n", "question": "…", "options": ["…", "…", "…"]},
"writing": {"prompt": "…", "example": "…", "itemIds": ["B1-U14-P01"]},
"independent": {"prompt": "…", "itemIds": ["B1-U14-P02"]}
}
```
- 选项恰好 3 个,第一个是正确答案(界面随机排序);选项不能互为子串。英文选项至少两个在原文出现,且正确选项必须出现。
- `writing.example` 必须用到它引用的每个产出核心。
### 4.6 scenes 与 tasks
```jsonc
{
"id": "B1-U14-C1", "kind": "main", // 恰好 1 个 main,另有 23 个 variant
"title": "…", "learnerRole": "客人", "aiRole": "酒店客服经理", "setting": "…",
"turns": [ // 47 轮
{"ai": "AI 说的话", "goal": "本轮目标", "model": "学习者示范回答", "zh": "示范的中文", "itemIds": ["B1-U14-P01"]}
]
}
```
```json
{"id": "B1-U14-T1", "sceneId": "B1-U14-C1", "goal": "…", "itemIds": ["B1-U14-P01"], "slots": ["目的", "经过", "结果", "要求"]}
```
- `model` 必须用到本轮 `itemIds` 引用的产出核心。
- 每个场景至少有一个任务,每个任务至少 1 个信息槽。
- 每个产出核心都要出现在教学段、场景示范和任务中。
### 4.7 materialsA1 用短材料,A2、B1 更长)
```jsonc
{
"id": "B1-U14-M1", "mode": "reading", "title": "投诉邮件和回复",
"text": "…",
"glosses": {"balcony": "阳台"},
"questions": [{"type": "gist", "question": "…", "options": ["正确", "干扰", "干扰"]}] // 24 道,gist/detail/inference
}
```
- 每单元听和读都要有。
- 超纲词(见第 5 节)按词次不超过该级 `materials.unknownTolerance`(默认 2%,A1 为 8%),每个超纲词都要写进 `glosses`
- A1 材料应尽量把本单元的理解词织进听读文本,让理解词在教学段之外再复现一次(本级平均单元内复现率约 77%);本单元的产出核心与理解词都算已教词,写进材料不计超纲。
### 4.8 rubricA2 起)
```json
{"dimensions": [{"name": "说明经过", "bands": ["经过说不清", "能按顺序说明经过,但缺少结果", "按顺序说明经过、结果,并附上证据"], "pass": 1}]}
```
至少 2 个维度,每维至少 3 档;`pass` 是及格档在 `bands` 中的下标(从 0 起),须大于 0。
### 4.9 可选字段:显式规则优先
以下字段都可省略;省略时由 `itemIds` 引用的产出核心推导(`match` 合并成一组“或”,示范取 `en`),写了就以写的为准。规则写法见 `lib/core/courses/answer_rules.dart`:外层“且”、内层“或”,内层首项可写 `#min:N` 表示至少用上 N 项;词条可写 `a + b``/正则/`,以及 `#spelling``#digit``#word``#question``#numbers:N``#words:N` 这些结构。
| 位置 | 字段 | 作用 | 省略时 |
|---|---|---|---|
| 单元 | `status` | `approved` / `draft` 等 | `draft` |
| 单元 | `vocabulary` | 整课词表(离线查词) | 产出核心 + 理解词 |
| 教学段 | `grammarNote` | 本段语法提示 | 不显示 |
| 教学段 | `vocabulary` | 预习词表 `[{id, word, meaning, example, exampleMeaning}]` | 本段产出核心 |
| 教学段 | `sceneId` | 本段对话用的场景 | 第 N 段用第 N 个场景,没有则用 main 场景 |
| 教学段 | `writing.accept` / `writing.hint` | 写作判定规则 / 未通过时的提示 | 由 `writing.itemIds` 推导 |
| 教学段 | `independent.accept` / `independent.help` | 独立表达的完整规则 / 帮助 | 由 `independent.itemIds` 推导;为空则任何真实尝试都算 |
| 场景 | `goal` | 对话目标 | `setting`,再没有则 `title` |
| 场景 | `practice` | 同时作为自由练习场景:`{title, summary, recapPrompt, alwaysOpen}`,都可省略;写 `{}` 即可 | 不进入自由练习 |
| 轮次 | `aiZh` | `ai` 这句的中文(`zh` 是示范回答的中文) | 不提供翻译 |
| 轮次 | `accept` | 本轮判定用的一组“或” | 由本轮 `itemIds` 推导 |
自由练习场景在其所属单元完成后解锁,`alwaysOpen: true` 则一开始就开放。
### 4.10 A0 单元
A0 用同一结构,文件为 `A0/a0-01.json``a0-10.json`,在 `course-map.json` 中排在最前。为兼容已保存的学习进度,A0 保留原有 ID:单元 `a0-01`,教学段 `a0-01-a`(拆段课为 `a0-04-a/b/c``a0-08-a/b/c`),产出核心 `A0-W01` / `A0-P01`。A0 的规则、提示和对话都用上面的显式字段写明;`validate.dart` 只把 A0 的产出核心和听写句当作“已教的词”,不按 A1–B1 的数量范围和词池校验 A0。
### 4.11 阶段测评(assessment.json
等级在 `course-map.json` 里写了 `assessment`,就由 `CourseRepository` 加载对应文件,运行时代码在 `lib/core/courses/assessment.dart`。目前只有 A0 的 `A0/assessment.json`
```jsonc
{
"schemaVersion": 1,
"packs": [
{"id": "A0-E1", "level": "A0", "tasks": [
{"id": "A0-A-L1", "skill": "listening", "prompt": "选择正确答案",
"audio": "Hello. Im Mia.", "choices": ["自我介绍", "买东西", "问时间"], "answerIndex": 0},
{"id": "A0-A-W1", "skill": "writing", "prompt": "用英语介绍自己。",
"accept": [["hello", "hi"], ["i am", "my name is"]],
"guide": "试着同时写一句问候和姓名,例如:Hello. I’m …"}
]},
{"id": "A0-E1R", "level": "A0", "replaces": "A0-E1", "tasks": []}
]
}
```
- `skill``listening` / `reading` / `writing` / `speaking`。听力、阅读是选择题,写 `choices``answerIndex`(从 0 开始);听力另写 `audio`
- 写作、口语是开放题,`accept` 用 4.9 的规则写法,`guide` 是答错后展示的要求。
- 没有 `replaces` 的套题按文件顺序解锁:第一套直接开放,之后每套要在上一套通过 24 小时后才开放。
-`replaces` 的是替换题,用不同的人名、地点、数字考同样的能力,成绩记在原题 ID 上。
- 题目和套题 ID 写进学习记录,发布后不要改。
`test/course_self_consistency_test.dart` 检查:每道开放题都有 `accept``guide`,每道选择题的 `answerIndex` 有效,`replaces` 指向存在的套题。
## 5. 文本用词检查
“已知词”包括:`alwaysAllowedWords`、功能词、A0 已教的词、推荐顺序在前的单元的产出核心和理解词、本单元的产出核心和理解词(均按词形还原),以及全局与本单元的 `names`。数字不计。
| 文本 | 允许的未知词 |
|---|---|
| 教学段的听、说、读、写示范,场景的 AI 句和示范句 | 0 |
| 听读材料 | ≤ `materials.unknownTolerance`(默认 2%,A1 为 8%),且全部有 `glosses` |
注意:等级低于本单元、但还没有被任何单元教过的词(如 `history``culture`)同样算未知词。要在文本中使用,须改写,或把它加为本单元的理解词。
## 6. 等级与覆盖
- 语法:`active` 语法项不高于本等级,否则写 `reason`
- 产出核心中的词不高于本等级,除非是场景词。
- 覆盖率:等级词池(`tool/courses/pools/`)中,被产出核心、`match` 词块或理解词覆盖的词(含 A0 已教)占比须 ≥ `minimumPoolCoverage`
## 7. 状态流转
`draft`AI 起草、校验通过)→ `reviewed`(独立审核后,在 `meta.reviews` 记录审核模型、日期和问题清单)→ `frozen`(用户确认,填 `frozenAt`)。已冻结单元的 ID 不复用;修改须提升 `revision` 并遵循《学习引擎规格》5.5 的 stageVersion 规则。