הדגמות AI נמצאות בכל מקום. סקריפט Python בודד יכול להחליף פנים בתמונה סטטית, והתוצאה נראית קסומה. אבל לבנות משהו שאנשים אמיתיים באמת יכולים להעלות אליו קבצים, להתרחק ממנו ולחזור אליו? זו עבודה שונה לחלוטין. לאחרונה בניתי כלי אינטרנט שמקבל GIF מונפש ופנים למקור, ואז מחזיר את אותה אנימציה עם הפנים מוחלפות בכל פריים. המודל עושה את רוב העבודה הוויזואלית הכבדה, אך המאמץ ההנדסי האמיתי הושקע בארכיטקטורה שמקיפה אותו: שמירה על חיבורים פעילים, עמידה ברענוני דפים, ווידוא שתהליך inference של שתי דקות לא ייעלם בתוך שגיאת 504 Gateway Timeout.
זהו הפער בין אב-טיפוס למוצר. מהנדסים אוהבים לדבר על ארכיטקטורות diffusion ופרמטרים של inference. אבל כשמשתמש מעלה GIF עמוס של מאתיים פרימים, לאף אחד לא אכפת מהמודל שלך אם לשונית הדפדפן נכנעת אחרי שלושים שניות. התשתית שמסביב ל-inference חשובה בדיוק כמו ה-inference עצמו.
הבעיה עם ההמתנה
ארכיטקטורת אינטרנט סטנדרטית מניחה תגובות מהירות. משתמש לוחץ על כפתור, השרת עונה, והדף מתעדכן. החלפת פנים לאורך עשרות פרימים ב-GIF שוברת את ההנחה הזו מיד. המודל רץ על ה-GPUs של Replicate, לא על השרת שלי, ו-GIF ארוך יכול בקלות לקחת דקה או יותר לעיבוד. אם תנסה להשאיר בקשת HTTP פתוחה כל כך זמן, אתה מחפש צרות. Load balancers מנתקים חיבורים לא פעילים. דפדפנים מניחים שהרשת נכשלה ומנסים שוב. משתמשים בוהים בספינר קפוא ומניחים שהאפליקציה שבורה.
נמנעתי מזה לחלוטין על ידי ניתוק (decoupling) בין השליחה לבין התוצאה. כשמשתמש מעלה GIF ותמונת פנים, ה-backend ב-Next.js שלי מתחיל prediction ב-Replicate ומחזיר מיידית job ID. המשתמש מקבל אישור מיידי. התוצאה האמיתית מגיעה מאוחר יותר דרך webhook ברגע שעבודת ה-GPU מסתיימת. התבנית הזו אינה יוצאת דופן, אך עבור משימות מדיה ארוכות זמן היא חיונית לחלוטין. היא הופכת המתנה בלתי צפויה ל"לחיצת יד" בטוחה: המשימה התקבלה, ותקבל הודעה כשהיא תסתיים.
בניית State Machine שניתן לסמוך עליה
ברגע שעוברים לעבודה אסינכרונית, צריך נראות (visibility). משתמשים ירעננו את הדף. הם יסגרו את הלשונית ויפתחו אותה מחדש. הם יעתיקו את הקישור וישלחו אותו לעמית שיבדוק אותו שעתיים מאוחר יותר. ללא תיעוד עמיד של מה שקרה, יש לך כאוס.
השתמשתי ב-Supabase כמקור האמת היחיד (single source of truth). כל העלאה יוצרת שורה עם job ID ייחודי, והשורה הזו עוברת דרך מצבים (states) ספציפיים: queued, processing, succeeded, failed, או expired. כשהמשתמש לוחץ לראשונה על submit, השורה עוברת ל-queued. ברגע ש-Replicate מקבל את ה-prediction, היא עוברת ל-processing. ה-webhook מעביר אותה ל-succeeded או failed. הוספתי את המצב expired עבור משימות שנשארות זמן רב מדי ללא callback, כדי שהמערכת לא תרדוף אחרי רוחות רפאים לנצח.
ה-frontend, שנבנה ב-TypeScript, מבצע polling ל-Supabase במרווחים קצרים ומציג את מה שהמצב הנוכחי אומר. ה-polling הזה נראה פרימיטיבי, אבל הוא פותר את בעיית הרענון לחלוטין. משתמש יכול לסגור את הלפטופ, לפתוח אותו מחר, ולראות בדיוק היכן הדברים עומדים, כי מסד הנתונים — ולא זיכרון הדפדפן — הוא זה שמנהל את ההתקדמות. Supabase גם עוקב אחר קרדיטים, כך שהחשבונאות נשארת קשורה לאותה רשומת משימה שעוקבת אחר המצב. הכל נמצא במקום אחד.
טיפול בקבצים מבלי להכריע את הדפדפן
קבצי GIF כבדים יותר ממה שאנשים חושבים. קובץ שגודלו מושלם עבור ה-AI pipeline עשוי עדיין לשקול מספר מגה-בייטים. רינדור של הקובץ כתצוגה מקדימה בלופ בתוך הדפדפן יהרוס את הביצועים, במיוחד במכשירים חלשים יותר. נדרשו לי שני צינורות (pipelines) קבצים נפרדים לחלוטין: אחד עבור ה-AI, ואחד עבור ממשק המשתמש.
עבור תצוגות מקדימות, אני ממיר GIFs גדולים ל-WebP מונפש באמצעות FFmpeg שקומפל ל-WebAssembly. זה רץ כולו בצד הלקוח (client-side) בתוך הדפדפן. התוצאה היא תצוגה מקדימה קלה ששומרת על ממשק מהיר מבלי לגעת בקובץ המקורי. ה-GIF שנשלח ל-Replicate נשאר ללא שינוי. ההפרדה הזו חשובה. אתה לא רוצה שארטיפקטים של דחיסה מתצוגה מקדימה יחדרו לנתוני האימון או לרינדור הסופי שלך, ואתה לא רוצה שממשק המשתמש ייתקע על גוש (blob) של מספר מגה-בייטים בזמן שה-AI עדיין חושב.
FFmpeg WASM מטפל גם במשימות GIF אחרות לפני שההעלאה עוזבת את הדפדפן. אני משתמש בו כדי לנתח (parse) את מספר הפרימים, לאמת מימדים ולזהות קבצים פגומים בשלב מוקדם. זיהוי בעיה לפני שהיא צורכת קרדיטים של GPU חוסך גם כסף וגם את סבלנות המשתמש.
להפוך דמו לתוכנה שאנשים סומכים עליה
יש כאן לקח רחב יותר שחל על כמעט כל כלי generative AI בשוק. המודל עצמו עשוי להוות שלושים אחוזים מהעבודה. שבעים האחוזים הנותרים הם ה"צנרת" הלא נוצצת שאף אחד לא מצייץ עליה: שחזור מצב (state recovery), חתימות webhook, המרת קבצים, מעקב קרדיטים וניהול כשלים בצורה אלגנטית (graceful failure).
ה-stack שלי פשוט ומכוון מטרה. Next.js ו-TypeScript מטפלים בממשק ובנתיבי ה-API. Replicate מריץ את המודלים. Supabase מנהלת מצבים (states), נתונים וקרדיטים. FFmpeg WASM מטפל בעבודת מדיה בצד הלקוח. WebP מונפש שומר על ממשק המשתמש (UI) מהיר. לכל חלק יש תפקיד אחד, והם מתחברים באמצעות מעברי מצב (state transitions) מפורשים במקום באמצעות בקשות שבירות וארוכות טווח.
כשמשתמש מחליף קרדיטים בהחלפת פנים (face swap), הוא מצפה לאמינות, לא מאמר מחקרי. אם תחזית נכשלת, המערכת צריכה לדעת ולדווח על כך. אם המשתמש ממתין, הוא צריך לקבל תצוגה מקדימה קלה לצפייה וסטטוס ששורד הפעלה מחדש של הדפדפן. הפרטים האלו בלתי נראים כשהם עובדים, אך קריטיים כשהם לא.
החלפת הפנים עצמה היא טריק נחמד. אבל הכלי מרגיש אמיתי רק בגלל שמשתמש יכול להעלות קובץ, לעזוב ולחזור מבלי לאבד את מקומו. זה מה שהופך קריאת API לתוכנה שאנשים באמת סומכים עליה.
