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
+269
View File
@@ -0,0 +1,269 @@
import type { Stage } from '../types'
function vocalChoice(vocal: string, correctLabel: string, distractors: string[], emoji: string, instruction: string, audioText?: string) {
const options = [
{ label: correctLabel, correct: true, id: vocal },
...distractors.map((d) => ({ label: d, correct: false, id: d })),
].sort(() => Math.random() - 0.5)
return {
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'eleccion' as const,
prerequisites: getVocalPrereqs(vocal),
content: {
instruction,
text: correctLabel,
options,
audioText: audioText || `${correctLabel}`,
emoji,
color: getVocalColor(vocal),
},
functionalContext: `Identificar la vocal ${vocal.toUpperCase()} en diferentes contextos.`,
errorType: 'confusion-letra' as const,
}
}
function getVocalColor(vocal: string): string {
const colors: Record<string, string> = { a: '#FF6B6B', o: '#4ECDC4', i: '#45B7D1', e: '#96CEB4', u: '#DDA0DD' }
return colors[vocal] || '#FF6B6B'
}
function getVocalPrereqs(vocal: string): string[] {
const order = ['a', 'o', 'i', 'e', 'u']
const idx = order.indexOf(vocal)
if (idx === 0) return ['lec2-segmentar-pato']
return [`vocal-${order[idx - 1]}-completa`]
}
const gestos: Record<string, { gesto: string, palabra: string, fonema: string, emoji: string }> = {
a: { gesto: 'Abrí la mano como una boca grande', palabra: 'ÁRBOL', fonema: 'AAA', emoji: '🌳' },
o: { gesto: 'Redondeá la mano como una O', palabra: 'OJO', fonema: 'OOO', emoji: '👁️' },
i: { gesto: 'Señalá el cielo con el dedo', palabra: 'INDIO', fonema: 'III', emoji: '🪶' },
e: { gesto: 'Estirá la mano como una trompa de elefante', palabra: 'ELEFANTE', fonema: 'EEE', emoji: '🐘' },
u: { gesto: 'Señalá el suelo con la mano', palabra: 'UNO', fonema: 'UUU', emoji: '1️⃣' },
}
function buildVocalExercises(vocal: string, index: number): any[] {
const g = gestos[vocal]
const exs: any[] = []
// Ejercicio de gesto (sensibilizacion)
exs.push({
id: `lec3-${vocal}-gesto`,
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'sensibilizacion',
prerequisites: getVocalPrereqs(vocal),
content: {
instruction: `Hacé así para la ${vocal.toUpperCase()}: ${g.gesto}`,
audioText: `Esta es la letra ${vocal.toUpperCase()}. ${g.palabra}. ${g.fonema}`,
emoji: g.emoji,
color: getVocalColor(vocal),
},
functionalContext: `Aprender el gesto Borel-Maisonny para la vocal ${vocal.toUpperCase()}.`,
})
// Reconocer la vocal entre otras
exs.push(vocalChoice(
vocal, vocal.toUpperCase(),
['A', 'O', 'I', 'E', 'U'].filter((v) => v !== vocal.toUpperCase()),
g.emoji,
`Señalá la letra ${vocal.toUpperCase()}`,
))
// ¿Con qué vocal empieza?
exs.push({
id: `lec3-${vocal}-inicial`,
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'eleccion',
prerequisites: [`lec3-${vocal}-gesto`],
content: {
instruction: `¿Con qué vocal empieza ${g.palabra}?`,
text: g.palabra,
audioText: `${g.palabra} empieza con ${vocal.toUpperCase()}`,
options: [
{ label: vocal.toUpperCase(), correct: true, id: vocal },
...['A', 'O', 'I', 'E', 'U'].filter((v) => v !== vocal.toUpperCase()).slice(0, 2).map((v) => ({ label: v, correct: false, id: v })),
],
emoji: g.emoji,
},
functionalContext: `Reconocer la vocal inicial en palabras.`,
errorType: 'discriminacion-auditiva',
})
// Emparejar imagen con vocal
exs.push({
id: `lec3-${vocal}-emparejar`,
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'conciencia',
prerequisites: [`lec3-${vocal}-inicial`],
content: {
instruction: `¿Cuál empieza con ${vocal.toUpperCase()}?`,
audioText: `¿Qué empieza con ${vocal.toUpperCase()}?`,
options: [
{ label: g.emoji, correct: true, id: vocal },
...Object.entries(gestos)
.filter(([k]) => k !== vocal)
.slice(0, 2)
.map(([k, v]) => ({ label: v.emoji, correct: false, id: k })),
],
},
functionalContext: `Asociar la vocal con imágenes que empiezan con ella.`,
errorType: 'confusion-letra',
})
// Discriminación auditiva
exs.push({
id: `lec3-${vocal}-auditiva`,
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'conciencia',
prerequisites: [`lec3-${vocal}-emparejar`],
content: {
instruction: `Escuchá bien. ¿Cuál suena como ${vocal.toUpperCase()}?`,
audioText: `${g.fonema}... ${g.fonema}... como ${g.palabra}`,
options: [
{ label: vocal.toUpperCase(), correct: true, id: vocal },
...['A', 'O', 'I', 'E', 'U'].filter((v) => v !== vocal.toUpperCase()).slice(0, 2).map((v) => ({ label: v, correct: false, id: v })),
],
emoji: '👂',
},
functionalContext: `Discriminar auditivamente la vocal ${vocal.toUpperCase()}.`,
errorType: 'discriminacion-auditiva',
})
// Escribir con el dedo (trazo)
exs.push({
id: `lec3-${vocal}-trazo`,
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'trazo',
prerequisites: [`lec3-${vocal}-auditiva`],
content: {
instruction: `Trazá la letra ${vocal.toUpperCase()} con tu dedo`,
text: vocal.toUpperCase(),
audioText: `Trazá la ${vocal.toUpperCase()}`,
color: getVocalColor(vocal),
emoji: g.emoji,
},
functionalContext: `Desarrollar memoria motriz de la vocal ${vocal.toUpperCase()}.`,
errorType: 'confusion-letra',
})
// Repaso final
exs.push({
id: `lec3-${vocal}-completa`,
stage: 3,
skillCode: `vocal-${vocal}`,
type: 'eleccion',
prerequisites: [`lec3-${vocal}-trazo`],
content: {
instruction: `¡Repaso! Señalá la letra ${vocal.toUpperCase()}`,
audioText: `Buscá la ${vocal.toUpperCase()}`,
options: [
{ label: vocal.toUpperCase(), correct: true, id: vocal },
...['A', 'O', 'I', 'E', 'U'].filter((v) => v !== vocal.toUpperCase()).map((v) => ({ label: v, correct: false, id: v })),
],
emoji: g.emoji,
color: getVocalColor(vocal),
},
functionalContext: `Consolidar reconocimiento de la vocal ${vocal.toUpperCase()}.`,
errorType: 'confusion-letra',
})
return exs
}
// Additional cross-vowel discrimination exercises
const mixedExercises: any[] = [
{
id: 'lec3-mix-ae',
stage: 3,
skillCode: 'vocal-discriminacion',
type: 'eleccion',
prerequisites: ['lec3-a-completa', 'lec3-e-completa'],
content: {
instruction: '¿Cuál es la A y cuál es la E? Señalá la A',
options: [
{ label: 'A', correct: true, id: 'a' },
{ label: 'E', correct: false, id: 'e' },
{ label: 'O', correct: false, id: 'o' },
],
emoji: '🔤',
},
functionalContext: 'Discriminar entre A y E.',
errorType: 'confusion-letra',
},
{
id: 'lec3-mix-iu',
stage: 3,
skillCode: 'vocal-discriminacion',
type: 'eleccion',
prerequisites: ['lec3-i-completa', 'lec3-u-completa'],
content: {
instruction: '¿Cuál es la I y cuál es la U? Señalá la I',
options: [
{ label: 'I', correct: true, id: 'i' },
{ label: 'U', correct: false, id: 'u' },
{ label: 'A', correct: false, id: 'a' },
],
emoji: '🔤',
},
functionalContext: 'Discriminar entre I y U.',
errorType: 'confusion-letra',
},
{
id: 'lec3-mix-ao',
stage: 3,
skillCode: 'vocal-discriminacion',
type: 'eleccion',
prerequisites: ['lec3-a-completa', 'lec3-o-completa'],
content: {
instruction: 'Señalá la letra O',
options: [
{ label: 'A', correct: false, id: 'a' },
{ label: 'O', correct: true, id: 'o' },
{ label: 'U', correct: false, id: 'u' },
],
emoji: '🔤',
},
functionalContext: 'Discriminar entre A y O.',
errorType: 'confusion-letra',
},
{
id: 'lec3-mix-ei',
stage: 3,
skillCode: 'vocal-discriminacion',
type: 'eleccion',
prerequisites: ['lec3-e-completa', 'lec3-i-completa'],
content: {
instruction: 'Señalá la E',
options: [
{ label: 'I', correct: false, id: 'i' },
{ label: 'E', correct: true, id: 'e' },
{ label: 'U', correct: false, id: 'u' },
],
emoji: '🔤',
},
functionalContext: 'Discriminar entre E e I.',
errorType: 'confusion-letra',
},
]
const exercises: any[] = []
for (const [i, vocal] of ['a', 'o', 'i', 'e', 'u'].entries()) {
exercises.push(...buildVocalExercises(vocal, i))
}
exercises.push(...mixedExercises)
const stage: Stage = {
id: 3,
name: 'Vocales A-O-I-E-U',
description: 'Aprender las 5 vocales con gestos Borel-Maisonny, discriminación auditiva y visual, trazo y reconocimiento.',
exercises,
}
export default stage