ci-story 是做什么的?
每一课都是一个短视频。一小群演员表演一个简单的故事,同时一个平静的声音朗读简短的句子,每个句子重复多次,图片使含义一目了然。字幕使用该语言的文字加罗马拼音,并高亮正在说的词。最后约15%没有字幕,用于纯听力练习。
根据课程数据和学习者的请求自行制定计划,然后呈现计划并等待学习者的批准后再生成任何内容(步骤5)。除此之外,不要在运行过程中提问,除非是首次运行设置或下载大文件之前。
位置
-
<skill>:此技能的文件夹(包含本文件的文件夹),内有scripts/、references/和assets/。 -
配置:
~/.config/ci-story/config.json包含workspace、tts_project(仅用于本地 qwen 语音)和可选的env。如果该文件缺失,或其中的路径不存在,请先按照references/setup.md操作。 -
课程:
<workspace>/courses/<lang>/,包含:course.json:语音、字体notes.md:学习者覆盖项art/drawn/、art/generated/audio-cache/lessons/NNN-slug/
当学习者没有指定课程且只有一个课程存在时,使用该课程。
-
永远不要在 tts 项目中运行
uv lock --upgrade,也永远不要更改其中的 torch 路径源。一个多 GB 的 torch wheel 可能被固定为本地文件。
工作流程:“一个新故事”
-
阅读课程。 运行
python3 <skill>/scripts/curriculum.py report <course>。阅读notes.md,以及最近的一两个lesson.json文件和它们的story.py,以了解可以复用的角色、道具和辅助工具。 -
记录学习者说的话。
- 一个难词或已遗忘的词:添加一行
struggling: 给。 - 一个他们已经很熟悉的词:添加
solid: 苹果。 - 他们想多接触的词:添加
focus: …。
标签行只包含单词,用逗号分隔。后面的行会覆盖前面针对同一单词的行,括号
(…)内的备注会被忽略。 - 一个难词或已遗忘的词:添加一行
-
像老师一样规划:
- 首先满足请求:话题、重点词汇或复习目标。
- 选择 2-3 个新词。每个词必须具体且可展示(一个物体、一个可见的动作、一个数字、一种颜色,或一个对比,如 有/没有 或 大/小),在日常口语中常见,并能与已知单词组合成简单句子。课程的第一课总共只使用 5-7 个词(例如 这 是 一个 加上 2-3 个名词),每个词被听到 10 次以上。由于第一课的每个词都是新的,请将全部列在
new_words中。 - 将每个到期的词穿插至少 3 次。更频繁地使用脆弱和
struggling的词。除非故事需要,否则不出现solid的词。 - 每个新词被听到至少 10 次,每次图片都展示其含义。
- 故事结构:引入(这是…),对比(有/没有),一个动作或变化,然后是一个总结所有内容的回顾。保持现有的角色(
man,woman),除非故事需要新角色。 - 长度由内容决定。 默认情况下,使用尽可能多的句子以确保每个新词有 10 次以上的出现机会并覆盖到期词汇。通常是 30-50 个 2-6 个词的句子,大约 2-4 分钟(大约每句 4 秒)。如果学习者要求更长的故事(“大约 6 分钟”、“一个长故事”),保持相同的 2-3 个新词并添加场景:复述故事,将相同的词放在新的情境中,并重新引入较老的词。永远不要为了填补时间而增加新词。
- 更喜欢完全重复一个句子而非创造变体。完全重复听起来相同(它们来自缓存)并有助于听力。
-
草稿
<course>/lessons/NNN-slug/lesson.json。 NNN 来自报告。此草稿是计划的唯一真实来源,你将不断修改直到学习者批准。将每个节拍放在一行,以便节拍编号易于查看:{"title": "…", "art": "drawn", "new_words": ["…"], "beats": [ {"words": [["这","zhè"],["是","shì"],["一个","yí ge"],["人","rén"]], "show": "一个单独的人", "target": ["man"], "caption": true} ]}words包含带音调标记罗马拼音的词单元,使用实际的语流音变(一个 = yí ge)。words内部没有标点符号。每个节拍被读作以句号结束的陈述句,所以不要计划问句。show是描述图片的散文,供你和美术使用。target列出在节拍说话时发光的精灵 ID。caption在最后约 15% 的节拍中为 false。pause可选:节拍后的静默时间,默认 1.5 秒,最小 1.2 秒。如果节拍后有动作(如交接),可延长(3-3.5 秒),使动作发生在静默中。art当你使用 Claude 时为drawn,当你使用 Codex 时为generated。
然后运行
python3 <skill>/scripts/curriculum.py check <lesson.json>。修复每个 ERROR 和 WARNING。它还会打印一个带编号的节拍列表;为步骤 8 保留它。 -
呈现计划并等待批准。 在任何美术、旁白或渲染之前,向学习者展示一个基于草稿和
check输出的简短计划:- 故事: 用简单的英语描述发生了什么,一个场景接一个场景,2 到 4 句话。
- 新词: 每个词附带罗马拼音、含义以及图片如何展示它(例如“香蕉 xiāngjiāo, banana:女人拿着一个,然后递过去”),以及被听到的次数。
- 复习词: 包括每个词在计划中的原因(到期、脆弱、struggling 或 focus),以及被听到的次数。提及任何被省略的到期词及其原因。
- 长度: 句子数量以及视频大概运行时长(大约每句 4 秒),加上美术集和语音。
- 任何需要新绘制或生成的内容。
然后,礼貌地用简短的一行询问他们是否想要修改,或是否满意让你生成。停下来等待他们的回复。 将任何反馈(不同话题、其他词、更长或更短、不同的转折)视为修订:根据需要更新
lesson.json和notes.md,重新运行check,并以相同方式呈现修订后的计划。只有在学习者明确批准(“ok”、“go”、“generate it”、“looks good”)后才开始生成。如果他们事先明确表示不想审查计划(“just make it”、“surprise me”),则跳过此步骤。如果他们放弃了这个想法,删除草稿课程文件夹。 -
美术。 重用
<course>/art/<set>/中的精灵。对于任何缺失的内容,严格按照references/art.md操作,然后只添加新文件:- Claude: 手动编写 SVG 到
art/drawn/<name>.svg,以课程现有文件(或新课程的references/examples/)为蓝本。一个新角色需要所有三种表情。在使用前预览它:uv run --no-project --with 'cairosvg>=2.7,<3' python -c "import cairosvg,sys; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2], output_width=512)" <svg> /tmp/preview.png - Codex: 使用图像生成器。将透明 PNG 保存到
art/generated/,并将提示追加到art/generated/prompts.md。
- Claude: 手动编写 SVG 到
-
旁白。 使用与
course.json中voice.engine匹配的命令:qwen (本地 GPU):<config中的 env> uv run --project <tts_project> --no-sync python <skill>/scripts/audio.py <lesson 目录> edge (在线): uv run <skill>/scripts/audio.py <lesson 目录>在后台运行它。缓存的句子会重用,无需 GPU 或网络。新的句子在 GPU 上用 qwen 大约需要 30 秒(3次传递加对齐),或用 edge 大约 1 秒。如果运行中断,重新运行:已完成的句子会被缓存。这会写入
timing.json(实际节拍和单词时间)和narration.wav。如果缓存的剪辑听起来不对(误读的词、丢失的音节),删除它的两个文件并重新运行;用grep -l '<sentence>' <course>/audio-cache/*.json查找它们。 -
编排:编写
<lesson 目录>/story.py。 它定义了scene(t, timing)并返回(sprites, marks)。合约在scripts/engine.py的文档字符串中。- 所有运动都同步到节拍时间(
timing["beats"][i]["start"]和["end"]),绝不用绝对秒数。 - 手部位置来自
e.anchor/e.on_hand配合e.HAND_L/e.HAND_R。在镜像的右侧角色上,HAND_R是朝向中心的手。 e.ease提供平滑运动。对于环和任何与“正在说的句子”绑定的东西,使用index, _ = e.active_beat(t, timing)。- 首先查看课程早期的
story.py文件,以及references/examples/story-002-banana.py:一个包含track(带弧线的关键帧)、eat(到嘴边,然后缩小)、一个MOUTH点、交接和环的完整 44 节拍编排。复制合适的内容。当一个辅助函数被复制到第二个课时,在单独的步骤中将其移到engine.py中。
一个骨架:
import engine as e def scene(t, timing): s = [beat["start"] for beat in timing["beats"]] man = {"id": "man", "sprite": "man-0", "x": e.LEFT_X, "y": e.CHARACTER_Y, "h": e.CHARACTER_H} woman = {"id": "woman", "sprite": "woman-0", "x": e.RIGHT_X, "y": e.CHARACTER_Y, "h": e.CHARACTER_H, "flip": True, "opacity": e.ease((t - s[2] + 0.8) / 0.8)} ax, ay = e.on_hand(man, e.HAND_R, 84) return [man, woman, {"id": "apple", "sprite": "apple", "x": ax, "y": ay, "h": 84}], []约定:
- 在角色空手时使用表情
-1(失望)。 - 当提到 没有 时,在空手上绘制一个环(
marks)。 - 在命名角色或物体的节拍之前,将其引入。
- 规划物体的运动路径,确保它们永远不会经过角色的脸部。
- 所有运动都同步到节拍时间(
-
渲染:
uv run <skill>/scripts/render.py <lesson 目录>大约需要 1-2 分钟。修复它打印的任何WARNING beat N: target not on screen错误。 -
报告前验证。
- 打开每个
contact-sheet-N.jpg:每个节拍一个瓷砖,句子中间。检查发光的目标是否与句子匹配,物体是否在手中,环是否在空手上,表情是否匹配,并且没有内容与字幕条重叠或超出框架。 - 动作发生在暂停期间,所以检查一些交接或到达:
ffmpeg -v error -ss <秒数> -i lesson.mp4 -frames:v 1 /tmp/frame.png。节拍时间在timing.json中。 - 修复
story.py(或 lesson.json 中的target/caption;两者都在渲染时读取)并重新渲染。如果你更改了words,先重新运行步骤 7。 - 然后告诉学习者课程路径、新词和复习的词。
其他请求
-
“复习……” / “我忘了……”: 制作一个没有新词或只有一个新词的课程,围绕指定的词或该课程中的词构建。如果学习者表示他们正在忘记,将这些词标记为
struggling。 -
词汇或进度: 运行报告并总结。
-
语音: 课程的语音在
course.json中:可以是qwen(本地,例如带有慢速教师指令的Serena)或edge(在线,例如速率-25%的zh-CN-XiaoxiaoNeural)。- Qwen 说话人:
<env> uv run --project <tts_project> --no-sync python -c "from qwen_tts import Qwen3TTSModel as M; print(M.from_pretrained('Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice').get_supported_speakers())"。这会加载模型。 - edge-tts 语音:
uvx edge-tts --list-voices | grep zh-。
切换引擎或语音是对
voice块的修改(参见references/setup.md,步骤3)。它会启动一组全新的缓存剪辑。 - Qwen 说话人:
规则
- 一次运行只写入其课程文件夹、课程的
audio-cache/和notes.md,以及<course>/art/<set>/下的新文件。 - 对
<skill>/scripts、现有美术或 tts 项目的更改是单独的步骤,在学习者要求时进行。 - 没有
story.py的导入课程永远不会被重新渲染。