Amal koristi Rive (ranije Flare) za sve animacije likova — uključujući sinkronizirani govor, prilagodbu avatara, reakcije na povratne informacije i likove u igrama. Odabrali smo Rive umjesto Lottie ili sprite listova jer podržava runtime state-machine, programatsku manipulaciju i GPU ubrzano renderiranje pri 60fps, sve u jednoj kompaktnijoj datoteci po liku.
Biblioteka animacijskih resursa
Osnovni likovi
lip-sync-amal-01.riv
- Glavni Amal lik (varijante cijelog tijela i samo lica)
- Više artboarda po poziciji usta (za mapiranje fonema)
- Stanja: mirovanje, govor, greška, slavlje, spavanje
- Veličina datoteke: 1.2 MB (u odnosu na 50+ MB za sprite listove)avatar.riv
- Prilagodljivi korisnički avatar (3 artboarda):
1. Cijelo tijelo: glava, trup, udovi s odjećom
2. Samo glava: za nadzornu ploču i roditeljsku aplikaciju
3. Pratilac leptir: animacija nagrade
- Komponentna struktura: oblik glave, kosa, oči, odjeća, dodaci, boje
- Veličina datoteke: 2.4 MBcoin-01.riv&coins-01.riv
- Animacije nagrada (leteći i skupljajući novčići)
- Jedan novčić: 150 KB
- Više novčića: 300 KBcute-monster-final.riv
- Lik za povratne informacije sa više emotivnih stanja
- Stanja: sretan (tačan odgovor), zbunjen (pogrešan), razmišlja, slavi (niz pobjeda)
- Veličina datoteke: 1.8 MB
Optimizacija za Android
- Prilagođena NDK verzija (Rive NDK-r28) za usklađenost s 16KB poravnanje stranice
- Smanjuje veličinu binarne datoteke za 8% u odnosu na standardnu izgradnju
- Osigurava kompatibilnost s naprednim upravljanjem memorijom u Android 12+
Proces sinkronizacije usana (Tehnički detalji)
Korak 1: Generisanje TTS zvuka i vađenje oznaka govora
# 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' # WaveNet glas
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Zvučnik za djecu
)
# Zahtjev za oznake govora (vrijeme fonema)
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)
# Odgovor sadrži:
# - audio_content: MP3 podaci
# - timepoints: [{character, byte_pos, time_ms}, ...]
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # Timestamps na nivou fonema
}
Korak 2: Mapiranje fonema na Rive stanja usta
{
"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 } },
...
]
}
Korak 3: Kontroler sinkronizacije usana upravlja reprodukcijom
// 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) {
// Korak 1: Učitaj Rive lik
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// Korak 2: Učitaj oznake govora iz TTS izlaza
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// Korak 3: Pusti zvuk dok upravljaš animacijom usta
audioPlayer.play(AudioSource.file(audioPath));
// Korak 4: Na svaki audio frame ažuriraj položaj usta
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
Korak 4: RiveCharacterController upravlja životnim ciklusom
// Upravljanje kompletnim animacijskim stanjima lika (ne samo ustima)
class RiveCharacterController extends GetxController {
States: idle → prepare → speaking → idle → error/celebration
void startExercise() {
// Prelaz lika: idle → prepare (spreman za slušanje)
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();
}
}
}
Sistem prilagođavanja avatara
Arhitektura po komponentama
Djeca mogu prilagođavati svog avatara od dijelova:
{
"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"]
}
}
}
Mapa naziva Rive elemenata (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',
// ... preko 50 mapa elemenata
};
Kada dijete izabere „okruglu glavu + plavu košulju“, aplikacija:
- Aktivira Rive element
Head_Round - Aktivira Rive element
Shirt_Blue - Deaktivira ostale oblike glave i boje košulja
- Personalizovani avatar djeteta pojavljuje se u cijeloj aplikaciji
Zašto Rive umjesto drugih opcija
| Karakteristika | Rive | Lottie | Sprite lists | Video |
|---|---|---|---|---|
| State machine | ✓ | ✗ | ✗ | ✗ |
| Kontrola u runtimeu | ✓ (puna) | Djelimična | Ručna | ✗ (pasivna) |
| Veličina datoteke | 1-2 MB | 2-3 MB | 50+ MB | 100+ MB |
| Performanse | 60fps GPU | 30fps CPU | 60fps GPU | Varijabilno |
| Interaktivnost | ✓ puna | ✓ djelimična | ✓ puna | ✗ nema |
| Učenje | Umjereno | Lako | Lako | Lako |
| Održavanje | Jedna .riv datoteka | Jedan JSON | Stotine slika | Jedan video |
Rive pobeđuje jer je potrebna programatska kontrola, state-machine i kompaktna veličina za mobilnu aplikaciju.
Optimizacija performansi
- Predučitavanje likova: Učitavanje .riv datoteka pri pokretanju aplikacije, ne po vježbi
- GPU renderiranje: Rive koristi GPU kada je dostupan, uz CPU fallback na starijim uređajima
- Mrežni ponovni uporabni kontroleri: Ponovno korištenje Rive kontrolera između ekrana za smanjenje pauza prikupljanja smeća
- Komprimirane datoteke: Rive datoteke su već komprimirane; dodatna optimizacija nije potrebna
Rezultat: 60fps animacije na Snapdragon 662+ (telefoni srednjeg ranga iz 2019).
Česta pitanja
P: Mogu li izvesti animacije iz Adobe Animate u Rive?
A: Ne direktno. Koristimo Rive-ov vlastiti editor (rive.app). Animatori rade dizajn u Rive, ne u Animate ili After Effects. Tok rada: dizajn u Rive → izvoz u .riv → integracija u Flutter aplikaciju.
P: Kako se nosite s različitim tipovima tijela ili invaliditetima?
A: Prilagodba avatara uključuje opcije oblika tijela (vitak, atletski, okrugli) i dodatke (naočale, slušni aparati, pomagala za kretanje) da sva djeca budu zastupljena.
P: Šta ako dijete ne voli svoj avatar?
A: Može ga mijenjati u bilo kojem trenutku. Aplikacija ne nameće izgled — djeca imaju potpunu kreativnu kontrolu.



