تستخدم Amal أداة Rive (المعروفة سابقاً بـ Flare) لكل عمليات تحريك الشخصيات — بما في ذلك تزامن الشفاه، تخصيص الصور الرمزية، ردود الأفعال، وشخصيات الألعاب. اخترنا Rive بدلاً من Lottie أو أوراق الرسوم المتحركة لأنها تدعم الآلات الحالة وقت التنفيذ، التلاعب البرمجي، ونظام الرسوم المعجل بواسطة GPU بسرعة 60 إطار/ثانية، كل ذلك في ملف واحد مدمج لكل شخصية.
مكتبة أصول الرسوم المتحركة
الشخصيات الأساسية
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 ميجابايت
أنظمة الأمثل لمنصة أندرويد
- إصدار NDK مخصص (Rive NDK-r28) للامتثال لمحاذاة الصفحات 16KB
- يقلل حجم السجل الثنائي بنسبة 8% مقابل الإصدار القياسي
- يضمن التوافق مع إدارة الذاكرة المتقدمة في أندرويد 12+
أنابيب تزامن الشفاه (تعمق تقني)
الخطوة 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' # WaveNet voice
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # Child's speaker
)
# طلب علامات الكلام (توقيت الصوتيات)
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
# - النقاط الزمنية: [{character, byte_pos, time_ms}, ...]
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) {
// الخطوة 1: تحميل شخصية Rive
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// الخطوة 2: تحميل علامات الكلام من مخرجات TTS
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// الخطوة 3: تشغيل الصوت مع تحريك الفم
audioPlayer.play(AudioSource.file(audioPath));
// الخطوة 4: تحديث وضع الفم في كل إطار صوتي
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 {
States: 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+ element mappings
};
عندما يختار الطفل "رأس دائري + قميص أزرق"، يقوم التطبيق:
- بتفعيل العنصر
Head_Roundفي Rive - بتفعيل العنصر
Shirt_Blueفي Rive - بتعطيل جميع أشكال الرأس وألوان القمصان الأخرى
- تظهر الصورة الرمزية المخصصة للطفل الآن في كامل التطبيق
لماذا Rive بدلاً من البدائل؟
| الميزة | Rive | Lottie | أوراق الرسوم المتحركة | الفيديو |
|---|---|---|---|---|
| الآلات الحالة | ✓ | ✗ | ✗ | ✗ |
| التحكم في وقت التشغيل | ✓ (كامل) | جزئي | يدوي | ✗ (سلبي) |
| حجم الملف | 1-2 ميجابايت | 2-3 ميجابايت | 50+ ميجابايت | 100+ ميجابايت |
| الأداء | 60fps GPU | 30fps CPU | 60fps GPU | متغير |
| التفاعل | ✓ كامل | ✓ جزئي | ✓ كامل | ✗ لا يوجد |
| منحنى التعلم | متوسط | سهل | سهل | سهل |
| الصيانة | ملف .riv واحد | ملف JSON واحد | مئات من الصور | فيديو واحد |
الأفضلية لـ Rive لأننا نحتاج للتحكم البرمجي، الآلات الحالة، والدمج الملفي لتطبيق الهاتف المحمول.
تحسين الأداء
- تحميل مسبق للشخصيات: تحميل ملفات
.rivأثناء بدء التطبيق، وليس لكل تمرين - الرسوم المتحركة بواسطة GPU: يستخدم Rive تلقائياً GPU إذا كان متاحاً، والاعتماد على CPU للأجهزة القديمة
- تخصيص الذاكرة: إعادة استخدام وحدات التحكم Rive عبر الشاشات لتجنب توقفات جمع القمامة
- الضغط: ملفات Rive مضغوطة بالفعل؛ لا حاجة لتحسين إضافي
النتيجة: رسوم متحركة بسرعة 60 إطار/ثانية على Snapdragon 662+ (هواتف الفئة المتوسطة 2019).
الأسئلة الشائعة
س: هل يمكنني تصدير الرسوم المتحركة من Adobe Animate إلى Rive؟
ج: ليس بشكل مباشر. نستخدم محرر Rive الأصلي (rive.app). المصممون يصممون في Rive، وليس Animate أو After Effects. تدفق العمل هو: تصميم الشخصية في Rive → التصدير كـ .riv → الدمج في تطبيق Flutter.
س: كيف تعاملتم مع أنواع الأجسام المختلفة أو الإعاقات؟
ج: تخصيص الصور الرمزية يتضمن خيارات لأنواع الجسم (نحيفة، رياضية، دائرية) وملحقات (نظارات، مساعدات سمعية، مساعدات حركية). يضمن هذا رؤية جميع الأطفال للتمثيل.
س: ماذا إذا لم يعجب الطفل في صورتهم الرمزية؟
ج: يمكنهم التخصيص في أي وقت. التطبيق لا يفرض شكل معين — الأطفال لديهم سيطرة إبداعية كاملة.



