React Native'e animasyonlu maskot: Wan 2.2 klibi, WebP ile uygulamada
Raku'nun maskotunu, elimdeki 3D görselden Wan 2.2 ile ürettiğim kliplerle canlandırdım. Klipler alfa kanallı animasyonlu WebP olarak uygulamaya giriyor, expo-image oynatıyor. Sekiz ifadenin en küçük boyutları toplam 4,7 MB tutuyor.
Bu yol herkese uymuyor. Maskotun parmağını izleyen bir gözü ya da konuşurken oynayan bir ağzı olacaksa Rive hâlâ doğru araç. Ben yalnızca "ders bitti, doğru cevap, yanlış cevap" gibi anlarda bir ifade klibi oynatıyorum ve bunun için video modelinin çıktısı yetti.
Raku ve maskot
Raku, ortak geliştirdiğimiz bir mikro-öğrenme uygulaması. Okuma, mini test ve seri derken maskot ekranın büyük kısmında duruyor. Karşılamada, düşünürken, ders sonucunda ve hata ekranında o var. Sekiz ifadesi idle, thinking, happy, celebrate, angry, surprised, sad ve wave. Uygulama hâlâ geliştirme aşamasında.
Önce Rive'ı denedim
Duolingo tarzı maskot arayınca karşıma hep aynı tavsiye çıkıyor. Bir dev.to yazısı bunu açıkça söylüyor, "The answer is: You don't use Lottie. You use Rive." Gerekçesi de "But the real magic is the built-in State Machine." cümlesinde.
Haklı da. Durum makinesi, bir mood değerini değiştirip maskotu ifadeler arasında geçirmenin temiz yolu. Kurdum. @rive-app/react-native paketini ve Nitro modülünü ekledim, RakuMascot bileşenine mood, talking ve tap, correct, wrong, blink tetikleyicileriyle bir sözleşme yazdım. Rive'ın React Native belgesi en az React Native 0.78 ve Nitro Modules istiyor, Raku'nun Expo 57 tabanı bunu karşılıyordu.
Eksik olan .riv dosyasıydı. Rig çıkaracak, animasyonu verecek biri gerekiyor ve o kişi bende yoktu. Ben de kısayol aradım. 3D maskotu düz 2D'ye çevirdim, kafa, kollar ve kuyruk diye katmanlara böldüm, kodla oynattım. Kötüydü sonuç. Maskot 3D hâlinden bir anda çok geride kalmıştı.
Beğenmedim ve bıraktım.
Hatam stili değiştirirken kaliteyi ölçmemekti. Yan yana koyup bakmam gerekiyordu, koymadım.
Rive tarafı yine de silinmedi. RakuMascot önce klibe bakıyor, klip yoksa Rive kaynağına, o da yoksa sabit PNG'ye düşüyor.
Adım 1: Kaynak görsel
Klibin ilk karesi mevcut 3D Raku görseli, 640 x 640, düz krem bir zeminde (246, 231, 201), ve zemini düz tuttum, çünkü sonraki adımda maskeyle kesiliyor.
İfadeye göre ayrı kaynak kare kullandım, src_happy, src_sad, src_thinking gibi. Model hareketi buradan çıkarıyor, o yüzden kaynak karedeki poz ifadeye zaten yakın olmalı.
Adım 2: Wan 2.2 ile klip
Wan 2.2 görüntüden videoya 14B modelini ComfyUI'da koşuyorum. İki model dosyası var, wan2.2_i2v_high_noise_14B_fp8_scaled ve wan2.2_i2v_low_noise_14B_fp8_scaled. Her birine lightx2v'nin 4 adımlık LoRA'sını bağlıyorum. Metin kodlayıcı umt5_xxl_fp8_e4m3fn_scaled, VAE wan_2.1_vae.
WanImageToVideo düğümü 640 x 640 ve 81 kare üretiyor. Ardından iki KSamplerAdvanced geliyor. İlki 0 ile 2 arasındaki adımları high noise modeliyle, ikincisi kalanı low noise modeliyle yapıyor. İkisinde euler, simple ve cfg 1.0 var, ModelSamplingSD3 shift değeri 5.0. Çıkışı VAEDecodeTiled ile 512'lik karolarda çözüp 16 fps'te videoya çeviriyorum. ComfyUI'yı --reserve-vram 1.8 --cache-none --disable-pinned-memory ile açıyorum, 12 GB'lık kartta bellek bununla yetiyor.
Süre 12 GB'lık kartımda klip başına 120 ile 145 saniye. Rakamlar benim donanımımda ve benim ayarlarımda ölçüldü, başka kartta başka çıkar.
İstem
İstemi iki parçaya ayırdım. İlk parça bütün ifadelerde aynı kalıyor ve karakteri, stili, zemini sabitliyor.
A cute 3D animated raccoon mascot in a yellow hoodie with a navy backpack, Pixar style, plain cream background, static camera, full body stays in frame.
İkinci parça ifadeyi anlatıyor. Düşünme klibinde "he taps his chin with his finger, looks up to the side with curious eyes, tilts his head" gibi sırayla olan hareketler yazılı. Yanlış cevap tepkisinin istemi de aynı kalıpta, "he winces, scrunches his face with a cute oops expression, shrugs his shoulders and scratches his head".
Doğru ve yanlış cevap tepkileri 33 kare, yani iki saniye. Bunlar 45 saniyede bitiyor.
Takıldığım iki yer
WanFirstLastFrame düğümü VAE kodlama aşamasında access violation verdi. İlk ve son kareyi vermek istemiştim, WanImageToVideo'ya dönünce sorun bitti.
İkincisi bellek. Aynı anda başka bir proje için ComfyUI açıkken Wan işi hostbuf_file_reader_read failed diye düşüyordu. Çözüm betiğin başına bir bekleme döngüsü koymak oldu. Başka bir main.py süreci çalışıyorsa dakikada bir kontrol edip bekliyor, bitince kendi ComfyUI'sını açıyor.
Çok beğendim klipleri ilk gördüğümde.
Adım 3: Alfa maskesi
Klibin zemini krem, uygulamada ise maskot kartın ya da sayfanın üstünde duracağı için zemini atmam gerekiyor, o yüzden her kareyi BiRefNet'ten geçirip bir maske PNG dizisi çıkarıyorum. Klip başına 27 ile 33 saniye sürdü.
Şaşkın ifadesi ilk denemede zayıf çıktı. Dört varyant ürettim. b ile c aynı istemin farklı seed'leri, d'de kaynak kareyi ve cümleyi değiştirdim. Henüz hangisini koyacağıma karar vermedim.
Adım 4: Üç boyutta WebP
Maske dizisini ffmpeg'e alphamerge ile veriyorum. Aşağıdaki filtre rengi ve maskeyi aynı boyuta ve kare hızına getirip birleştiriyor.
[0:v]scale=320:320:force_original_aspect_ratio=decrease:flags=lanczos,fps=10,format=rgb24[c];[1:v]scale=320:320:force_original_aspect_ratio=decrease:flags=lanczos,fps=10,format=gray[m];[c][m]alphamerge,format=yuva420p[o]
Çıkış libwebp_anim ile kalite 55, sıkıştırma seviyesi 6, sonsuz döngü. Aynı betik her klibi üç katmana kodluyor.
| Katman | Boyut | Kare hızı | idle klibi |
|---|---|---|---|
| low | 320 px | 10 fps | 605 KB |
| mid | 480 px | 15 fps | 1,49 MB |
| high | 720 px | 20 fps | 2,63 MB |
Sekiz ifadenin low sürümleri uygulamaya gömülü, toplam 4,7 MB. Mid ve high henüz dışarıda. İleride uzak sunucudan indirmeyi düşünüyorum ama bağlamadım.
Adım 5: Uygulamada oynatma
Klip animasyonlu WebP olduğu için expo-image'ın Image bileşeni yetiyor, video oynatıcı ya da özel modül gerekmiyor. Expo belgesinde autoplay için şu yazıyor, "Determines if an image should automatically begin playing if it is an animated image."
Hangi katmanın seçileceğine cihazın belleği, yılı ve ekranı karar veriyor.
export function computeRakuTier(
totalMemory: number | null,
yearClass: number | null,
screenPx: number,
): RakuTier {
const old = yearClass !== null && yearClass <= OLD_DEVICE_YEAR_CLASS;
if ((totalMemory !== null && totalMemory < LOW_MEMORY_BYTES) || old) return 'low';
if (totalMemory !== null && totalMemory >= HIGH_MEMORY_BYTES && screenPx >= HIGH_SCREEN_PX) return 'high';
return 'mid';
}
3,5 GB altı bellek ya da 2018 ve öncesi cihaz sınıfı low alıyor. 5,5 GB ve üstü bellek ile en az 1080 piksel ekran genişliği high alıyor, geri kalan mid. İstenen katman yoksa resolveRakuClip bir aşağıdakine düşüyor. Şu an elimde yalnız low olduğu için her cihaz low görüyor.
İfadeler arası geçişte 200 milisaniyelik bir çapraz solma var, yoksa klip değişince maskot zıplıyor gibi görünüyor. Ders sonucunda ifade puana göre seçiliyor. Yüzde 75 ve üstü celebrate, yüzde 50 ve üstü happy, altı sad.
Görünmeyen klip oynamasın
Onboarding'de yan yana slaytlar var ve her birinde maskot duruyor. Ekranda görünmeyen slaytın klibi de oynamaya devam ediyordu. RakuMascot'a bir paused özelliği ekledim ve Image'a autoplay={!paused} verdim. Etkin olmayan slayt artık durağan.
Bunun CPU'ya etkisini ölçmedim, yalnızca gereksiz işi kestim.
Ölçmek sırada.
iOS'ta henüz denemedim
Burası açık.
Expo belgesi iOS için Apple'ın varsayılan kodeğinin hızlı olduğunu ve daha az bellek kullandığını söylüyor, ama "may render animated images with incorrect blending or play them at the wrong framerate" diye ekliyor. Ardından, "Some animated WebP files also decode very slowly with Apple's codec."
Alfa kanallı animasyonlu WebP tam da bu duruma giriyor. Ben yalnızca Android emülatöründe baktım. iOS'ta görüntü bozuk çıkarsa belgenin anlattığı alternatif kodeğe geçmem gerekecek, ona bakmadım.
Bu yöntem ne zaman işe yaramaz
Maskotun kullanıcının dokunuşuna ya da yazdığı metne tepki vermesi gerekiyorsa işe yaramaz. Klip sabit bir hareket, bir yere bakamaz. Ağız hareketi sesle eşleşmeliyse de olmaz, burada konuşma yok.
İfade sayısı çok artacaksa da zorlanırsın. Her yeni ifade bir Wan koşusu (2 dakika civarı), bir alfa koşusu (yarım dakika) ve üç kodlama demek. Sekizde sorun çıkmaz. Yirmide dönüp bakmak gerekir.
Son olarak, elinde iyi bir 3D kaynak görsel yoksa bu hat işe başlamıyor bile, çünkü ben hazır bir 3D Raku görseliyle başladım ve model hareketi o görselin üstüne kuruyor.
Şimdi nerede
Sekiz ifade uygulamada ve low katmanıyla çalışıyor, sırada şaşkın klibini seçmek, mid ve high katmanlarını dışarıdan indirmek ve iOS'ta alfa kanallı WebP'nin gerçekten nasıl oynadığına bakmak var. Doğru ve yanlış anları için iki kısa klip de üretildi, henüz ekrana bağlamadım.