Pipeline animation pour donner vie aux personnages arabes
6 min de lectureMohammad Shaker

Pipeline animation pour donner vie aux personnages arabes

Amal utilise Rive pour la synchro labiale, les avatars et les personnages de jeu, avec des machines d etat plus souples que Lottie.

Engineering

Réponse rapide

Amal utilise Rive pour la synchro labiale, les avatars et les personnages de jeu, avec des machines d etat plus souples que Lottie.

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)
    1. Corps entier : tête, torse, membres avec vêtements
    2. Tête uniquement : pour le tableau de bord et l'application parent
    3. 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 :

  1. Active l'élément Rive Head_Round
  2. Active l'élément Rive Shirt_Blue
  3. Désactive toutes les autres formes de tête et couleurs de chemise
  4. 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 .riv lors 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.

Articles connexes