Communitygithub.com

ci-story

为初学者生成可理解输入的故事视频(配有插画、旁白和大量重复),适用于语言课程(目前为普通话)。当学习者要求新故事、复习课、特定主题或词汇的故事,或表示某个词太难/已掌握时使用。

ci-story 是什么?

ci-story 是一个 AI 驱动的辅助工具,专为语言课程(目前是普通话)设计,通过生成简短、高度重复、配有插画和旁白的故事视频,帮助初学者实现可理解输入。技能的核心工作流程包括:分析课程数据和学习者的请求,制定包含2-3个新词和所需复习词的课程计划,然后生成插画、旁白(支持本地 qwen 或在线 edge-tts 引擎),并编排动画场景,最后渲染为视频。它强调使用具体、可视化的词汇,确保每个新词至少重复10次,并利用精确重复和图像辅助理解。该工具支持 Claude(绘制 SVG 插画)和 Codex(使用图像生成器)两种平台,并且会先生成计划供学习者审批(除非学习者明确跳过),然后才开始实际生成。

兼容平台✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/victorblaga/ci-story/tree/HEAD/skills/ci-story

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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 可能被固定为本地文件。

工作流程:“一个新故事”

  1. 阅读课程。 运行 python3 <skill>/scripts/curriculum.py report <course>。阅读 notes.md,以及最近的一两个 lesson.json 文件和它们的 story.py,以了解可以复用的角色、道具和辅助工具。

  2. 记录学习者说的话。

    • 一个难词或已遗忘的词:添加一行 struggling: 给。
    • 一个他们已经很熟悉的词:添加 solid: 苹果。
    • 他们想多接触的词:添加 focus: …。

    标签行只包含单词,用逗号分隔。后面的行会覆盖前面针对同一单词的行,括号 (…) 内的备注会被忽略。

  3. 像老师一样规划:

    • 首先满足请求:话题、重点词汇或复习目标。
    • 选择 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 个新词并添加场景:复述故事,将相同的词放在新的情境中,并重新引入较老的词。永远不要为了填补时间而增加新词。
    • 更喜欢完全重复一个句子而非创造变体。完全重复听起来相同(它们来自缓存)并有助于听力。
  4. 草稿 <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 保留它。

  5. 呈现计划并等待批准。 在任何美术、旁白或渲染之前,向学习者展示一个基于草稿和 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”),则跳过此步骤。如果他们放弃了这个想法,删除草稿课程文件夹。

  6. 美术。 重用 <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。
  7. 旁白。 使用与 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 查找它们。

  8. 编排:编写 <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)。
    • 在命名角色或物体的节拍之前,将其引入。
    • 规划物体的运动路径,确保它们永远不会经过角色的脸部。
  9. 渲染: uv run <skill>/scripts/render.py <lesson 目录> 大约需要 1-2 分钟。修复它打印的任何 WARNING beat N: target not on screen 错误。

  10. 报告前验证。

  • 打开每个 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)。它会启动一组全新的缓存剪辑。

规则

  • 一次运行只写入其课程文件夹、课程的 audio-cache/ 和 notes.md,以及 <course>/art/<set>/ 下的新文件。
  • 对 <skill>/scripts、现有美术或 tts 项目的更改是单独的步骤,在学习者要求时进行。
  • 没有 story.py 的导入课程永远不会被重新渲染。

相关技能