Turritopsis dohrnii Dark Intelligence
← Voltar aos cases
Estudo pessoal → Case técnico

HSK1 com IA — Flashcards de Mandarim

Um app de flashcards com áudio pra estudar o vocabulário do HSK1 (nível básico de mandarim), que evoluiu de um script simples no Replit pra uma aplicação com arquitetura separada front/back e repetição espaçada baseada em ML.

Frontend

HTML / CSS / JS puro

Backend

Python (Flask) + gunicorn

Hospedagem

Hostgator (front) + Render (API)

ML

FSRS (repetição espaçada)

O problema

O projeto começou como um app simples no Replit: um Flask servindo uma palavra aleatória do HSK1 por vez, com áudio gerado sob demanda via gTTS. Funcionava, mas tinha três limitações que valia a pena resolver de verdade em vez de só remendar: o Replit hibernava sozinho no plano free, a ordem das palavras era puramente aleatória (sem lógica de aprendizado), e o áudio dependia de chamar uma API não-oficial do Google em tempo real — o que, como descobri em produção, tem rate limit agressivo.

Arquitetura: separar front e back

A primeira decisão foi sair do modelo "um Flask que gera HTML e serve tudo" pra uma API JSON pura no backend (/api/word, /api/words, /api/audio/<hanzi>) consumida por um frontend estático via fetch(). Isso trouxe dois benefícios diretos: o frontend pôde ir pra uma hospedagem compartilhada comum (Hostgator, sem custo adicional), e o backend ficou livre pra rodar num serviço que suporta Python de verdade (Render), sem as limitações de hospedagem compartilhada tradicional.

O bug em produção: rate limit do gTTS

Depois do primeiro deploy, o áudio simplesmente não tocava — erro 500. O log do Render revelou a causa: gTTS usa a API não-oficial do Google Translate, e o IP compartilhado do Render (usado por muitos outros projetos) já estava sendo limitado com erro 429 (excesso de requisições). A solução não foi tentar contornar o rate limit, e sim eliminar a dependência dele em produção: os 113 áudios do deck passaram a ser pré-gerados uma única vez, localmente, e enviados junto com o deploy — o backend em produção só serve arquivos estáticos, sem nunca chamar o Google.

A parte de ML: FSRS

A evolução mais interessante foi trocar a ordem aleatória por repetição espaçada real, usando o algoritmo FSRS (o mesmo que hoje roda por trás do Anki moderno). Implementei o algoritmo em JavaScript puro, direto no navegador: cada palavra guarda uma estabilidade (S, em dias) e uma dificuldade (D), recalculadas a cada avaliação do usuário (Esqueci / Difícil / Bom / Fácil). O app prioriza mostrar primeiro as palavras que estão prestes a ser esquecidas — estimado por uma fórmula de retrievability baseada em quanto tempo se passou desde a última revisão em relação à estabilidade atual — em vez de reforçar aleatoriamente palavras que o usuário já domina.

O progresso é salvo no localStorage do navegador — uma limitação consciente da v1 (não é multi-dispositivo), documentada como próximo passo natural: mover esse estado pro backend com autenticação simples.

Próximos passos

  • — Persistência de progresso no backend (conta de usuário, multi-dispositivo)
  • — Feedback de pronúncia: gravação do usuário + análise de contorno tonal (pitch)
  • — Re-treino dos pesos do FSRS com dados reais de uso, em vez dos pesos padrão