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 路徑來源。可能有一個大型的本地 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": "man alone", "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的輸出:- 故事: 用兩到四句英文描述場景,逐場說明。
- 新詞: 每個詞條含其拼音、意義以及圖片如何展示它(例如「香蕉 xiāngjiāo, 香蕉:女人拿著一個,然後遞出去」),以及它被聽到的次數。
- 複習詞彙: 說明每個詞為何包含在內(到期、脆弱、struggling 或 focus)以及它被聽到的次數。提及你省略的任何到期單詞及其原因。
- 長度: 句子數量以及影片大約的播放時間(每句約 4 秒),加上美術組和語音。
- 任何需要新繪製或生成的內容。
然後,用一句簡短的句子有禮地詢問他們是否想要修改還是滿意並讓你生成。停止並等待他們的回覆。 將任何回饋(不同的主題、其他詞彙、希望更長或更短、不同的轉折)都視為修訂:視需要更新
lesson.json和notes.md,重新執行check,並以相同方式呈現修訂後的計畫。只有在學習者明確表示同意(「好」、「開始」、「生成」、「看起來不錯」)後才開始生成。如果他們一開始就表示不想審查計畫(「直接做」、「給我驚喜」),則跳過此步驟。如果他們放棄這個想法,請刪除草稿課程資料夾。 -
美術。 重複使用
<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 (local GPU):<env from config> uv run --project <tts_project> --no-sync python <skill>/scripts/audio.py <lesson dir> edge (online): uv run <skill>/scripts/audio.py <lesson dir>在背景執行。快取的句子會重複使用,不會消耗 GPU 或網路。新的句子在 GPU 上使用 qwen 大約需要 30 秒(3 次處理 + 對齊),或使用 edge 大約一秒。如果執行中斷,請重新執行:已完成句子的快取會被保留。此操作會寫入
timing.json(真實的段落和詞的時間)和narration.wav。如果一個快取片段聽起來不對(讀錯詞、漏掉音節),請刪除它的兩個檔案並重新執行;grep -l '<sentence>' <course>/audio-cache/*.json可以找到它們。 -
編排:撰寫
<lesson dir>/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:這是一個完整的 44 段落編排,包含track(帶弧線的關鍵影格)、eat(送到嘴邊,然後縮小)、一個MOUTH點、物品傳遞和環圈。複製適合的部分。當一個輔助函式被複製到第二課時,請在單獨的步驟中將其移入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 dir>大約需要 1–2 分鐘。修正它印出的任何WARNING beat N: target not on screen。 -
在回報前驗證。
- 開啟每個
contact-sheet-N.jpg:每個段落一張圖格,來自句子中間。檢查發光目標是否與句子匹配、物體是否在手中、環圈是否在空手上、表情是否相符,以及沒有東西覆蓋字幕區域或超出畫面邊界。 - 運動發生在暫停中,因此檢查一些物品傳遞或進場:
ffmpeg -v error -ss <seconds> -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(線上,例如zh-CN-XiaoxiaoNeural,速率-25%)。- 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的匯入課程永遠不會被重新渲染。