文档/开放格式
collections 格式说明
题库是几种普通 JSON 文件。最简单的题库只要一个文件、几行字;复杂的可以有分组、配图、音频、拼音和译文。写好后在 App 的「题库管理」里导入即可。
最小的题库#
一个文件就是一个题库(collection set),一个 item 是一篇文章。这个文件已经可以导入:
{
"id": "my-first-bank",
"name": "我的第一个题库",
"language": "zh",
"items": [
{"title": "春晓", "text": "春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。"},
{"title": "静夜思", "text": "床前明月光,疑是地上霜。举头望明月,低头思故乡。", "align": "center"}
]
}只有 items 和每条的 text 是必填的,其他都可以不写。
五种文件#
| 名称 | 文件名 | 作用 |
|---|---|---|
| collections(索引) | collections.json | 在线题库的入口,列出可下载的题库或题库包 |
| collection package(题库包) | *.package.json | 把多个题库组织成一棵有序的树 |
| collection set(题库) | *.json | 一组文章,每个 item 是一篇 |
| collection article(文章) | *.article.json | 一篇富文本文章:内容块 + 图片 / 音频 / 视频 |
| catalog(目录) | *.catalog.json | 发布时生成的目录,只有标题,详情页只读它 |
每种都有 JSON Schema(draft 2020-12):collections、package、set、article、catalog。
collections.json ──path──▶ *.package.json ──collection──▶ set.json ──article──▶ x.article.json
└─path──▶ set.json(不经过包也行)- 所有引用都用相对路径,相对「引用它的那个文件」所在目录,可带子目录;不能是绝对路径或完整网址,不能含
..。 - 导入是全有或全无:任何被引用的文件找不到或解析失败,整个导入取消,不留半个题库。
通用字段#
索引的条目、题库包、题库、文章都可以带这些元数据,都可省略:
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 名称;不写用文件名 |
version | 正整数 | 内容版本,你改了内容自己递增 |
description | string | 简介,也认 简介 / 说明 |
category | string | 一级分类,用于筛选 |
language | "zh" | "en" | 决定打分方式:中文逐字、英文逐词;不写按正文推断 |
tags | string[] | 用 / 分隔的层级标签,如 "语言/中文/HSK/初级" |
稳定标识 id#
「读过没有」「已练 N 次」靠它在重新下载后保留。题库写 id,每篇也写 id,记录就挂在 题库id/条目id 上。只接受字母、数字、汉字和 ._-,不超过 64 字符,不以 . 开头。同一个 id 的题库再次导入,内容不同会覆盖,内容相同会提示已导入。
题库(collection set)#
内嵌正文的 item#
| 字段 | 说明 |
|---|---|
text | 必填,正文;空正文的条目被跳过 |
title | 标题;不写取正文第一行(最多 24 字) |
id / hash | 稳定标识,见上 |
audio | 示范音频,http(s) 网址(mp3 / m4a / m3u8,在线流式播放) |
pinyin | 每个汉字一个带调拼音,空格分隔,与正文汉字按顺序一一对应;个数对不上时整条拼音被忽略 |
translation | 译文:中文文章放英文,反之亦然,「显示翻译」开关用 |
align | center = 居中,适合诗词;不写 = 左对齐 |
其他字段(level、slug、grade 等)会被忽略,可以放自己的备注。
引用 article 的 item#
{"article": "lilliput/02.article.json", "id": "02", "title": "覆盖标题"}article 必填,是以 .json 结尾的相对路径;title、id 可选,用来覆盖文章自己的。导入时文章会展开成一个普通 item,所以朗诵、复述、打分、历史记录都照常工作。
文章(collection article)#
需要配图、音频或小标题时,用 article 文件。正文在 contents,媒体放在同级的 images / audios / videos,内容块用键来引用。
{
"type": "article",
"id": "wordroots-greek-pogonophobia",
"name": "Are You Scared of Santa and His Beard?",
"language": "en",
"tags": ["Word Roots/Greek"],
"images": {
"photo": {"src": "images/pogonophobia.jpg", "alt": "Santa and a child", "credit": "Adobe Stock"}
},
"audios": {"read": "https://example.com/pogonophobia.mp3"},
"contents": [
{"image": "photo"},
{"audio": "read"},
"If you see a big bushy beard, you might have **pogonophobia**.",
{"text": "The roots", "style": "heading"},
{"text": "*pogon* means beard and *phobia* means fear.", "translation": "pogon 是胡须,phobia 是恐惧。"}
]
}type必须是"article";contents至少有一块可朗读的正文段落。- 内容块是字符串(一个段落),或对象:
text、style(paragraph默认 /heading小标题,小标题不朗读)、image/audio/video(引用媒体键)、pinyin/translation/align(每段可以各自选择写不写)。 - 行内标记只有三种:
**粗体**、*斜体*、***粗斜体***;\*表示字面的星号。 - 媒体的
src是 http(s) 网址,或相对本文件的路径;alt是替代文字,credit写来源与版权。内容块引用了不存在的键,整个导入失败。
用 Markdown 写正文#
不需要拼音、译文、居中时,可以不写 contents,直接把正文写成 Markdown 字符串(字段名 markdown,也认 md)。导入时会转成上面的内容块,朗诵、复述、打分和阅读界面与 contents 写法完全一致;两者都写时只用 contents。
{
"type": "article",
"language": "en",
"markdown": "# Are You Scared of Santa and His Beard?\n\n\n\nIf you see a big bushy beard, you might have **pogonophobia**.\n\n## The roots\n\n- *pogon* means beard.\n- *phobia* means fear.\n\n"
}| Markdown | 转成 |
|---|---|
# … ###### 标题 | 小标题块(不朗读);没写 name 时,第一个一级标题当文章标题 |
| 段落(连续的行合并,中文之间不补空格) | 一个正文段落 |
列表项(- * + 1.,含 - [ ])、引用(>) | 去掉标记,一项一个正文段落 |
独占一行的  | 图片块;扩展名是 mp3 / m4a / wav / aac / ogg 则是音频块,mp4 / mov / m4v / webm 则是视频块。地址是 http(s) 网址,或不含 .. 的相对路径 |
**加粗**、*斜体*、__加粗__ | 保留为行内标记 |
[文字](网址)、行内代码 | 只留文字 |
| 代码块、分隔线、表格、HTML 注释 | 忽略 |
Markdown 写法没有拼音、译文、对齐字段;需要这些时用 contents。
题库包(collection package)#
把多个题库排成一棵树,用 children。分组写 name + children,叶子写 collection 路径。
{
"name": "中小学生示范朗读",
"category": "语文教材",
"children": [
{"name": "小学", "children": [
{"name": "一年级", "collection": "中小学生示范朗读/一年级.json"}
]},
{"collection": "中小学生示范朗读/七年级.json"}
]
}children 的顺序就是显示顺序。包里引用的文章,路径相对引用它的那个题库文件,不是相对包文件。叶子的 collection 也可以指向一个 .zip。
索引(collections)与在线发布#
自己搭一个在线题库,就要写一份索引:一个对象数组,每项指向一个题库或题库包。
[
{"name": "HSK 阅读", "path": "HSK-阅读.package.json",
"description": "HSK 1–6 级阅读短文", "category": "HSK", "language": "zh"},
{"path": "普通话水平测试-2024-朗读作品50篇.json"}
]path 必填,相对索引网址,以 .json 或 .zip 结尾;包以 .package.json 结尾。索引里不能再套索引。
打成一个 zip#
把入口文件和它引用的所有文件(题库、文章、图片、音频)放进一个 zip,分发和导入只要一个文件。「题库管理」选 .zip,或索引里 path 写 .zip 都行。
Greek.zip
├── Greek.json ← 入口:一个题库(或 *.package.json,或一篇文章)
└── Greek/
├── pogonophobia.article.json
└── images/pogonophobia.jpg- 入口文件在 zip 根目录,且必须唯一。整个文件夹压缩时外面多套的一层会自动去掉。
- 包用
*.package.zip,单个题库用*.zip。 - 只支持 stored / deflate,不支持加密、分卷、Zip64;单个文件上限 256 MB,整包 2 GB。
__MACOSX/、._*、.DS_Store会被忽略。 - macOS 上打包:
zip -r Greek.zip Greek.json Greek -x '*.DS_Store'
宽松的简写#
为了兼容,下面几种读取时也接受(Schema 只描述规范写法):
["第一段文字", "第二段文字"]
[{"title": "标题", "text": "正文"}]
{"name": "题库名", "items": [{"title": "标题", "text": "正文"}]}正文字段也认 content、passage、body、正文 等;列表字段也认 questions、passages、题目 等。新写的文件建议用规范写法。
校验#
pip install jsonschema
python3 - <<'EOF'
import json
from jsonschema import Draft202012Validator as V
schema = json.load(open("collection-set.schema.json"))
print(list(V(schema).iter_errors(json.load(open("my-bank.json")))))
EOF空列表就是通过。Schema 描述的是规范写法,比 App 的读取更严格;它检查不了被引用的文件在不在,所以带引用的题库还是要在 App 里导入试一次。
让 AI 来写,并用上面的 Schema 校验。见 用 AI 制作 collections。