Notre pipeline d'animation Rive : donner vie aux personnages arabes
Amal utilise Rive (anciennement Flare) pour toutes les animations de personnages — y compris la synchronisation labiale, la personnalisation d'avatar, les réactions de retour et les personnages de jeu. Nous avons choisi Rive plutôt que Lottie ou les planches de sprites car il prend en charge les machines d'état à l'exécution, la manipulation programmatique et le rendu accéléré par GPU à 60fps, le tout dans un fichier compact par personnage.
La bibliothèque d'actifs d'animation
Personnages principaux
lip-sync-amal-01.riv
- Personnage principal Amal (variantes en pleine forme et visage uniquement)
- Plusieurs planches par position de bouche (pour la cartographie des phonèmes)
- États : inactif, parlant, erreur, célébration, dormant
- Taille du fichier : 1.2 MB (vs. plus de 50 MB pour des planches de sprites)
avatar.riv
- Avatar utilisateur personnalisable (3 planches)
- Corps entier : tête, torse, membres avec vêtements
- Tête uniquement : pour le tableau de bord et l'application parent
- Compagnon papillon : animation de récompense
- Basé sur des composants : forme de la tête, cheveux, yeux, vêtements, accessoires, couleurs
- Taille du fichier : 2.4 MB
coin-01.riv & coins-01.riv
- Animations de récompense (pièces flottantes, collecte)
- Pièce unique : 150 KB
- Plusieurs pièces : 300 KB
cute-monster-final.riv
- Personnage de retour avec plusieurs états émotionnels
- États : heureux (bonne réponse), confus (incorrect), pensif (en traitement), célébrant (série)
- Taille du fichier : 1.8 MB
Optimisation spécifique à Android
- Build NDK personnalisé (Rive NDK-r28) pour la conformité à l'alignement de pages de 16KB
- Réduit la taille du binaire de 8% par rapport à la build standard
- Assure la compatibilité avec une gestion de mémoire agressive dans Android 12+
Pipeline de synchronisation labiale (Approfondissement technique)
Étape 1 : Génération audio TTS + Extraction des marques vocales
# 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' # Voix WaveNet
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Haut-parleur de l'enfant
)
# Demande des marques vocales (timing des phonèmes)
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)
# La réponse inclut :
# - audio_content : octets MP3
# - points temporels : [{caractère, position_octet, temps_ms}, ...]
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # Horodatages au niveau des phonèmes
}
Étape 2 : Mapper les phonèmes aux états de bouche dans Rive
lip_sync_avatar.json associe les phonèmes arabes aux positions de bouche :
{
"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 } },
...
]
}
Étape 3 : LipSyncController orchestre la lecture
// 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) {
// Étape 1 : Charger le personnage Rive
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// Étape 2 : Charger les marques vocales depuis la sortie TTS
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// Étape 3 : Jouer l'audio tout en animant la bouche
audioPlayer.play(AudioSource.file(audioPath));
// Étape 4 : À chaque trame audio, mettre à jour la position de bouche
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
Étape 4 : RiveCharacterController gère le cycle de vie
// Gère l'état d'animation complet du personnage (non juste la bouche)
class RiveCharacterController extends GetxController {
States: idle → prepare → speaking → idle → error/celebration
void startExercise() {
// Transitions de personnage : inactif → prêt (prêt à écouter)
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();
}
}
}
Système de personnalisation d'avatar
Architecture basée sur des composants
Les enfants personnalisent leur avatar à partir de pièces :
{
"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"]
}
}
}
Correspondance des éléments nommés 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',
// ... 50+ correspondances d'éléments
};
Lorsqu'un enfant sélectionne "tête ronde + chemise bleue", l'application :
- Active l'élément Rive
Head_Round - Active l'élément Rive
Shirt_Blue - Désactive toutes les autres formes de tête et couleurs de chemise
- L'avatar personnalisé de l'enfant apparaît maintenant dans toute l'application
Pourquoi Rive plutôt que les alternatives
| Fonctionnalité | Rive | Lottie | Planches de sprites | Vidéo |
|---|---|---|---|---|
| Machines d'état | ✓ | ✗ | ✗ | ✗ |
| Contrôle en temps réel | ✓ (complet) | Partiel | Manuel | ✗ (passif) |
| Taille de fichier | 1-2 MB | 2-3 MB | 50+ MB | 100+ MB |
| Performance | 60 fps GPU | 30 fps CPU | 60 fps GPU | Variable |
| Interactivité | ✓ Pleine | ✓ Partielle | ✓ Pleine | ✗ Aucune |
| Courbe d'apprentissage | Modérée | Facile | Facile | Facile |
| Entretien | Un fichier .riv | Un fichier JSON | Des centaines d'images | Une vidéo |
Rive est gagnant car nous avons besoin de contrôle programmatique, de machines d'état, et de compacité pour une application mobile.
Optimisation des performances
- Précharger les personnages : Charger les fichiers
.rivlors du démarrage de l'application, pas par exercice - Rendu GPU : Rive utilise automatiquement le GPU quand disponible, repli CPU sur les appareils anciens
- Mise en commun de mémoire : Réutilise les contrôleurs Rive entre les écrans pour éviter les pauses de collecte des ordures
- Compression : Les fichiers Rive sont déjà compressés ; aucune optimisation supplémentaire nécessaire
Résultat : animations à 60 fps sur Snapdragon 662+ (téléphones milieu de gamme 2019).
FAQ
Q : Puis-je exporter des animations d'Adobe Animate vers Rive ? R : Pas directement. Nous utilisons l'éditeur natif de Rive (rive.app). Les animateurs conçoivent dans Rive, pas dans Animate ou After Effects. Le workflow est : concevoir le personnage dans Rive → exporter en .riv → intégrer dans l'application Flutter.
Q : Comment gérez-vous les différents types de corps ou handicaps ? R : La personnalisation de l'avatar inclut des options de type de corps (svelte, athlétique, rond) et des accessoires (lunettes, appareils auditifs, aides à la mobilité). Cela garantit que tous les enfants voient une représentation.
Q : Et si un enfant n'aime pas son avatar ? R : Ils peuvent le personnaliser à tout moment. L'application ne force pas un look particulier — les enfants ont un contrôle créatif total.



