Nosso Pipeline de Animação Rive: Dando Vida a Personagens Árabes
Amal utiliza Rive (antigo Flare) para todas as animações dos personagens — incluindo fala sincronizada com os lábios, personalização de avatar, reações de feedback e personagens em jogos. Escolhemos Rive em vez de Lottie ou folhas de sprites porque ele suporta máquinas de estado em tempo real, manipulação programática e renderização acelerada por GPU a 60fps, tudo em um único arquivo compacto por personagem.
Biblioteca de Ativos de Animação
Personagens Principais
lip-sync-amal-01.riv- Personagem principal Amal (variantes corpo inteiro e só rosto)
- Múltiplas pranchetas para posições da boca (mapeamento de fonemas)
- Estados: parado, falando, erro, comemoração, dormindo
- Tamanho do arquivo: 1,2 MB (contra 50+ MB em folhas de sprites)
avatar.riv- Avatar do usuário personalizável (3 pranchetas)
- Corpo inteiro: cabeça, torso, membros com roupas
- Apenas cabeça: para painel e app dos pais
- Companheiro borboleta: animação de recompensa
- Arquitetura por componentes: formato da cabeça, cabelo, olhos, roupas, acessórios e cores
- Tamanho do arquivo: 2,4 MB
- Avatar do usuário personalizável (3 pranchetas)
coin-01.rivecoins-01.riv- Animações de recompensa (moedas flutuando e coletando)
- Coin simples: 150 KB
- Múltiplas moedas: 300 KB
cute-monster-final.riv- Personagem de feedback com múltiplos estados emocionais
- Estados: feliz (resposta certa), confuso (resposta errada), pensando (processando), comemorando (sequência)
- Tamanho do arquivo: 1,8 MB
Otimizações específicas para Android
- Build NDK personalizado (Rive NDK-r28) para conformidade com alinhamento de página de 16KB
- Reduz tamanho do binário em 8% comparado ao build padrão
- Garante compatibilidade com gerenciamento agressivo de memória no Android 12+
Pipeline de Sincronização Labial (Detalhes Técnicos)
Passo 1: Geração de Áudio TTS + Extração de Marcas de Fala
# src/services/tts_client.py
from google.cloud import texttospeech
def generate_speech_with_marks(text: str, language: str = 'ar-SA'):
client = texttospeech.TextToSpeechClient()
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code=language,
name='ar-SA-Neural2-A' # Voz WaveNet
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Alto-falante infantil
)
# Solicitar marcas de fala (timing dos fonemas)
request = texttospeech.SynthesizeSpeechRequest(
input=synthesis_input,
voice=voice,
audio_config=audio_config,
enable_text_to_speech_as_cloud_service=True
)
response = client.synthesize_speech(request=request)
# Resposta inclui:
# - audio_content: bytes MP3
# - timepoints: [{character, byte_pos, time_ms}, ...]
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # Timestamps por fonema
}
Passo 2: Mapear Fonemas para Estados da Boca no Rive
lip_sync_avatar.json mapeia fonemas árabes para posições da boca:
{
"phoneme_map": {
"ا": { "rive_state": "mouth_a_open", "duration_ms": 200 },
"ب": { "rive_state": "mouth_lips_closed", "duration_ms": 150 },
"ع": { "rive_state": "mouth_pharyngeal", "duration_ms": 250 },
"ق": { "rive_state": "mouth_uvular", "duration_ms": 180 },
...
},
"mouth_positions": [
{ "id": "mouth_a_open", "blend_values": { "jaw_open": 0.8, "lips_rounded": 0.2 } },
{ "id": "mouth_lips_closed", "blend_values": { "jaw_open": 0.1, "lips_rounded": 0.9 } },
...
]
}
Passo 3: LipSyncController Gerencia Reprodução
// lib/src/modules/animations/controllers/lip_sync_controller.dart
class LipSyncController extends GetxController {
late Rive riveCharacter;
late AudioPlayer audioPlayer;
late LipSyncMapper mapper;
void playWithLipSync(String text, String audioPath) {
// Passo 1: Carregar personagem Rive
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// Passo 2: Carregar marcas de fala do TTS
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// Passo 3: Tocar áudio enquanto anima a boca
audioPlayer.play(AudioSource.file(audioPath));
// Passo 4: Em cada quadro de áudio, atualizar posição da boca
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
Passo 4: RiveCharacterController Gerencia Ciclo de Vida
// Gerencia o estado completo da animação do personagem (não só boca)
class RiveCharacterController extends GetxController {
States: idle → prepare → speaking → idle → error/celebration
void startExercise() {
// Transição: idle → prepare (preparar para ouvir)
character.setStateInput('state_machine', 'prepare');
}
void childSpeaks(String recognizedText, double accuracy) {
character.setStateInput('state_machine', 'speaking');
lipSyncController.playFeedback(recognizedText);
}
void onFeedbackComplete(bool wasCorrect) {
if (wasCorrect) {
character.setStateInput('state_machine', 'celebrate');
playRewardAnimation();
} else {
character.setStateInput('state_machine', 'error');
playEncouragingPhrase();
}
}
}
Sistema de Personalização de Avatar
Arquitetura por Componentes
Crianças personalizam seu avatar escolhendo partes:
{
"avatar_customization": {
"head_shapes": [
{ "id": "round", "rive_element": "head_round" },
{ "id": "oval", "rive_element": "head_oval" },
{ "id": "square", "rive_element": "head_square" }
],
"hair_styles": [
{ "id": "ponytail", "rive_element": "hair_ponytail" },
{ "id": "braids", "rive_element": "hair_braids" },
{ "id": "straight", "rive_element": "hair_straight" }
],
"colors": {
"skin_tone": ["light", "medium", "dark"],
"hair_color": ["black", "brown", "blonde", "red"],
"shirt_color": ["blue", "pink", "green", "yellow", "purple"],
"accent_color": ["red", "orange", "green", "blue"]
}
}
}
Mapeamento dos Elementos Nomeados Rive (avatar_customization_rive_names.dart)
const avatarRiveNames = {
'head_round': 'Head_Round',
'head_oval': 'Head_Oval',
'hair_ponytail': 'Hair_Ponytail',
'shirt_blue': 'Shirt_Blue',
'shirt_pink': 'Shirt_Pink',
// ... mais de 50 mapeamentos
};
Quando a criança escolhe "cabeça redonda + camisa azul", o app:
- Ativa elemento Rive
Head_Round - Ativa elemento Rive
Shirt_Blue - Desativa todos os outros formatos de cabeça e cores de camisa
- O avatar personalizado aparece em toda a experiência
Por que Usar Rive ao Invés de Outras Alternativas
| Recurso | Rive | Lottie | Folhas de Sprites | Vídeo |
|---|---|---|---|---|
| Máquinas de estado | ✓ | ✗ | ✗ | ✗ |
| Controle em execução | ✓ (completo) | Parcial | Manual | ✗ (passivo) |
| Tamanho do arquivo | 1-2 MB | 2-3 MB | 50+ MB | 100+ MB |
| Performance | 60fps GPU | 30fps CPU | 60fps GPU | Variável |
| Interatividade | ✓ Completa | ✓ Parcial | ✓ Completa | ✗ Nenhuma |
| Curva de aprendizado | Moderada | Fácil | Fácil | Fácil |
| Manutenção | Um arquivo .riv | Um JSON | Centenas de imagens | Um vídeo |
Rive vence porque precisamos de controle programático, máquinas de estado e arquivos compactos para apps móveis.
Otimização de Performance
- Pré-carregamento dos personagens: carregamos arquivos .riv no início do app, não por exercício
- Renderização por GPU: Rive usa GPU automaticamente quando disponível e faz fallback para CPU em dispositivos antigos
- Pool de memória: reutilizamos controladores Rive entre telas para evitar pausas de coleta de lixo
- Compressão: arquivos Rive já são compactados, sem necessidade de otimização extra
Resultado: animações a 60fps em Snapdragon 662+ (celulares intermediários de 2019).
Perguntas Frequentes
P: Posso exportar animações do Adobe Animate para Rive?
R: Não diretamente. Usamos o editor nativo do Rive (rive.app). Animadores criam personagens no Rive, não no Animate ou After Effects. O fluxo é: criar no Rive → exportar .riv → integrar no app Flutter.
P: Como lidam com diferentes tipos de corpo ou deficiências?
R: A personalização de avatar inclui opções de tipos físicos (magro, atlético, arredondado) e acessórios (óculos, aparelhos auditivos, ajudas motoras). Isso garante representação para todas as crianças.
P: E se a criança não gostar do avatar?
R: Pode personalizar a qualquer momento. O app não força um visual fixo — as crianças têm controle criativo total.



