Come Animiamo i Personaggi Arabi con Rive
5 min di letturaMohammad Shaker

Come Animiamo i Personaggi Arabi con Rive

Scopri come Amal usa Rive per animazioni di personaggi arabi con sincronizzazione labiale e personalizzazione avatar.

Engineering

Risposta rapida

Scopri come Amal usa Rive per animazioni di personaggi arabi con sincronizzazione labiale e personalizzazione avatar.

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:

  1. Attiva elemento Rive Head_Round
  2. Attiva elemento Rive Shirt_Blue
  3. Disattiva tutte le altre forme testa e colori maglietta
  4. L’avatar personalizzato appare in tutta l’app

Perché Rive Rispetto alle Alternative

CaratteristicaRiveLottieSprite SheetsVideo
Macchine a stati
Controllo a runtime✓ (completo)ParzialeManuale✗ (passivo)
Dimensione file1-2 MB2-3 MB50+ MB100+ MB
Performance60fps GPU30fps CPU60fps GPUVariabile
Interattività✓ Completa✓ Parziale✓ Completa✗ Nessuna
Curva d’apprendimentoModerataFacileFacileFacile
ManutenzioneUn file .rivUn JSONCentinaia di immaginiUn 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.

Articoli correlati