TianTianSpeak 文档

文档/开放格式

collections 格式说明

题库是几种普通 JSON 文件。最简单的题库只要一个文件、几行字;复杂的可以有分组、配图、音频、拼音和译文。写好后在 App 的「题库管理」里导入即可。

最小的题库#

一个文件就是一个题库(collection set),一个 item 是一篇文章。这个文件已经可以导入:

my-bank.json
{
  "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(不经过包也行)
  • 所有引用都用相对路径,相对「引用它的那个文件」所在目录,可带子目录;不能是绝对路径或完整网址,不能含 ..。
  • 导入是全有或全无:任何被引用的文件找不到或解析失败,整个导入取消,不留半个题库。

通用字段#

索引的条目、题库包、题库、文章都可以带这些元数据,都可省略:

字段类型说明
namestring名称;不写用文件名
version正整数内容版本,你改了内容自己递增
descriptionstring简介,也认 简介 / 说明
categorystring一级分类,用于筛选
language"zh" | "en"决定打分方式:中文逐字、英文逐词;不写按正文推断
tagsstring[]用 / 分隔的层级标签,如 "语言/中文/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译文:中文文章放英文,反之亦然,「显示翻译」开关用
aligncenter = 居中,适合诗词;不写 = 左对齐

其他字段(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,内容块用键来引用。

Greek.article.json
{
  "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。

Greek.article.json
{
  "type": "article",
  "language": "en",
  "markdown": "# Are You Scared of Santa and His Beard?\n\n![Santa and a child](images/pogonophobia.jpg)\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![read aloud](https://example.com/pogonophobia.mp3)"
}
Markdown转成
# … ###### 标题小标题块(不朗读);没写 name 时,第一个一级标题当文章标题
段落(连续的行合并,中文之间不补空格)一个正文段落
列表项(- * + 1.,含 - [ ])、引用(>)去掉标记,一项一个正文段落
独占一行的 ![说明](地址)图片块;扩展名是 mp3 / m4a / wav / aac / ogg 则是音频块,mp4 / mov / m4v / webm 则是视频块。地址是 http(s) 网址,或不含 .. 的相对路径
**加粗**、*斜体*、__加粗__保留为行内标记
[文字](网址)、行内代码只留文字
代码块、分隔线、表格、HTML 注释忽略

Markdown 写法没有拼音、译文、对齐字段;需要这些时用 contents。

题库包(collection package)#

把多个题库排成一棵树,用 children。分组写 name + children,叶子写 collection 路径。

中小学生示范朗读.package.json
{
  "name": "中小学生示范朗读",
  "category": "语文教材",
  "children": [
    {"name": "小学", "children": [
      {"name": "一年级", "collection": "中小学生示范朗读/一年级.json"}
    ]},
    {"collection": "中小学生示范朗读/七年级.json"}
  ]
}

children 的顺序就是显示顺序。包里引用的文章,路径相对引用它的那个题库文件,不是相对包文件。叶子的 collection 也可以指向一个 .zip。

索引(collections)与在线发布#

自己搭一个在线题库,就要写一份索引:一个对象数组,每项指向一个题库或题库包。

collections.json
[
  {"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.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、题目 等。新写的文件建议用规范写法。

校验#

shell
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。