O que ci-story faz?
Cada lição é um vídeo curto. Um pequeno elenco representa uma história simples enquanto uma voz calma narra frases curtas, e cada frase se repete muitas vezes com a imagem tornando seu significado óbvio. Legendas na escrita do idioma mais romanização destacam a palavra que está sendo falada. Os últimos ~15% rodam sem legendas, para audição pura.
Elabore o plano você mesmo a partir dos dados do curso e do pedido do aprendiz, depois apresente-o e aguarde o aval do aprendiz antes de gerar qualquer coisa (etapa 5). Fora isso, não faça perguntas durante a execução, exceto durante a configuração inicial e antes de grandes downloads.
Locais
-
<skill>: a pasta desta skill (a que contém este arquivo), comscripts/,references/eassets/. -
Config:
~/.config/ci-story/config.jsoncontémworkspace,tts_project(apenas para uma voz qwen local) eenvopcional. Se estiver faltando, ou se um caminho nele não existir, sigareferences/setup.mdprimeiro. -
Um curso:
<workspace>/courses/<lang>/, contendo:course.json: voz, fontesnotes.md: substituições do aprendizart/drawn/,art/generated/audio-cache/lessons/NNN-slug/
Quando o aprendiz não nomeia um curso e apenas um existe, use esse.
-
Nunca execute
uv lock --upgradeno projeto tts, e nunca altere uma origem de caminho torch lá. Um wheel torch de vários GB pode estar fixado como um arquivo local.
Fluxo de trabalho para "uma nova história"
-
Leia o curso. Execute
python3 <skill>/scripts/curriculum.py report <course>. Leianotes.md, e o último ou dois arquivoslesson.jsonmais seusstory.py, para continuidade de elenco, adereços e ajudantes que você pode reutilizar. -
Registre o que o aprendiz disse.
- Uma palavra que é difícil ou esquecida: adicione uma linha
struggling: 给. - Uma palavra que eles conhecem bem: adicione
solid: 苹果. - Palavras que eles querem mais: adicione
focus: ….
As linhas de marcação contêm apenas palavras, separadas por vírgula. Uma linha posterior substitui uma anterior para a mesma palavra, e observações entre (parênteses) são ignoradas.
- Uma palavra que é difícil ou esquecida: adicione uma linha
-
Planeje, como um professor:
- Atenda ao pedido primeiro: seu tópico, palavras focadas ou alvo de revisão.
- Escolha 2–3 palavras novas. Cada uma deve ser concreta e mostrável (um objeto, uma ação visível, um número, uma cor ou um contraste como tem/não tem ou grande/pequeno), comum na fala cotidiana e capaz de se combinar com palavras conhecidas em frases simples. A primeira lição de um curso usa apenas 5–7 palavras no total (por exemplo 这 是 一个 mais 2–3 substantivos), cada uma ouvida 10+ vezes. Como toda palavra em uma primeira lição é nova, liste todas em
new_words. - Incorpore cada palavra devida pelo menos 3 vezes. Use palavras frágeis e
strugglingcom mais frequência. Deixe palavrassolidde fora, a menos que a história precise delas. - Cada palavra nova é ouvida pelo menos 10 vezes, cada vez com a imagem mostrando o que significa.
- Forma da história: introduza (这是…), contraste (有 / 没有), uma ação ou mudança, depois um resumo que combina tudo. Mantenha o elenco existente (
man,woman) a menos que a história precise de alguém novo. - O comprimento segue do conteúdo. Por padrão, use quantas frases forem necessárias para dar a cada palavra nova 10+ audições e cobrir as palavras devidas. Isso geralmente é 30–50 frases de 2–6 palavras, aproximadamente 2–4 minutos (cerca de 4 s por frase). Se o aprendiz pedir uma história mais longa ("cerca de 6 minutos", "uma longa"), mantenha as mesmas 2–3 palavras novas e adicione cenas: reconte a história, coloque as mesmas palavras em uma nova situação e traga palavras mais antigas de volta. Nunca adicione mais palavras novas para preencher o tempo.
- Prefira repetir uma frase exatamente a inventar uma variante. Repetições exatas soam idênticas (vêm do cache) e ajudam na audição.
-
Elabore
<course>/lessons/NNN-slug/lesson.json. NNN vem do relatório. Este rascunho é a fonte da verdade do plano, e você o revisará até o aprendiz aprovar. Coloque cada batida em uma linha, para que os números das batidas sejam fáceis de ver:{"title": "…", "art": "drawn", "new_words": ["…"], "beats": [ {"words": [["这","zhè"],["是","shì"],["一个","yí ge"],["人","rén"]], "show": "man alone", "target": ["man"], "caption": true} ]}wordscontém as unidades de palavras com romanização com marcação de tom, usando sandhi como falado (一个 = yí ge). Sem pontuação dentro dewords. Cada batida é lida como uma declaração terminada em ., então não planeje perguntas.showé uma prosa descrevendo a imagem, para você e para a arte.targetlista os ids de sprites que brilham enquanto a batida é falada.captioné falso para aproximadamente os últimos 15% das batidas.pauseé opcional: o silêncio após a batida, padrão 1,5 s, mínimo 1,2. Torne-o mais longo (3–3,5 s) em uma batida seguida por movimento, como uma entrega, para que o movimento ocorra no silêncio.artédrawnquando você é Claude egeneratedquando você é Codex.
Em seguida, execute
python3 <skill>/scripts/curriculum.py check <lesson.json>. Corrija cada ERROR e cada WARNING. Ele também imprime uma lista numerada de batidas; mantenha-a para a etapa 8. -
Apresente o plano e aguarde o aval. Antes de qualquer arte, narração ou renderização, mostre ao aprendiz um plano curto construído a partir do rascunho e da saída de
check:- História: duas a quatro frases sobre o que acontece, cena por cena, em português claro.
- Palavras novas, cada uma com sua romanização, significado e como a imagem a mostra (por exemplo "香蕉 xiāngjiāo, banana: a mulher segura uma, depois a entrega"), e quantas vezes é ouvida.
- Palavras de revisão, com o motivo de cada uma estar incluída (devida, frágil, struggling ou focus) e quantas vezes é ouvida. Mencione qualquer palavra devida que você deixou de fora, e por quê.
- Duração: o número de frases e aproximadamente quanto tempo o vídeo dura (cerca de 4 s por frase), além do conjunto de arte e da voz.
- Qualquer coisa nova para desenhar ou gerar.
Em seguida, pergunte, educadamente e em uma linha curta, se eles querem mudanças ou se estão felizes para você gerar. Pare e aguarde a resposta deles. Trate qualquer feedback (um tópico diferente, outras palavras, mais longo ou mais curto, uma reviravolta diferente) como uma revisão: atualize
lesson.jsonenotes.mdconforme necessário, executechecknovamente e apresente o plano revisado da mesma forma. Gere apenas quando o aprendiz aprovar claramente ("ok", "vai", "gera", "parece bom"). Se eles disseram de antemão que não querem revisar o plano ("apenas faça", "surpreenda-me"), pule esta etapa. Se eles desistirem da ideia, exclua a pasta de lição do rascunho. -
Arte. Reutilize os sprites em
<course>/art/<set>/. Para qualquer coisa faltando, sigareferences/art.mdexatamente, depois adicione apenas novos arquivos:- Claude: escreva SVG manualmente em
art/drawn/<nome>.svg, modelado nos arquivos existentes do curso (oureferences/examples/em um novo curso). Um novo personagem precisa de todas as três expressões. Veja-o antes de usá-lo: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: use o gerador de imagens. Salve PNGs transparentes em
art/generated/e anexe o prompt aart/generated/prompts.md.
- Claude: escreva SVG manualmente em
-
Narração. Use o comando que corresponde a
voice.engineemcourse.json:qwen (GPU local): <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>Execute-o em segundo plano. Frases em cache são reutilizadas sem GPU ou rede. Novas levam cerca de 30 s cada com qwen em uma GPU (3 takes mais alinhamento), ou cerca de um segundo cada com edge. Se a execução for interrompida, execute novamente: as frases finalizadas estão em cache. Isso escreve
timing.json(tempos reais de batida e palavra) enarration.wav. Se um clipe em cache soar errado (uma palavra mal lida, uma sílaba omitida), exclua seus dois arquivos e execute novamente;grep -l '<sentence>' <course>/audio-cache/*.jsonos encontra. -
Coreografia: escreva
<lesson dir>/story.py. Ele definescene(t, timing)e retorna(sprites, marks). O contrato está no docstring descripts/engine.py.- Key all movement to beat times (
timing["beats"][i]["start"]e["end"]), nunca a segundos literais. - Posições das mãos vêm de
e.anchor/e.on_handcome.HAND_L/e.HAND_R. No personagem espelhado à direita,HAND_Ré a mão em direção ao centro. e.easedá movimento suave. Para anéis e qualquer coisa ligada a "a frase sendo falada", useindex, _ = e.active_beat(t, timing).- Veja primeiro os arquivos
story.pyanteriores do curso ereferences/examples/story-002-banana.py: uma coreografia completa de 44 batidas comtrack(quadros-chave com arcos),eat(para a boca, depois encolher), um pontoMOUTH, entregas e anéis. Copie o que for adequado. Quando um ajudante foi copiado para uma segunda lição, mova-o paraengine.pyem uma etapa separada.
Um esqueleto:
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}], []Convenções:
- Use expressão
-1(decepcionado) enquanto um personagem está de mãos vazias. - Desenhe um anel (
marks) sobre uma mão vazia enquanto 没有 é dito sobre ela. - Traga um personagem ou objeto para dentro logo antes da batida que o nomeia.
- Roteie objetos para que nunca passem sobre um rosto.
- Key all movement to beat times (
-
Renderize:
uv run <skill>/scripts/render.py <lesson dir>leva cerca de 1–2 minutos. Corrija qualquerWARNING beat N: target not on screenque ele imprimir. -
Verifique antes de relatar.
- Abra cada
contact-sheet-N.jpg: um bloco por batida, no meio da frase. Verifique se o alvo brilhante corresponde à frase, os objetos estão nas mãos, os anéis estão nas mãos vazias, os rostos correspondem e nada sobrepõe a faixa de legenda ou sai do quadro. - O movimento ocorre nas pausas, então verifique algumas entregas ou chegadas:
ffmpeg -v error -ss <seconds> -i lesson.mp4 -frames:v 1 /tmp/frame.png. Os tempos das batidas estão emtiming.json. - Corrija
story.py(outarget/captionem lesson.json; ambos são lidos no momento da renderização) e renderize novamente. Se você alterarwords, execute novamente a etapa 7 primeiro. - Em seguida, informe ao aprendiz o caminho da lição, as novas palavras e as palavras revisadas.
Outros pedidos
-
"Revisar …" / "Eu esqueci …": faça uma lição sem palavras novas, ou com uma, construída em torno das palavras nomeadas ou das palavras dessa lição. Marque-as como
strugglingse o aprendiz disser que estão escapando. -
Vocabulário ou progresso: execute o relatório e resuma-o.
-
Vozes: a voz do curso está em
course.json: ouqwen(local, por exemploSerenacom uma instrução de professor lento) ouedge(online, por exemplozh-CN-XiaoxiaoNeuralna taxa-25%).- Locutores 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())". Isso carrega o modelo. - Vozes edge-tts:
uvx edge-tts --list-voices | grep zh-.
Trocar de mecanismo ou voz é uma edição no bloco
voice(vejareferences/setup.md, etapa 3). Isso inicia um novo conjunto de clipes em cache. - Locutores Qwen:
Regras
- Uma execução escreve apenas dentro de sua pasta de lição, no
audio-cache/do curso enotes.md, mais novos arquivos sob<course>/art/<set>/. - Alterações em
<skill>/scripts, arte existente ou no projeto tts são uma etapa separada, feita quando o aprendiz pede. - Lições importadas sem um
story.pynunca são renderizadas novamente.