Amal utilizza Rive (ex Flare) per tutte le animazioni dei personaggi — inclusi il parlato sincronizzato con le labbra, la personalizzazione degli avatar, le reazioni di feedback e i personaggi di gioco. Abbiamo scelto Rive invece di Lottie o sprite sheets perché supporta macchine a stati al runtime, manipolazione programmata e rendering accelerato dalla GPU a 60fps, il tutto in un file compatto per personaggio.
La Libreria degli Asset di Animazione
Personaggi Principali
lip-sync-amal-01.riv
- Personaggio principale Amal (varianti corpo intero e solo volto)
- Multipli artboard per ogni posizione della bocca (mappatura fonemi)
- Stati: inattivo, parlante, errore, celebrazione, sonno
- Dimensione file: 1,2 MB (contro oltre 50 MB degli sprite sheets)
avatar.riv
- Avatar personalizzabile dell’utente (3 artboard)
- Corpo intero: testa, busto, arti con vestiti
- Solo testa: per dashboard e app genitori
- Compagno farfalla: animazione ricompensa
- Architettura a componenti: forma testa, capelli, occhi, vestiti, accessori, colori
- Dimensione file: 2,4 MB
coin-01.riv & coins-01.riv
- Animazioni di ricompensa (monete fluttuanti, raccolta)
- Singola moneta: 150 KB
- Più monete: 300 KB
cute-monster-final.riv
- Personaggio di feedback con vari stati emotivi
- Stati: felice (risposta corretta), confuso (sbagliata), riflette (elaborazione), festeggia (combo)
- Dimensione file: 1,8 MB
Ottimizzazione Specifica per Android
- Build NDK personalizzata (Rive NDK-r28) per allineamento pagine 16KB
- Riduce dimensione binaria dell’8% rispetto a build standard
- Garantisce compatibilità con gestione della memoria aggressiva su Android 12+
Pipeline di Sincronizzazione Labiale (Approfondimento Tecnico)
Passo 1: Generazione Audio TTS + Estrazione Marchi Vocali
# 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' # Voce WaveNet
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Altoparlante per bambini
)
# Richiesta marchi vocali (tempistiche fonemi)
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)
# Response include:
# - audio_content: bytes MP3
# - timepoints: [{character, byte_pos, time_ms}, ...]
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # timestamp a livello fonemico
}
Passo 2: Mappare Fonemi agli Stati Bocca in Rive
lip_sync_avatar.json mappa i fonemi arabi alle posizioni della bocca:
{
"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 Coordina la Riproduzione
// 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: Carica personaggio Rive
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// Passo 2: Carica marchi vocali da output TTS
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// Passo 3: Riproduci audio sincronizzando bocca
audioPlayer.play(AudioSource.file(audioPath));
// Passo 4: Ad ogni frame audio aggiorna posizione bocca
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
Passo 4: RiveCharacterController Gestisce il Ciclo di Vita
// Gestisce stato animazione personaggio completo (non solo bocca)
class RiveCharacterController extends GetxController {
Stati: inattivo → preparazione → parlante → inattivo → errore/celebrazione
void startExercise() {
// Transizioni personaggio: inattivo → preparazione (pronto ad ascoltare)
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 di Personalizzazione Avatar
Architettura a Componenti
I bambini personalizzano il proprio avatar scegliendo tra parti:
{
"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"]
}
}
}
Mappatura degli Elementi Nominati di 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',
// ... oltre 50 mappature
};
Quando un bambino seleziona "testa rotonda + maglietta blu," l’app:
- Attiva elemento Rive
Head_Round - Attiva elemento Rive
Shirt_Blue - Disattiva tutte le altre forme testa e colori maglietta
- L’avatar personalizzato appare in tutta l’app
Perché Rive Rispetto alle Alternative
| Caratteristica | Rive | Lottie | Sprite Sheets | Video |
|---|---|---|---|---|
| Macchine a stati | ✓ | ✗ | ✗ | ✗ |
| Controllo a runtime | ✓ (completo) | Parziale | Manuale | ✗ (passivo) |
| Dimensione file | 1-2 MB | 2-3 MB | 50+ MB | 100+ MB |
| Performance | 60fps GPU | 30fps CPU | 60fps GPU | Variabile |
| Interattività | ✓ Completa | ✓ Parziale | ✓ Completa | ✗ Nessuna |
| Curva d’apprendimento | Moderata | Facile | Facile | Facile |
| Manutenzione | Un file .riv | Un JSON | Centinaia di immagini | Un video |
Rive vince perché serve controllo programmatico, macchine a stati e compattezza per un’app mobile.
Ottimizzazione delle Prestazioni
- Precaricamento personaggi: carica file .riv all’avvio app, non ad ogni esercizio
- Rendering GPU: Rive usa GPU se disponibile, fallback CPU su dispositivi meno recenti
- Pooling memoria: riusa controller Rive tra schermate per evitare pause di garbage collection
- Compressione: file Rive sono già compressi, nessun ulteriore ottimizzazione richiesta
Risultato: animazioni a 60fps su Snapdragon 662+ (smartphone di fascia media 2019).
FAQ
D: Posso esportare animazioni da Adobe Animate a Rive?
R: Non direttamente. Usiamo l’editor nativo di Rive (rive.app). Gli animatori disegnano in Rive, non in Animate o After Effects. Il flusso è: design personaggio in Rive → esporta .riv → integra in app Flutter.
D: Come gestite diversi tipi di corpo o disabilità?
R: La personalizzazione avatar include opzioni per tipo di corpo (snello, atletico, rotondo) e accessori (occhiali, apparecchi acustici, ausili per la mobilità). Così tutti i bambini si sentono rappresentati.
D: Se un bambino non gradisce il proprio avatar?
R: Può personalizzare in qualsiasi momento. L’app non impone uno stile unico — i bambini hanno pieno controllo creativo.



