feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes

This commit is contained in:
renato97
2026-07-21 15:31:46 -03:00
commit fa8e0c58ce
120 changed files with 16013 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
import type { Stage, Exercise } from '../types'
type Info = {
letra: string
palabra: string
emoji: string
gesto: string
silaba: string
palabraLower: string
}
const moduleA: Info[] = [
{ letra: 'A', palabra: 'ÁRBOL', emoji: '🌳', gesto: 'Abrí la mano como una boca grande', silaba: 'a', palabraLower: 'árbol' },
{ letra: 'E', palabra: 'ELEFANTE', emoji: '🐘', gesto: 'Estirá la mano como una trompa', silaba: 'e', palabraLower: 'elefante' },
{ letra: 'I', palabra: 'INDIO', emoji: '🪶', gesto: 'Señalá el cielo con el dedo', silaba: 'i', palabraLower: 'indio' },
{ letra: 'O', palabra: 'OJO', emoji: '👁️', gesto: 'Redondeá la mano como una O', silaba: 'o', palabraLower: 'ojo' },
{ letra: 'U', palabra: 'UNO', emoji: '1️⃣', gesto: 'Señalá el suelo con la mano', silaba: 'u', palabraLower: 'uno' },
]
const moduleB: Info[] = [
{ letra: 'M', palabra: 'MAMÁ', emoji: '👩', gesto: 'Manos en la boca como mmm', silaba: 'ma', palabraLower: 'mamá' },
{ letra: 'P', palabra: 'PAPÁ', emoji: '👨', gesto: 'Manos que abren y cierran como pompa', silaba: 'pa', palabraLower: 'papá' },
{ letra: 'L', palabra: 'LUNA', emoji: '🌙', gesto: 'Lengua arriba como lámpara', silaba: 'la', palabraLower: 'luna' },
{ letra: 'S', palabra: 'SOL', emoji: '☀️', gesto: 'Dedos como serpiente sss', silaba: 'sa', palabraLower: 'sol' },
]
const moduleC: Info[] = [
{ letra: 'T', palabra: 'TASA', emoji: '☕', gesto: 'Golpecito con la mano', silaba: 'ta', palabraLower: 'tasa' },
{ letra: 'N', palabra: 'NIDO', emoji: '🪺', gesto: 'Nariz como nido', silaba: 'na', palabraLower: 'nido' },
{ letra: 'D', palabra: 'DEDO', emoji: '👉', gesto: 'Dedo señalando', silaba: 'da', palabraLower: 'dedo' },
{ letra: 'R', palabra: 'RATÓN', emoji: '🐭', gesto: 'Lengua que vibra como rrr', silaba: 'ra', palabraLower: 'ratón' },
]
const allModules = [moduleA, moduleB, moduleC]
function getDistractors(pool: Info[], current: Info, count: number): Info[] {
return pool.filter((x) => x.letra !== current.letra).slice(0, count)
}
function getPoolIndex(info: Info): number {
for (let i = 0; i < allModules.length; i++) {
if (allModules[i].find((x) => x.letra === info.letra)) return i
}
return 0
}
function buildExercises(): Exercise[] {
const all: Exercise[] = []
let prevId: string | null = 'lec4-r-completa'
for (const pool of allModules) {
for (const info of pool) {
const MIN = info.letra.toLowerCase()
const dist = getDistractors(pool, info, 3)
// 1. Descubrimiento del par
all.push({
id: `lec5-${MIN}-descubre`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'sensibilizacion',
prerequisites: prevId ? [prevId] : [],
content: {
instruction: `${info.letra} y ${MIN} suenan igual`,
audioText: `${info.letra} grande y ${MIN} chica suenan igual: ${gestoKey(info.gesto)}`,
emoji: info.emoji,
},
functionalContext: `Descubrir que ${info.letra} y ${MIN} son la misma letra.`,
})
prevId = `lec5-${MIN}-descubre`
// 2. Práctica emparejada: Tocá Mm
const pairOptions = [
{ label: `${info.letra}${MIN}`, correct: true, id: MIN },
...dist.map((d) => ({
label: `${d.letra}${d.letra.toLowerCase()}`,
correct: false,
id: d.letra.toLowerCase(),
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-pareja`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `Tocá la ${info.letra}${MIN}`,
audioText: `¿Dónde está ${info.letra}${MIN}?`,
options: pairOptions,
emoji: info.emoji,
},
functionalContext: `Asociar el par ${info.letra}${MIN}.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-pareja`
// 3. Buscar mayúscula entre minúsculas: Tocá la M
const mayusOptions = [
{ label: info.letra, correct: true, id: MIN },
...dist.map((d) => ({
label: d.letra.toLowerCase(),
correct: false,
id: `${d.letra.toLowerCase()}-may`,
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-busca-mayus`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `Tocá la ${info.letra} grande`,
audioText: `¿Dónde está la ${info.letra} grande?`,
options: mayusOptions,
emoji: info.emoji,
},
functionalContext: `Reconocer ${info.letra} mayúscula entre minúsculas.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-busca-mayus`
// 4. Buscar minúscula entre mayúsculas: Tocá la m (CLAVE)
const minusOptions = [
{ label: MIN, correct: true, id: MIN },
...dist.map((d) => ({
label: d.letra,
correct: false,
id: `${d.letra.toLowerCase()}-min`,
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-busca-minus`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `Tocá la ${MIN} chica`,
audioText: `¿Dónde está la ${MIN} chica?`,
options: minusOptions,
emoji: info.emoji,
},
functionalContext: `Reconocer ${MIN} minúscula entre mayúsculas — discriminación clave para dislexia.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-busca-minus`
// 5. Pareja inversa: ¿Cuál es la pareja de M? → m
const parejaMayusOptions = [
{ label: MIN, correct: true, id: `${MIN}-pm` },
...dist.map((d) => ({
label: d.letra.toLowerCase(),
correct: false,
id: `${d.letra.toLowerCase()}-pm`,
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-pareja-mayus`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `¿Cuál es la pareja de ${info.letra}?`,
audioText: `Buscá la ${MIN} chica, pareja de la ${info.letra} grande`,
options: parejaMayusOptions,
emoji: info.emoji,
},
functionalContext: `Asociar la mayúscula ${info.letra} con su minúscula ${MIN}.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-pareja-mayus`
// 6. Pareja inversa 2: ¿Cuál es la pareja de m? → M
const parejaMinusOptions = [
{ label: info.letra, correct: true, id: `${MIN}-pm2` },
...dist.map((d) => ({
label: d.letra,
correct: false,
id: `${d.letra.toLowerCase()}-pm2`,
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-pareja-minus`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `¿Cuál es la pareja de ${MIN}?`,
audioText: `Buscá la ${info.letra} grande, pareja de la ${MIN} chica`,
options: parejaMinusOptions,
emoji: info.emoji,
},
functionalContext: `Asociar la minúscula ${MIN} con su mayúscula ${info.letra}.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-pareja-minus`
// 7. Sílabas en minúscula: Tocá ma
const silabaOptions = [
{ label: info.silaba, correct: true, id: `${MIN}-sil` },
...dist.map((d) => ({
label: d.silaba,
correct: false,
id: `${d.letra.toLowerCase()}-sil`,
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-silaba`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `Tocá ${info.silaba}`,
audioText: `¿Dónde está ${info.silaba}?`,
options: silabaOptions,
emoji: info.emoji,
},
functionalContext: `Leer la sílaba ${info.silaba} en minúscula.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-silaba`
// 8. Palabra en minúscula: Tocá mamá
const palabraOptions = [
{ label: info.palabraLower, correct: true, id: `${MIN}-pal` },
...dist.map((d) => ({
label: d.palabraLower,
correct: false,
id: `${d.letra.toLowerCase()}-pal`,
})),
].sort(() => Math.random() - 0.5)
all.push({
id: `lec5-${MIN}-palabra`,
stage: 5,
skillCode: `puente-${MIN}`,
type: 'eleccion',
prerequisites: [prevId],
content: {
instruction: `Tocá ${info.palabraLower}`,
audioText: `¿Dónde dice ${info.palabraLower}?`,
options: palabraOptions,
emoji: info.emoji,
},
functionalContext: `Leer la palabra ${info.palabraLower} en minúscula.`,
errorType: 'confusion-letra',
})
prevId = `lec5-${MIN}-palabra`
}
}
return all
}
function gestoKey(gesto: string): string {
const map: Record<string, string> = {
'Abrí': 'a-a-a',
'Estirá': 'e-e-e',
'Señalá el cielo': 'i-i-i',
'Redondeá': 'o-o-o',
'Señalá el suelo': 'u-u-u',
'Manos en la boca': 'mmm',
'Manos que abren': 'p-p-p',
'Dedos como serpiente': 'sss',
'Lengua arriba': 'l-l-l',
'Golpecito': 't-t-t',
'Nariz como nido': 'n-n-n',
'Dedo señalando': 'd-d-d',
'Lengua que vibra': 'rrr',
}
const key = Object.keys(map).find((k) => gesto.startsWith(k))
return key ? map[key] : gesto
}
const stage: Stage = {
id: 5,
name: 'Puente Mayúscula-Minúscula',
description: 'Asociar letras mayúsculas con sus correspondientes minúsculas. 8 ejercicios por letra — 104 en total.',
exercises: buildExercises(),
}
export default stage