Главная / Блог / Генератор выполняет направление, но не величину: 48 кадров

Генератор выполняет направление, но не величину: 48 кадров

Я сгенерировал 48 кадров своего лица в ChatGPT. В продакшен ушло 11 — в сумме 46 килобайт.

На главной странице kishkin.dev стоит моя фотография в чертёжной рамке. До недавнего времени это была просто фотография. Теперь аватар следит за курсором, моргает — и улыбается, когда вы тянетесь к кнопке. Одно фото с телефона весит в сто раз больше, чем весь эффект.

По дороге я построил и выбросил семь направлений — трёхмерную сетку, карту глубины, оптический поток. И упёрся в предел, который не обойти: генератор выполняет «посмотри вбок», но не выполняет «насколько». Ниже — измеренная цифра под каждым из этих решений.

Коротко. Портрет на главной следит за курсором, моргает и улыбается, а стоит странице 7 398 байт в двух запросах на загрузке; остальные 38 930 приходят, только когда сдвинулась мышь. По дороге я построил и выбросил семь направлений: трёхмерную сетку, карту глубины, оптический поток, подвижный свет, плавный переход между взглядами. Главное, за что я заплатил двумя днями: генератор выполняет «посмотри вбок», но не выполняет «насколько» — на дословно одинаковом промпте амплитуда взгляда разлеталась от 94 до 130 % внутри одной пачки.

ЛИСТ · АВТОПОРТРЕТ

REF DK-01

Dmytro Kishkin
ИМЯD. KISHKIN
РОЛЬTECH LEAD
REFDK-01

Это не снимок. Это тот самый блок, что стоит на главной, — наведите курсор.


Сколько это стоило: 33 промпта, 48 кадров, 46 КБ

Эффект стоил 33 написанных промпта и 48 сгенерированных кадров, из которых в продакшен ушло 11. Всё сгенерировано в ChatGPT images 2.0 — это важно, потому что всё, что ниже про амплитуду взгляда, относится именно к этому движку и именно к этой версии. Вместе они весят 46 328 байт. Каждый кадр — это моё лицо, сгенерированное из одной реальной фотографии: тот же человек, тот же ракурс, изменены только глаза или только рот.

Показатель Значение
Написано промптов 33
Сгенерировано кадров (ChatGPT images 2.0) 48
Ушло в продакшен 11
Весит всё вместе 46 328 байт

Из 48 попыток выжили восемь направлений взгляда, два положения век и одна улыбка.

Остальные 37 — это и есть работа. Не черновики, а проверенные и закрытые гипотезы.


Ложный путь: полторы недели на перетекание фото в маскота

Одиннадцать прототипов подряд разбились об одну цифру, которую я измерил в самом начале. До этого плита имела два состояния: фотография и рисованный маскот, и портрет должен был «перетекать» в иллюстрацию.

Голова маскота на 7 % шире моей на фото, а плечи — на 10 % уже. Ни один трансформ не совпадает с обоими одновременно.

Дальше был список изобретений, чтобы это расхождение спрятать: переворот плитки, размытие, выносные подписи, подмена фона, стопка листов, окошко-лупа, WebGL, флип карточки.

Паттерн везде одинаковый:

Усилия уходили на сокрытие несоответствия. А сокрытие всегда читается дёшево.

Разворот был простой и бесплатный. Не делать вторую картинку. Взять одну фотографию и заставить её реагировать. Прятать ничего не нужно — потому что прятать нечего.

Разворот того же рода я уже делал с внешним диском: отказался от NTFS-драйвера в macOS вместо того, чтобы искать понадёжнее.

Семь подходов, которые я построил и выбросил: 3D-сетка, глубина, оптический поток

Каждый из семи был живым прототипом, а не идеей на салфетке. Трёхмерная сетка на 16 тысяч точек работала и оказалась избыточной. Карта глубины дала 5,29 пикселя движения на голове, у которой на экране 102. Оптический поток нашёл 0,2 пикселя там, где глаз прошёл 10. Вместо всего этого остался параллакс на чистом CSS.

Фотосессия со штативом. Технически самый правильный путь: 20 минут, штатив, серия кадров «глаза влево / вправо / вверх / вниз». Я даже расписал себе порядок кадров — он до сих пор лежит в репозитории. Отброшено, потому что я не сниму себя хорошо — и это нормальная причина.

Трёхмерная модель лица. Three.js, настоящая сетка на 16 тысяч точек. Прототип работал. Отброшено как избыточное.

Карта глубины. Самое дорогое из выброшенного: я построил её целиком, с отдельным стендом, чтобы увидеть эффект в движении. Посчитал честно: 5,29 пикселя движения «нос против уха» на голове, у которой на экране 102 пикселя. За эти пять пикселей пришлось бы платить WebGL, запасным путём и перекалибровкой каждый раз, когда сменится фотография.

Подвижный свет. Выглядело так, будто в лицо светят фонариком. Отброшено на глаз за десять секунд.

Плавный переход между взглядами. Кажется очевидным — и не работает в принципе. Между «смотрит прямо» и «смотрит вбок» нет промежуточного кадра. Если смешать два кадра пополам, тёмная радужка одного ложится на белый белок другого, и глаз становится серым, как при катаракте.

Оптический поток. Вторая попытка сделать промежуточный кадр, математически. Алгоритм нашёл 0,2 пикселя движения там, где глаз прошёл 10: тёмная радужка, едущая по гладкому белку, для него просто невидима.

Промежуточная амплитуда из генератора. Об этом — отдельно, ниже.

Вместо глубины остался чистый CSS: стена позади движется на 25 пикселей, портрет — на 5. Двадцать пикселей параллакса за ноль новых файлов.


Можно ли задать ChatGPT точный угол взгляда?

Нет. Генератор выполняет «посмотри вбок», но не выполняет «насколько». Я просил половину от максимума пятью разными формулировками: «около 18 градусов» дало взгляд до упора, «на четверть ширины радужки» — 81 %, координаты в сантиметрах и пикселях — 117 и 124 %. Формулировка, которая попала в 74 %, на повторе дала 111 % с разбросом от 94 до 130 % — внутри одной пачки, на дословно одинаковом тексте.

Как просил Что пришло
«около 18 градусов» до упора
«на четверть ширины радужки» 81 % от максимума
точка в 15 см от объектива + сдвиг в пикселях 117 %
то же самое, с более широкого кропа 124 % — и начало перерисовывать волосы и челюсть
«он смотрит в ваш второй глаз» 74 % — попал!

Последнее было похоже на рецепт. Я расписал его на все восемь направлений и запустил пачкой — и получил 111 %, с разбросом от 94 до 130 %, внутри одной пачки, на дословно одинаковой формулировке.

То есть 74 % были счастливым жребием внутри собственного разброса модели, а не ответом на мои слова.

Вывод, за который я заплатил двумя днями: направление генератор отдаёт, дозировку — нет. Вопрос закрыт, обходного пути нет.


Как это работает: спрайт глаз 201 × 114 поверх одного фото

Держать одиннадцать полных версий фотографии стоило бы 360 килобайт и 25 мегабайт памяти браузера — ради эффекта в аватарке. Вместо этого каждый кадр обрезан до одних глаз: прямоугольник 201 × 114 пикселей, который ложится поверх основного фото, как наклейка. Лицо, волосы, рубашка и стена загружаются один раз. Рот — вторая, независимая наклейка на 6,8 КБ.

Fig. 01 · Компас взгляда — восемь кадров и покой kishkin.dev
  • Глаза сведены вверх и влево: радужки во внешних верхних углах, под ними видны белки

    Вверх-влево 216°

  • Глаза подняты вверх почти без бокового сдвига: радужки под верхними веками

    Вверх 274°

  • Глаза сведены вверх и вправо: радужки у верхних правых краёв век

    Вверх-вправо 332°

  • Взгляд строго влево: обе радужки прижаты к левым углам глаз, белки открыты справа

    Влево 179°

  • Базовый кадр: глаза смотрят прямо в камеру, радужки посередине век

    Покой —

  • Взгляд строго вправо: обе радужки у правых углов глаз, белки открыты слева

    Вправо 354°

  • Глаза опущены вниз и влево: радужки в нижних левых углах, верхние веки слегка надвинуты

    Вниз-влево 139°

  • Глаза опущены прямо вниз: радужки у нижних век, взгляд ниже линии камеры

    Вниз 90°

  • Глаза опущены вниз и вправо: радужки в нижних правых углах век

    Вниз-вправо 47°

Угол под каждым кадром — не тот, что просили в промпте, а измеренный по смещению радужки на уже собранном ассете. Поэтому компас ищет ближайшее из этих девяти настоящих состояний, а не делит круг на равные секторы.

Это не ровный восьмилучевой компас, хотя я просил именно такой. Диагонали завалены к горизонтали: на «вверх и влево» генератор отдаёт по большей части «влево». Я измерил все 45 сгенерированных кадров и назвал их второй раз, независимо от первого, — оба раза совпали. Выбор лучших из этих 45 вместо первых попавшихся сбил среднюю ошибку с 7,2° до 4,3°. Одно направление, «вверх-вправо», так и осталось с ошибкой 15°: в наборе просто нет кадра, который его обслуживает.

Fig. 02 · Веки и улыбка — отдельные слои kishkin.dev
  • Портрет в покое: глаза открыты, рот в базовом положении

    Покой —

    Базовая плита. Ни одной заплатки сверху.

  • Портрет с полуприкрытыми веками — средний кадр моргания

    Полуприкрытые 86 / 88 %

    Апертура век. Только в покое.

  • Портрет с закрытыми глазами — нижний кадр моргания

    Закрытые 66 / 62 %

    Апертура век. Без подмигивания.

  • Портрет улыбается: более широкая улыбка с сохранённой щелью между передними зубами

    Улыбка 6,8 КБ

    Вторая заплатка со своей маской.

Глаза и рот — два независимых слоя над двумя независимыми участками, поэтому портрет улыбается, не переставая следить за курсором.

В этом и суть разделения на два слоя: слежение само по себе — это внимание, а слежение плюс улыбка — это уже приветствие.

Три случая, когда метрика сказала «да», а глаз — «нет»

Автоматическая проверка трижды выбрала кадр, который на экране выглядел неправильно. Она путала опускание века с движением зрачка, поэтому кадр с прищуренным глазом получал идеальный угол. Полоса прокрутки шириной 15 пикселей ломала взгляд только вправо. А шов через всё лицо нашли рецензенты, не я.

Человек, который щурился. Автоматическая проверка выбрала кадр «взгляд вправо» по идеальному углу — 1 градус от идеала. На экране это был человек, который щурится и едва смотрит вбок. Оказалось, проверка приняла опускание века за движение зрачка и подарила кадру ход, которого не было. Я трижды отбрасывал этот кадр на глаз, прежде чем додумался, что мерить надо другим методом.

Взгляд, который срывался только вправо. Симптом был конкретный: взгляд возвращался в центр каждый раз, когда я вёл курсор вправо. Причина — полоса прокрутки. Страница на 15 пикселей уже окна, курсор пересекает эту полоску, браузер решает, что мышь ушла со страницы, и глаза возвращаются в центр. Слева, сверху и снизу такой полоски нет — дефект мог появиться только справа.

Шов, которого я не видел, и чужие зубы. Я собрал улыбку и проверил её, увеличив втрое. Чисто. Тогда дал четырём рецензентам вслепую, не сказав, что есть что. Трое нашли одно и то же: жёсткий шов через всё лицо на уровне ноздрей — он шёл вдоль естественной тени под носом, поэтому я его и не заметил. А ещё они нашли то, чего не видел никто два дня: в предыдущем варианте улыбки были чужие зубы — ровные, белее, без моей настоящей щели.

Метрика сказала «да», глаз сказал «нет». Глаз прав.

Сколько весит на загрузке: 7,4 КБ вместо 46

На загрузке страница берёт 7 398 байт в двух запросах вместо 46 328 в одиннадцати. Остальное — 38 930 байт в девяти запросах — приходит только тогда, когда сдвинулась мышь. Портрет моргает независимо от того, пришёл ли курсор, но восемь кадров взгляда мертвы, пока мышь не сдвинется, а улыбка — пока кто-нибудь не потянется к ссылке.

На загрузке Потом, по необходимости
Было 46 328 байт, 11 запросов —
Стало 7 398 байт, 2 запроса 38 930 байт, 9 запросов

Посетитель с клавиатуры не тянет ни одного кадра взгляда. Телефон берёт только пару для моргания. Кто включил «уменьшить движение» (prefers-reduced-motion) — не берёт ничего.

Первое моргание: 0,7 секунды вместо 5,6

Первое моргание бралось из того же генератора случайных пауз, что и все остальные, — от 2,6 до 6,8 секунды. То есть увидит ли посетитель главный эффект вообще — решал бросок кубика в тот единственный момент, когда это значило больше всего.

Последнее, что оставалось, я сформулировал себе одним предложением: моргание должно случиться, пока человек ещё смотрит на портрет, а не когда он уже листает дальше.

Измерил на продакшене: кадры готовы на 0,5 секунды, страница догружается на 0,8, а первое моргание падало на 5,6 секунды. К этому времени человек уже прочитал заголовок и ушёл.

Теперь — 0,7 секунды после появления век. Один раз, не случайно. Дальше обычный ритм.


Пять выводов, которые переносятся на другие задачи

1. Работой был отсев, а не сборка. Отсеивалось и «как», и «что». Я хотел эффект, который заметят, и не был уверен, что сделаю такой сам, — поэтому сначала искал готовые библиотеки, примеры, плавные переходы одного изображения в другое. Семь направлений построено и выброшено, 37 кадров из 48 не пошли никуда. А задуманное оказалось проще и легче любого готового решения: 46 килобайт, без WebGL, без перекалибровки — и стоило оно ровно столько, сколько стоило закрыть остальное. Так же я считаю стоимость решений и на большем масштабе — в обзоре проектов та же арифметика записана в тысячах сайтов и миллионах инсталляций.

2. Смотрите в том масштабе, в котором это увидит человек. Плита на экране — 358 пикселей, глаз — 22. Дефект при трёхкратном увеличении и дефект в жизни — это два разных списка.

3. Ищите край, где среда устроена иначе. Взгляд срывался только вправо — не потому, что правая сторона тяжелее, а потому, что это единственный край с полосой прокрутки: страница на 15 пикселей уже окна. Слева, сверху и снизу такой полоски нет, так что больше нигде этот дефект и не мог появиться.

4. Сторонняя слепая проверка видит то, чего не видит автор. Четыре рецензента, которые не знали, что есть что, за пять минут нашли шов, который я лично проверял и пропустил. Это часть того, как я работаю, а не разовая мера.

5. Генератор выполняет «посмотри вбок», но не выполняет «насколько». Планируйте работу так, чтобы «немного» и «наполовину» вам не понадобились.

Что ещё не сделано: нижние веки в улыбке

Когда человек по-настоящему смеётся, у него поднимаются нижние веки. У моего портрета они остаются спокойными над поднятыми щеками. Починить это можно только перегенерировав все восемь кадров взгляда в улыбающемся состоянии — и снова играть в лотерею амплитуды.

Портрет живой. Он просто ещё не смеётся по-настоящему.


Остальные инженерные разборы — в блоге; если хотите обсудить что-то из этого, напишите мне.

FIG. ✕ — СОТРУДНИЧЕСТВО

Нужен senior engineering leadership?

Архитектурное направление, performance engineering и senior-техническая ответственность для команд, которым это нужно. Давайте обсудим, подходим ли мы друг другу.

— EOF

MANIFEST · ENGAGEMENT.cfg

ОткрытКСотрудничеству

Предметный разговор о senior engineering leadership, сложных системных задачах или встроенной консультационной работе.

Локация
Удалённо
Ответ
< 24h
Форматы
Консультации / lead-роли
Обсудить сотрудничество