Привет, Хабр. Меня зовут Степанян Альберт (albert.stepanyan@beorg.ru), и я всё ещё бегун по граблям и разработчик отдела R&D в компании «Биорг».

В Древнем Риме говорили: «Mens sana in corpore sano» — в здоровом теле здоровый дух. Два тысячелетия спустя мы, айтишники, превратили эту мудрость в её полную противоположность: здоровый дух, сидящий в нездоровом теле перед монитором. Наш позвоночник принимает форму вопросительного знака, а шея начинает напоминать ржавый механизм — повернуть голову можно только всем корпусом, издавая при этом характерный хруст.
Мы в «Биорг» знаем об этой проблеме не понаслышке. Но, в отличие от большинства офисных работников, у нас под рукой оказалась система видеоидентификации, способная видеть каждое микродвижение человеческого лица. И как-то раз, тестируя систему видеоидентификации, мы поймали себя на мысли:
— То, что мы создавали для идентификации личности, по сути готовый трекер движений. Дай ему только интерфейс и он превратится в игру. Осталось только придумать, во что играть…
Попробовать разминку с Джонни и Джулией
Бесплатно, без смс, без регистрации и без подвоха. Под музыку или без! Нам не жалко. Мы вообще-то хотим просто шею размять, а не бизнес на этом строить.
Обычный антифрод и его необычная судьба
Одна из основных наших задач в R&D — борьба с фродом. Мы разработали систему видеоидентификации, которая определяет живость человека по движениям головы, согласно командам. Корректность их выполнения подтверждает, что перед нами живой пользователь.
О том, как мы пришли к этому решению и почему оно эффективно, уже рассказано в статье Развенчан маскарад подделок: прорыв в проверке документов
В процессе работы мы научились детектировать ключевые точки лица с высокой точностью. Алгоритм в реальном времени вычисляет углы поворота головы, наклоны, моргания. И однажды мы заметили, что эти движения похожи на те, которые человек выполняет во время зарядки.
Тогда и родилась идея: а что, если мы сделаем не просто идентификацию, а полезную разминку для шеи, встроенную прямо в браузер?
Рождение Джонни и Джулии
Испытать искреннее удивление от собственных возможностей и окружающего мира — это прекрасные и глубокие ощущения. Получить их может человек, который вырвался из привычной рутины и столкнулся с тем, что выходит за рамки его повседневного опыта. Так на свет появились два персонажа: Джонни и Джулия.
Их история похожа на красивый роман. За плечами дипломы одного из лучших университетов мира и блестящие перспективы за рубежом. Они приехали в Россию как гости, желая увидеть страну. Россия встретила их искренним гостеприимством, новой культурой, непередаваемой атмосферой. Наша страна покорила их сердца навсегда. Очарованные увиденным, они приняли главное решение в своей жизни — остаться здесь, с нами.

Почему двое? Мы добавили в виджет определение пола пользователя на старте — система определяет, кто перед камерой, и предлагает тренера того же пола. Это мелочь, но она делает взаимодействие чуть более персонализированным. Для определения пола используется открытая библиотека face-api.js с лицензией MIT. По желанию, пол инструктора можно сменить. Для этого есть специальная кнопка на экране - справа вверху.
Персонажей мы нарисовали в мультяшном стиле — яркие, добрые, с улыбкой. Потому что разминка должна вызывать радость, а не напряжение. К тому же, мы подумали: если пользователь сейчас будет делать серьёзное лицо, разминая шею, это выглядит глупо. А если рядом весёлый инструктор — процесс идёт легче.
Есть один нехитрый приём, который делает картинку цельной. Мы убираем фон у окна с пользователем и заменяем его на белый. То же самое делаем с фоном инструктора. В результате их фоны сливаются с общим белым фоном страницы, и визуально окна перестают быть «окнами» — они превращаются в единую композицию. Пользователь видит себя и тренера в одном пространстве, а не в двух отдельных квадратах.

Как мы считаем синхронизацию
Разминка состоит из 30 движений, и они каждый раз генерируются случайным образом. Никаких заучиваний — каждый новый подход будет отличаться от предыдущего. Это не даёт скучать и не позволяет мозгу «засыпать» на автоматических движениях.
Разминка состоит из нескольких упражнений. У каждого есть ожидаемая длительность — t.time. Пока пользователь выполняет движение, мы замеряем его фактическую скорость и фиксируем задержку (тот самый delay) — насколько пользователь отстаёт от тренера.
Когда все упражнения завершены, мы суммируем две величины:
totalExp— общее ожидаемое время всех движений;totalDelay— суммарное отставание пользователя.
Дальше простая формула:
raw = 100 - (totalDelay / totalExp * 100); score = Math.max(0, Math.min(100, raw));
То есть, если вы выполнили разминку идеально синхронно — твой скор стремится к 100. Но мы оставили люфт, потому что в реальном мире есть погрешности: камера может чуть дрожать, браузер — подвисать, а твоя шея — иметь свою точку зрения на то, как быстро надо поворачиваться.
И ещё один важный нюанс по геймплею. Мы заметили в тестах, что некоторые пользователи не могли с первого раза повторить то или иное движение. Может шея затекла, может не поняли траекторию. Зависать на одном движении бесконечно — плохая идея. Это раздражает и убивает смысл разминки.
Поэтому мы сделали так: после двух неудачных повторов инструктор переходит к следующему движению. Пользователь не чувствует себя неудачником, а просто продолжает разминку. А если он вообще не понял, что делать, — система не наказывает, а мягко ведёт дальше. В конце его ждёт честный результат, а не строгий экзамен.
Итоговый результат мы очеловечиваем:
if (score >= 98) text = "Синхронизация 100% — Вы копия тренера!"; else if (score >= 90) text = "90% синхронизации — Осталось совсем чуть‑чуть!"; else if (score >= 70) text = "70% синхронизации — Подтягиваемся к идеалу!"; else if (score >= 50) text = "50% синхронизации — Половина пути пройдена!"; else text = "30% синхронизации — Ловим темп тренера!";
98%, а не 100% специально обозначено как идеал. Потому что перфекционизм — это хорошо в коде, но вредно для здоровья. Если ты получил 98% — считай, что ты чемпион. А вот 30% — это повод не расстраиваться, а попробовать ещё раз, просто поймав ритм.
Как мы подружили разминку с телефонами
Мы изначально проектировали разминку для больших мониторов лэптов и рабочих станций — удобно: сидишь перед камерой ноутбука, поворачиваешь голову, и Джонни с Джулией тебя понимают. Но когда мы решили затестить у коллег, то первым их вопросом было:
— А на телефоне это работать будет?
Первая мысль была — определять устройство по размеру экрана. Но это оказалось ловушкой. Современные телефоны имеют диагональ почти как у небольших ноутбуков, а планшеты и вовсе размывают границы.
Я лично столкнулся с этой проблемой на своём Redmi Note 9. Экран у него, конечно, не как у ноутбука, но по ширине в пикселях он вполне сопоставим с некоторыми дешёвыми ноутбуками. Система считала, что раз экран достаточно широкий — значит, это компьютер, и отказывалась переключаться в мобильный режим. Кнопки оставались мелкими, интерфейс не адаптировался, а инструктор с пользователем пытались уместиться в той же раскладке, что и на большом экране. Работать было невозможно.
Стало понятно, размер экрана — это ненадёжный признак, и нужно искать что-то другое.
Тогда я пошёл другим путём: проверять наличие датчика ориентации, но не одним, а двумя признаками:
function isMobileDevice() { return 'DeviceOrientationEvent' in window && 'ontouchstart' in window; }
Первое условие — поддержка события ориентации (есть у телефонов и планшетов). Второе — поддержка касаний (отсекает компьютеры, даже если у них случайно затесался акселерометр). Вместе 2 условия дают почти 100% точность.
Если проверка проходит — добавляем на страницу класс mobile:
if (isMobileDevice()) { document.body.classList.add('mobile'); }
И дальше уже через CSS управляем всем, что должно отличаться. Но самое главное — это размер и положение окна с пользователем и инструктором.
На компьютере, как и на телефоне, в горизонтальном режиме, мы используем классическую раскладку: пользователь слева, инструктор справа. Это естественно для широкого экрана — взгляд скользит слева направо, и ты видишь одновременно и себя, и тренера.
На телефоне в вертикальном режиме всё иначе. Экран узкий, и раскладка «слева-направо» не работает. Поэтому пользователь занимает верхнюю половину экрана, а окно с инструктором — нижнюю. Так пользователь видит и тренера, и себя одновременно.
И это не просто визуальная прихоть. Если окно с пользователем слишком маленькое — он не видит, правильно ли выполняет движения. Если слишком большое — перекрывает инструктора: теряется смысл «повторяй за мной». Поэтому мы потратили несколько итераций, чтобы найти идеальные пропорции для каждого типа устройства и каждой ориентации.
Сам полноэкранный режим мы, кстати, реализовали и для компа, и для телефона:
const hint = document.querySelector('.exit-fullscreen-hint'); if (hint) { hint.style.display = 'none'; }
Справедливости ради: подсказка про выход из полноэкранного режима появляется и на компьютере, и на телефоне. Просто она разная. На компьютере — «Нажмите Esc для выхода из полноэкранного режима». На телефоне — «Проведите снизу вверх для выхода из полноэкранного режима». В обоих случаях она исчезает через 3 секунды, чтобы не мешать пользователю разминаться.
Трекинг лица: локальная сборка без сюрпризов
Для трекинга лица в разминке мы используем MediaPipe Face Mesh. Это открытая библиотека от Google, распространяемая под лицензией Apache 2.0. Мы взяли её в локальную сборку, поэтому всё работает стабильно и без внешних запросов к CDN. Никаких сюрпризов с недоступными серверами или обновлёнными API. Только предсказуемая логика в твоём браузере.
Инициализируем трекер так:
this.faceLandmarker = new FaceMesh({ locateFile: (file) => /static/mediapipe-face-mesh/${file} }); this.faceLandmarker.setOptions({ maxNumFaces: 1, refineLandmarks: false, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 });
Обратите внимание на refineLandmarks: false — мы выключили его специально. Нам не нужна микро-детализация губ и глаз, только общий контур головы. Экономия ресурсов браузера — наше всё. И, кстати, именно благодаря локальной сборке мы можем настраивать параметры под свои задачи, не оглядываясь на внешние зависимости.
И ещё один бонус локального подхода: библиотека лежит в нашем проекте, а браузер кеширует её при первом запросе, так что при повторных запусках она не скачивается заново.
И главное, что MediaPipe работает независимо от того, как ты держишь телефон: вертикально или горизонтально. Главное, чтобы лицо было в кадре!
Какой режим выбрать?
В итоге оказалось, что в вертикальном режиме пользователь интуитивно держит телефон на уровне лица и это идеально для детекции. Плюс, интерфейс с Джонни и Джулией выглядит как видеозвонок с личным тренером.
А в горизонтальном режиме телефон можно поставить на подставку или положить на стол и выполнять упражнения, глядя на экран как на маленькое зеркало. Такой формат больше похож на классический фитнес-гаджет.
Так что теперь мы рекомендуем вертикальный режим для атмосферы, а горизонтальный для техничного прохождения с максимальным комфортом.
Музыка — двигатель прогресса (и шеи)
Когда прототип с Джонни и Джулией заработал, мы дали его потестить коллегам. Офис разделился на два лагеря.
Первые, хмуро глядя в монитор, синхронно поворачивали головы и напоминали роботов на конвейере. Вторые незаметно покачивались в такт и слегка пританцовывали, даже когда никто не смотрел. Разница была в одном: у вторых на фоне тихо играла музыка.
Мы решили, что пусть пользователь сам решает, хочет ли он разминаться в тишине или под ритмичный трек. Так появились две кнопки: «Под музыку» и «Без музыки».
И если с «тихим» режимом всё просто, то с музыкой пришлось повозиться. Мы перебрали десяток треков разного темпа, пока не нашли тот самый, который совпадает с комфортной скоростью поворота головы при разминке. Слишком быстро — пользователь не успевает, слишком медленно — скучно. Идеальный темп нашёлся где-то около 140 BPM, как у хорошего транс-хауса.
И вот теперь представь: ты сидишь после пяти часов дебаггинга, запускаешь разминку, выбираешь «Под музыку», и на экране появляется симпатичная Джулия, которая под ритмичный бит начинает поворачивать голову. Ты повторяешь и незаметно для себя перестаёшь думать о багах и просто двигаешься. Это работает как медитация.
Тайна, которую мы не раскроем (но вы всё равно узнаете)
Когда разминка заканчивается и на экране появляется твой результат что-то происходит.
Не будем раскрывать, что именно. Пусть это останется сюрпризом для тех, кто дойдёт до финала. Скажем одно: мы вложили туда частичку нашей души, и, судя по первым тестам, это вызывает чистую, детскую радость даже у самых суровых бэкенд-разработчиков.
Так что когда дойдёте до конца, поделитесь в комментариях, какой бонус увидели и понравилось ли вам.
Заключение
История этой разминки — отличное напоминание о том, что лучшие идеи рождаются не из строгих ТЗ. Мы не планировали делать разминку. Мы планировали делать антифрод. А заодно получили продукт, который одинаково полезен и для безопасности системы, и для шеи разработчика.
А вообще, Джонни и Джулии у нас нравится. Они уже члены нашей команды — ходят на парады и поздравляют нас с праздниками.

