¿Qué hace ci-story?
Cada lección es un video corto. Un pequeño elenco actúa una historia simple mientras una voz tranquila narra frases cortas, y cada frase se repite muchas veces con la imagen haciendo obvio su significado. Los subtítulos en la escritura del idioma más la romanización resaltan la palabra que se está hablando. El último ~15% se reproduce sin subtítulos, para escucha pura.
Elabora el plan tú mismo a partir de los datos del curso y la solicitud del aprendiz, luego preséntalo y espera la aprobación antes de generar cualquier cosa (paso 5). Aparte de eso, no hagas preguntas durante la ejecución, excepto durante la configuración inicial y antes de descargas grandes.
Ubicaciones
-
<skill>: la carpeta de esta habilidad (la que contiene este archivo), conscripts/,references/yassets/. -
Configuración:
~/.config/ci-story/config.jsoncontieneworkspace,tts_project(solo para una voz qwen local) yenvopcional. Si falta, o una ruta en ella no existe, siguereferences/setup.mdprimero. -
Un curso:
<workspace>/courses/<lang>/, que contiene:course.json: voz, fuentesnotes.md: anulaciones del aprendizart/drawn/,art/generated/audio-cache/lessons/NNN-slug/
Cuando el aprendiz no nombre un curso y solo exista uno, usa ese.
-
Nunca ejecutes
uv lock --upgradeen el proyecto tts, y nunca cambies una fuente de ruta de torch allí. Puede haber un wheel de torch multigigabyte fijado como archivo local.
Flujo de trabajo para "una historia nueva"
-
Lee el curso. Ejecuta
python3 <skill>/scripts/curriculum.py report <course>. Leenotes.mdy los últimos uno o dos archivoslesson.jsonjunto con sustory.py, para la continuidad del elenco, accesorios y ayudantes que puedas reutilizar. -
Registra lo que dijo el aprendiz.
- Una palabra que es difícil o se ha olvidado: agrega una línea
struggling: 给. - Una palabra que conocen bien: agrega
solid: 苹果. - Palabras de las que quieren más: agrega
focus: ….
Las líneas de etiqueta contienen solo palabras, separadas por comas. Una línea posterior reemplaza a una anterior para la misma palabra, y los comentarios entre (paréntesis) se ignoran.
- Una palabra que es difícil o se ha olvidado: agrega una línea
-
Planifica, como un profesor:
- Honra primero la solicitud: su tema, palabras de enfoque o objetivo de repaso.
- Elige 2–3 palabras nuevas. Cada una debe ser concreta y representable (un objeto, una acción visible, un número, un color o un contraste como tiene/no tiene o grande/pequeño), común en el habla cotidiana y capaz de combinarse con palabras conocidas en oraciones simples. La primera lección de un curso usa solo 5–7 palabras en total (por ejemplo 这 是 一个 más 2–3 sustantivos), cada una escuchada 10+ veces. Dado que cada palabra en una primera lección es nueva, enuméralas todas en
new_words. - Entreteje cada palabra pendiente al menos 3 veces. Usa las palabras frágiles y
strugglingcon más frecuencia. Deja fuera las palabrassolida menos que la historia las necesite. - Cada palabra nueva se escucha al menos 10 veces, cada vez con la imagen mostrando lo que significa.
- Forma de la historia: introducir (这是…), contrastar (有 / 没有), una acción o cambio, luego un resumen que lo combine todo. Mantén el elenco existente (
man,woman) a menos que la historia necesite a alguien nuevo. - La longitud se deriva del contenido. Por defecto, usa tantas oraciones como sea necesario para dar a cada palabra nueva 10+ repeticiones y cubrir las palabras pendientes. Normalmente son 30–50 oraciones de 2–6 palabras, aproximadamente 2–4 minutos (unos 4 s por oración). Si el aprendiz pide una historia más larga ("unos 6 minutos", "una larga"), mantén las mismas 2–3 palabras nuevas y agrega escenas: vuelve a contar la historia, pon las mismas palabras en una situación nueva y trae palabras anteriores. Nunca agregues más palabras nuevas para llenar el tiempo.
- Prefiere repetir una oración exactamente a inventar una variante. Las repeticiones exactas suenan idénticas (vienen del caché) y ayudan a la comprensión auditiva.
-
Redacta
<course>/lessons/NNN-slug/lesson.json. NNN proviene del informe. Este borrador es la fuente de verdad del plan, y lo revisarás hasta que el aprendiz lo apruebe. Pon cada beat en una línea, para que los números de beat sean fáciles de ver:{"title": "…", "art": "drawn", "new_words": ["…"], "beats": [ {"words": [["这","zhè"],["是","shì"],["一个","yí ge"],["人","rén"]], "show": "man alone", "target": ["man"], "caption": true} ]}wordscontiene las unidades de palabra con romanización marcada de tono, usando sandhi tal como se habla (一个 = yí ge). Sin puntuación dentro dewords. Cada beat se lee como una declaración que termina en 。, así que no planees preguntas.showes texto en prosa que describe la imagen, para ti y para el arte.targetenumera los ids de sprites que brillan mientras se habla el beat.captiones false para aproximadamente el último 15% de los beats.pausees opcional: el silencio después del beat, por defecto 1.5 s, mínimo 1.2. Hazlo más largo (3–3.5 s) en un beat seguido de movimiento, como una entrega, para que el movimiento ocurra en el silencio.artesdrawncuando eres Claude ygeneratedcuando eres Codex.
Luego ejecuta
python3 <skill>/scripts/curriculum.py check <lesson.json>. Corrige cada ERROR y cada ADVERTENCIA. También imprime una lista numerada de beats; guárdala para el paso 8. -
Presenta el plan y espera la aprobación. Antes de cualquier arte, narración o renderizado, muestra al aprendiz un plan breve construido a partir del borrador y la salida de
check:- Historia: dos a cuatro oraciones sobre lo que sucede, escena por escena, en inglés sencillo.
- Palabras nuevas, cada una con su romanización, significado y cómo la imagen la muestra (por ejemplo "香蕉 xiāngjiāo, banana: la mujer sostiene una, luego la entrega"), y cuántas veces se escucha.
- Palabras de repaso, con por qué está cada una (pendiente, frágil, struggling o focus) y cuántas veces se escucha. Menciona cualquier palabra pendiente que hayas dejado fuera y por qué.
- Duración: el número de oraciones y aproximadamente cuánto dura el video (unos 4 s por oración), más el conjunto de arte y la voz.
- Cualquier cosa nueva para dibujar o generar.
Luego pregunta, cortésmente y en una línea breve, si quieren cambios o están contentos de que generes. Detente y espera su respuesta. Trata cualquier comentario (un tema diferente, otras palabras, más largo o más corto, un giro diferente) como una revisión: actualiza
lesson.jsonynotes.mdsegún sea necesario, vuelve a ejecutarcheck, y presenta el plan revisado de la misma manera. Genera solo una vez que el aprendiz apruebe claramente ("ok", "adelante", "genéralo", "se ve bien"). Si dijeron de antemano que no quieren revisar el plan ("solo hazlo", "sorpréndeme"), omite este paso. Si abandonan la idea, elimina la carpeta del borrador de la lección. -
Arte. Reutiliza los sprites en
<course>/art/<set>/. Para cualquier cosa que falte, siguereferences/art.mdexactamente, luego agrega solo archivos nuevos:- Claude: escribe SVG a mano en
art/drawn/<name>.svg, modelado a partir de los archivos existentes del curso (oreferences/examples/en un curso nuevo). Un nuevo personaje necesita las tres expresiones. Míralo antes de usarlo: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: usa el generador de imágenes. Guarda PNG transparentes en
art/generated/y agrega el prompt aart/generated/prompts.md.
- Claude: escribe SVG a mano en
-
Narración. Usa el comando que coincida con
voice.engineencourse.json:qwen (GPU local): <env from config> uv run --project <tts_project> --no-sync python <skill>/scripts/audio.py <lesson dir> edge (en línea): uv run <skill>/scripts/audio.py <lesson dir>Ejecútalo en segundo plano. Las oraciones en caché se reutilizan sin GPU ni red. Las nuevas toman unos 30 s cada una con qwen en una GPU (3 tomas más alineación), o aproximadamente un segundo cada una con edge. Si la ejecución se interrumpe, vuélvela a ejecutar: las oraciones terminadas están en caché. Esto escribe
timing.json(tiempos reales de beat y palabra) ynarration.wav. Si un clip en caché suena mal (una palabra leída incorrectamente, una sílaba omitida), elimina sus dos archivos y vuelve a ejecutar;grep -l '<oracion>' <course>/audio-cache/*.jsonlos encuentra. -
Coreografía: escribe
<lesson dir>/story.py. Definescene(t, timing)y devuelve(sprites, marks). El contrato está en el docstring descripts/engine.py.- Clave todo el movimiento a los tiempos de beat (
timing["beats"][i]["start"]y["end"]), nunca a segundos literales. - Las posiciones de las manos provienen de
e.anchor/e.on_handcone.HAND_L/e.HAND_R. En el personaje reflejado a la derecha,HAND_Res la mano hacia el centro. e.easeproporciona movimiento suave. Para anillos y cualquier cosa vinculada a "la oración que se está hablando", usaindex, _ = e.active_beat(t, timing).- Mira primero los archivos
story.pyanteriores del curso yreferences/examples/story-002-banana.py: una coreografía completa de 44 beats contrack(fotogramas clave con arcos),eat(a la boca, luego encoger), un puntoMOUTH, entregas y anillos. Copia lo que se ajuste. Cuando un ayudante se ha copiado en una segunda lección, muévelo aengine.pyen un paso separado.
Un 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}], []Convenciones:
- Usa la expresión
-1(decepcionado) mientras un personaje está con las manos vacías. - Dibuja un anillo (
marks) sobre una mano vacía mientras se dice 没有 sobre ella. - Trae un personaje u objeto justo antes del beat que lo nombra.
- Enruta los objetos para que nunca pasen sobre una cara.
- Clave todo el movimiento a los tiempos de beat (
-
Renderizado:
uv run <skill>/scripts/render.py <lesson dir>toma aproximadamente 1–2 minutos. Corrige cualquierADVERTENCIA beat N: target not on screenque imprima. -
Verifica antes de informar.
- Abre cada
contact-sheet-N.jpg: un mosaico por beat, a mitad de la oración. Verifica que el objetivo brillante coincida con la oración, los objetos estén en las manos, los anillos estén en las manos vacías, las caras coincidan y nada se superponga a la tira de subtítulos o salga del cuadro. - El movimiento ocurre en las pausas, así que verifica algunas entregas o llegadas:
ffmpeg -v error -ss <segundos> -i lesson.mp4 -frames:v 1 /tmp/frame.png. Los tiempos de beat están entiming.json. - Corrige
story.py(otarget/captionen lesson.json; ambos se leen en el momento del renderizado) y vuelve a renderizar. Si cambiaswords, vuelve a ejecutar el paso 7 primero. - Luego dile al aprendiz la ruta de la lección, las palabras nuevas y las palabras repasadas.
Otras solicitudes
-
"Repasar …" / "Olvidé …": haz una lección sin palabras nuevas, o con una, construida alrededor de las palabras nombradas o las palabras de esa lección. Márcalas como
strugglingsi el aprendiz dice que se le escapan. -
Vocabulario o progreso: ejecuta el informe y resúmelo.
-
Voces: la voz del curso está en
course.json: ya seaqwen(local, por ejemploSerenacon una instrucción de profesor lento) oedge(en línea, por ejemplozh-CN-XiaoxiaoNeurala velocidad-25%).- Locutores de 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())". Esto carga el modelo. - Voces de edge-tts:
uvx edge-tts --list-voices | grep zh-.
Cambiar el motor o la voz es una edición al bloque
voice(verreferences/setup.md, paso 3). Inicia un conjunto nuevo de clips en caché. - Locutores de Qwen:
Reglas
- Una ejecución escribe solo dentro de su carpeta de lección, el
audio-cache/del curso ynotes.md, más archivos nuevos bajo<course>/art/<set>/. - Los cambios en
<skill>/scripts, el arte existente o el proyecto tts son un paso separado, que se realiza cuando el aprendiz lo solicita. - Las lecciones importadas sin un
story.pynunca se vuelven a renderizar.