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 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.
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.
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 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.