Наша технология анимации Rive: оживление арабских персонажей
Amal использует Rive (ранее Flare) для всей анимации персонажей — включая синхронизацию губ с речью, кастомизацию аватаров, реакции и игровых персонажей. Мы выбрали Rive вместо Lottie или спрайт-листов, потому что он поддерживает машины состояний во время выполнения, программное управление и аппаратное ускорение GPU с 60 fps, всё в одном компактном файле для каждого персонажа.
Библиотека анимационных ресурсов
Основные персонажи
lip-sync-amal-01.riv- Главный персонаж Amal (варианты с полным телом и только лицом)
- Несколько артбордов для разных положений рта (для фонем)
- Состояния: бездействие, речь, ошибка, празднование, сон
- Размер файла: 1.2 МБ (против 50+ МБ у спрайт-листов)
avatar.riv- Кастомизируемый аватар пользователя (3 артборда)
- Полное тело: голова, торс, конечности с одеждой
- Только голова: для панели и родительского приложения
- Бабочка компаньон: анимация награды
- Компонентная система: форма головы, волосы, глаза, одежда, аксессуары, цвета
- Размер файла: 2.4 МБ
- Кастомизируемый аватар пользователя (3 артборда)
coin-01.rivиcoins-01.riv- Анимации наград (монеты плавающие, собираемые)
- Одна монета: 150 КБ
- Несколько монет: 300 КБ
cute-monster-final.riv- Персонаж для обратной связи с несколькими эмоциональными состояниями
- Состояния: счастлив (правильный ответ), смущён (ошибка), задумчив (обработка), празднует (серия)
- Размер файла: 1.8 МБ
Оптимизация для Android
- Собственная сборка NDK (Rive NDK-r28) для 16КБ выравнивания страниц
- Уменьшает размер бинарника на 8% по сравнению со стандартной сборкой
- Обеспечивает совместимость с агрессивным управлением памятью в Android 12 и выше
Технический процесс синхронизации губ (lip-sync)
Шаг 1: Генерация TTS аудио и извлечение речевых меток
# 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' # Voice WaveNet
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Детская колонка
)
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)
# Ответ содержит:
# - аудиоданные в MP3
# - временные метки phoneme-level
return {
'audio': response.audio_content,
'speech_marks': response.timepoints
}
Шаг 2: Сопоставление фонем с состояниями рта в Rive
lip_sync_avatar.json содержит карту фонем арабского языка к положениям рта:
{
"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 } },
...
]
}
Шаг 3: LipSyncController управляет воспроизведением
// 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) {
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
audioPlayer.play(AudioSource.file(audioPath));
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
Шаг 4: RiveCharacterController управляет состояниями персонажа
// Управляет полным состоянием анимации персонажа (не только рот)
class RiveCharacterController extends GetxController {
// Состояния: idle → prepare → speaking → idle → error/celebration
void startExercise() {
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();
}
}
}
Система кастомизации аватара
Компонентная архитектура
Дети могут настраивать аватар, выбирая из частей:
{
"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"]
}
}
}
Матчинг названных 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 элементов
};
При выборе «круглая голова + синяя рубашка» приложение:
- Включает элемент Rive
Head_Round - Включает элемент
Shirt_Blue - Отключает остальные формы головы и цвета рубашек
- Персональный аватар ребёнка отображается во всех частях приложения
Почему мы выбрали Rive вместо альтернатив
| Функция | Rive | Lottie | Спрайт-листы | Видео |
|---|---|---|---|---|
| Машины состояний | ✓ | ✗ | ✗ | ✗ |
| Управление в реальном времени | ✓ (полное) | Частичное | Ручное | ✗ (пассивное) |
| Размер файла | 1-2 МБ | 2-3 МБ | 50+ МБ | 100+ МБ |
| Производительность | 60fps GPU | 30fps CPU | 60fps GPU | Переменная |
| Интерактивность | ✓ Полная | ✓ Частичная | ✓ Полная | ✗ Нет |
| Кривая обучения | Средняя | Лёгкая | Лёгкая | Лёгкая |
| Поддержка | Один .riv файл | Один JSON | Сотни изображений | Одно видео |
Победа за Rive благодаря программному управлению, поддержке машин состояний и компактности для мобильных приложений.
Оптимизация производительности
- Предзагрузка персонажей — загрузка .riv файлов при старте приложения, а не в каждом упражнении
- GPU-рендеринг — Rive автоматически использует аппаратное ускорение, с fallback на CPU на старых устройствах
- Повторное использование Rive контроллеров для сокращения пауз из-за сборщика мусора
- Файлы Rive уже сжаты — дополнительная компрессия не требуется
Результат: стабильная анимация с 60 fps на Snapdragon 662 и выше (средний сегмент 2019 года).
Часто задаваемые вопросы
В: Можно ли экспортировать анимации из Adobe Animate в Rive?
О: Нет напрямую. Мы используем нативный редактор Rive (rive.app). Аниматоры создают персонажей, экспортируют .riv и затем интегрируют в приложение Flutter.
В: Как вы учитываете разные типы телосложения и особенности детей?
О: В системе кастомизации есть варианты телосложения (стройное, атлетическое, круглое) и аксессуары (очки, слуховые аппараты, средства передвижения), обеспечивая представительство для всех детей.
В: Что если ребёнку не понравится его аватар?
О: Можно менять его в любое время. Приложение не навязывает внешний вид — дети имеют полный творческий контроль.



