You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
6.0 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 🇮🇹 Italiano Vivo
App de italiano **situacional-first** com voz livre e scoring de inteligibilidade.
Aprenda como uma criança: errando mas se comunicando.
## Visão
- **Scoring em 3 eixos**: Sentido transmitido / Naturalidade / Precisão gramatical
- **Tolerância estilo criança**: erro gramatical que não impede entendimento = sucesso comunicativo
- **Voz livre**: grave, receba feedback estruturado, nunca "Errado"
- **Salvavidas (Salvavita)**: treine estratégias de comunicação quando a palavra some
- **SRS de chunks**: revisão espaçada de frases, não palavras isoladas
- **PWA offline**: catálogo, áudio e SRS funcionam sem conexão
- **CEFR can-do**: badges baseados em competências reais ("você consegue reclamar de um item com defeito")
## Como Rodar
### Pré-requisitos
- Node.js ≥ 20
- LiteLLM proxy rodando (ou configure `LITELLM_BASE_URL` para outro endpoint OpenAI-compatible)
### Instalação
```bash
# Instalar dependências do monorepo
npm install
# Configurar variáveis de ambiente
# rode SEMPRE a partir de server/ (o dotenv le server/.env pelo cwd):
cd server && npm run dev # ou node dist/index.js
# Edite server/.env com sua chave da API
```
### Desenvolvimento
```bash
# Roda app + server simultaneamente
npm run dev
```
- App: http://localhost:5173
- Server: http://localhost:8787
- Health check: http://localhost:8787/health
### Build
```bash
npm run build # Build server + app
npm run typecheck # Verificação de tipos
```
## Endpoints do Server
| Método | Rota | Descrição |
|--------|------|-----------|
| GET | `/health` | Status do servidor |
| POST | `/api/judge` | Avalia fala do aluno em 3 eixos (sentido, naturalidade, gramática) |
| POST | `/api/transcribe` | Transcreve áudio via modelo omni (ASR tolerante a sotaque) |
| GET | `/api/tts-proxy?text=...&voice=elsa\|diego` | Serve áudio TTS pré-gerado do cache local |
### POST /api/judge
**Entrada:**
```json
{
"missionId": "bambino-m01",
"scenario": "Al bar",
"learnerGoalPt": "Pedir um café",
"expectedIt": "Vorrei un caffè, per favore",
"utteranceText": "io volere caffe",
"transcriptRaw": "io volere caffe",
"audioBase64": "..."
}
```
**Saída:**
```json
{
"meaning_conveyed": "full",
"naturalness": 45,
"natural_version": "Vorrei un caffè, per favore",
"grammar": [
{
"it_said": "io volere",
"problem_pt": "Infinitivo em vez de conjugação",
"fix_it": "vorrei / voglio",
"severity": "minor"
}
],
"next_move": {
"italian": "Certo! Un caffè. Lo vuole al banco o al tavolo?",
"portuguese": "Claro! Um café. Quer no balcão ou na mesa?"
},
"praise_or_push": "Quase lá! Um italiano diria: Vorrei un caffè, per favore"
}
```
## Estrutura de Pastas
```
cursoitaliano/
├── app/ # PWA React + Vite + TypeScript
│ ├── public/ # manifest.webmanifest, ícones
│ └── src/
│ ├── db/db.ts # IndexedDB via Dexie
│ ├── engine/srs.ts # SM-2 simplificado
│ ├── types/content.ts # Tipos do conteúdo JSON
│ ├── App.tsx # Componente principal
│ ├── main.tsx # Entry point
│ └── styles.css # Design system
├── server/ # Node + Express + TypeScript
│ ├── cache/audio/ # Áudios TTS pré-gerados (offline)
│ └── src/
│ ├── lib/llm.ts # Cliente LiteLLM singleton
│ ├── routes/
│ │ ├── judge.ts # Motor de feedback 3 eixos
│ │ ├── transcribe.ts # ASR via omni model
│ │ └── tts.ts # Proxy de áudio TTS
│ └── index.ts # Express app
├── content/ # JSONs versionados (gerados por pipeline)
├── audio/ # Assets de áudio por missão
├── SPEC.md # Especificação completa do produto
├── .env.example # Template de variáveis de ambiente
└── package.json # Monorepo root (npm workspaces)
```
## Dois ritmos de áudio 🐢🐇
Cada frase do catálogo possui **dois arquivos de áudio** gerados com velocidades de fala **reais** (não playbackRate):
- `<key>.mp3` — velocidade natural (padrão do TTS)
- `<key>--slow.mp3` — fala desacelerada em **-30%** via `edge-tts --rate=-30%`
### Por que rate real e não playback?
Crianças aprendem ouvindo devagar **de verdade**: a prosódia, entonação e pausas naturais são preservadas quando o TTS gera a fala mais lenta desde a síntese. Playback acelerado/desacelerado distorce formantes e perde a cadência natural da língua.
### Regerar áudios
```bash
# Gerar apenas variantes lentas (idempotente, pula >500 bytes)
node tools/gen-audio.mjs --only slow
# Gerar apenas variantes rápidas/naturais
node tools/gen-audio.mjs --only fast
# Gerar ambas (default)
node tools/gen-audio.mjs
```
No player da app, dois botões lado a lado: 🐢 lento e 🐇 naturale. Se o arquivo lento falhar (404), fallback silencioso para o natural.
## Modelos Utilizados
| Modelo | Uso | Variável |
|--------|-----|----------|
| qwen3.8-max | Juiz de feedback, geração de missões | `CHAT_MODEL` |
| qwen3.8-flash | Drills rápidos, fallback de quota | `FAST_MODEL` |
| qwen3-omni-flash | Transcrição de áudio (ASR) | `OMNI_MODEL` |
Todos acessados via LiteLLM proxy (`LITELLM_BASE_URL`).
## Roadmap
- [x] Esqueleto do monorepo (app PWA + server LiteLLM)
- [x] Geração de conteúdo: Trilha Bambino (B1-B12) + Viaggio (V1-V6) = 18 missões
- [x] Pipeline de geração de áudio TTS (edge-tts) — ~479 arquivos mp3
- [x] Judge 3-eixos CEFR com tolerância estilo criança
- [x] SRS de chunks com Dexie/IndexedDB
- [ ] Service Worker para modo offline completo
- [ ] Minijogo "descreva sem nomear" (circunlóquio)
- [ ] Modo carro/shadowing com medição de lag
- [ ] Trilhas restantes (Viaggio M7–M15, Vivere completa)
- [ ] Preparatório CILS/CELI B1 cidadania
- [ ] Wrapper Android (Capacitor)
- [ ] Calibração do judge com votos "isso estava certo"
## Licença
Privado — uso pessoal.