Инженерный тур. 3 этап
На заключительном этапе инженерного тура команды создают веб-ресурс с дополненной реальностью, в которой действует цифровой ИИ-агент. Он имеет внешность, похожую на реального человека, способность вести разумный диалог на заданную тему «Люди Байкала», отвечать на вопросы, давать советы.
Компания «Альтернативное будущее» продолжает свою миссию по созданию образовательных интернет-ресурсов с использованием передовых технологий дополненной реальности (webAR). На этот раз компания предлагает командам поучаствовать в проекте «Люди Байкала», посвященный разработке веб-ресурса с дополненной реальностью, объединяющего технологии webAR и искусственного интеллекта.
Байкал — жемчужина Сибири, крупнейшее пресноводное озеро планеты и объект Всемирного наследия ЮНЕСКО. Это место не только удивительной природы, но и богатой культуры, где переплетаются истории коренных народов, научные открытия и современные экологические вызовы. Однако многие люди знают о Байкале лишь поверхностно, а его истинная ценность остается скрытой за завесой времени и расстояний.
Чтобы сделать знания о Байкале доступными каждому, компания предлагает реализовать цифрового ИИ-агента в дополненной реальности. Этот агент будет обладать внешностью реального ученого, изучающего Байкал, и сможет вести осмысленный диалог на тему его исследования.
| Наименование | Описание |
|---|---|
Компьютер или ноутбук (базовые характеристики Operating system version Windows: Windows 10; CPU: x86, x64 architecture with SSE2 instruction set support; Graphics API: DX10, DX11, DX12 capable) |
Для разработки программных компонентов |
Планшет или смартфон (базовые характеристики Operating system: Android7, CPU: ARM 64-bit, Graphics API: OpenGL ES) |
Для тестирования разработанных AR-сценариев и просмотра веб-ресурса |
Редактор кода или IDE, например: VS Code (https://code.visualstudio.com/) Sublime text (https://www.sublimetext.com/) или другие аналоги |
Для разработки программных компонентов и их отладки; для развертки локального сервера |
| Графический редактор, например: Adobe Photoshop, Adobe Illustrator, Figma (https://www.figma.com/), Procreate, Gimp, Paint или другие аналоги | Для создания маркеров, макета веб-ресурса, 2D-материалов оверлея и сайта |
| 3D-редактор, например: Blender (https://www.blender.org/) или другие аналоги | Для создания трехмерных моделей |
Интернет-браузер, например: Яндекс Браузер (https://browser.yandex.ru/) или другие аналоги |
Для просмотра и отладки решений |
| Дополнительно: | |
| Видеокамера, наушники, компьютерная мышь | Для удобства в работе и тестировании |
| Доступ в интернет со скоростью около 100 Мбит/с. | Для тестирования и просмотра результатов работы, при разворачивании сервисов в сети интернет |
Количество участников в команде — 4 человека.
Компетенции, которыми должны обладать члены команды:
Создание 3D-модели (3D-конструктор, дизайнер):
- Разработка модели цифрового аватара и цифровой личности (Blender).
- Анимация моделей.
- Экспорт моделей в формате GLTF для удобного использования с AR.js.
Разработка веб-AR приложения (AR-разработчик):
- Формирование триггера и его обработка.
- Создание HTML-страницы, включающей необходимые библиотеки.
- Настройка AR-сцены с использованием AR.js и добавление 3D-модели человеческого тела.
- Реализация текстового и речевого интерфейса для ввода запросов и вывода ответов.
Логика обработки запросов (разработчик: бэкенд):
- Разработка логики для обработки вводимых пользователем команд.
- Создание базы данных, содержащей информацию о темах исследования.
- Интеграция: ИИ-агент + БД.
Чат-бот с базой знаний (специалист по ИИ):
- Создание интенций, соответствующих темам исследований ученых из задачи.
- Предобучение модели.
- Разработка логики обработки интенций и взаимодействие через вебхуки с AR-образом.
Разработать цифрового аватара: виртуальную 3D-модель реального человека со встроенным ИИ-агентом, настроенным отвечать на вопросы в рамках темы «Люди Байкала».
Цифровой аватар — воплощение реального ученого, обладающего знаниями в той области, которую он изучает. Аватар станет проводником в мир байкальской культуры, экологии и истории, помогая пользователям находить ответы на вопросы и делиться уникальными историями.
Все предлагаемые подзадачи для реализации во время заключительного этапа описаны в таблице 1.1. Участники могут выбрать некоторые из них для реализации, в зависимости от навыков и времени, затраченного на другие подзадачи, и реализовать только часть из полного списка предполагаемых работ.
Для удобства все задачи поделены по ролям, но участники вправе обмениваться задачами и/или перераспределять их.
| 3D-дизайнер | AR-разработчик | Web-разработчик | Специалист по ИИ |
|---|---|---|---|
| Обсуждение идеи, выбор компонентов для визуализации и определение необходимой информации | |||
| Изменение аватаров под личности выбранных людей | Проработка сценария взаимодействия пользователя с приложением | Определение формата и содержания ответа от бота | |
| Добавление объектов одежды и волос | AR-сцена не исчезает, когда маркер пропадает с камеры | Обработка взаимодействия с агентом (обработка ошибок) | Формирование базы знаний |
| Наложение материалов и текстур на аватары | Разработка собственного маркера в тематике команды | Обработка ответа от бота | Построение схемы работы ИИ-агента (соответствует полному сценарию взаимодействия) |
| Создание концепций предметов | Оптимизация текстового ответа от бота под пространство сцены | Создание отдельного чата под каждого пользователя/ сессию | Правильная интерпретация темы/ объекта разговора |
| Блокинг предметов | Анимирование оверлея | ||
| Доработка предметов ученых | Интерфейс для ввода вопроса разными способами | Разработка концепции внедрения голосовых запросов и ответов и последующая реализация | |
| Создание рига ученых | Доработка сценариев взаимодействия с учетом добавления интерактивности | Запоминание контекста общения (в рамках одной сессии) | |
| Создание анимации ученых | Добавление интерактивности оверлея (например, нажатия) | Расширение интерфейса взаимодействия с AR-контентом через внедрение жестов и или распознавания лица | Обработка вопросов вне тематики бота |
| Экспорт моделей | Тестирование на различных устройствах и запись демонстрационного видеоролика | Создание презентации | Рекомендуемые вопросы/темы с учетом истории сообщений |
| Внедрение 3D-моделей | |||
Оценка задачи заключительного этапа делится на несколько составляющих:
Реализация веб-ресурса:
- блок 1. AR-составляющая (22,5%);
- блок 2. Веб-разработка (22,5%);
- блок 3. ИИ-составляющая (22,5%);
- блок 4. 3D-моделирование (22,5%).
- Защита решений команды (10%): баллы за все составляющие нормируются к 100.
Все критерии оценки для всех составляющих представлены в таблицах №№ 1.2–1.5.
| Критерии оценки | Максимум баллов |
|---|---|
| Собственный маркер (дизайнерский — 2, не очень оригинально — 1) | 2 |
| Внедрили собственный маркер в проект и все работает | 1 |
| Отвязка оверлея от сцены (идейно обыграно — 4, с новым позиционированием — 3, без позиционирования — 2) | 4 |
| Текстовый ответ от бота (предусмотрена разбивка текста — 2, укорочен за счет промпта — 1,5, просто есть в наличии — 1) | 2 |
| Оверлей на сцене расположен с правильным позиционированием | 2 |
| Анимация оверлея (через атрибут — 1, реализован скрипт перемещения — 3) | 3 |
| Интерактивность оверлея (обработка нажатия на него — 5, взаимодействие через элементы экрана — 3) | 5 |
| Адаптивная верстка (адаптация оверлея — 2, адаптация только кнопок и поля для ввода — 1) | 3 |
| Интерфейс (отправка сообщения, включение голосовое, пауза) | 2 |
| Наличие приветствия | 1 |
| Отображение текущего статуса пользователю (ожидания ответа, нет ответа) | 2 |
| Итого баллов | 27 |
| ИТОГО НОРМИРОВАННОЕ | 22,5 |
| Критерии оценки | Максимум баллов |
|---|---|
| Подключение бота (API — 2, веб-чат — 1) | 2 |
| Обработка ошибок (подключение, неверный формат ответа, долгое ожидание) | 2 |
| Обработка ответа чат-бота (пустая строка, некорректный ответ) | 3 |
| Озвучка персонажа (модель — 3, заготовленные сообщения — 1) | 3 |
| Интерпретация голосового ввода вопросов | 4 |
| Создание сессий под каждого пользователя | 2 |
| Интерактивность (например, в виде жестов/распознавания лица и др.) | 6 |
| Итого баллов | 22 |
| ИТОГО НОРМИРОВАННОЕ | 22,5 |
| Критерии оценки | Максимум баллов |
|---|---|
| База знаний (объем, верная разметка, корректная информация) | 3 |
| Реализация в Langflow (условия ветки, правильные соединения) | 3 |
| Приветствие внутри бота | 2 |
| Правильная интерпретация темы/разговора (из инструкции — 1, несколько слов или предложения — 2, LLM — 2) | 2 |
| Запоминание контекста общения (только одна тема за сессию — 1, несколько тем — 3) | 3 |
| Рекомендуемые темы для общения (генерация дальнейших вопросов по контексту — 2, предложение одним сообщением без контекста — 1) | 2 |
| Итого баллов | 15 |
| ИТОГО НОРМИРОВАННОЕ | 22,5 |
| Критерии оценки | Максимум баллов |
|---|---|
| Количество триссов \(\le\) 20000 (коэффициент 0,1 за каждые 500 сверху) | 15 |
| Адаптация сетки под анимацию (сетка состоит преимущественно из квадов, наличие доп. петель вокруг сгибов) | 5 |
| Правильное направление нормалей | 3 |
| Отсутствие двойной геометрии и артефактов полигонов | 3 |
| Правильные значения трансформации | 3 |
| Портретное сходство с оригиналом | 5 |
| Наличие на объекте волос | 3 |
| Наличие на объекте одежды | 3 |
| Наличие рига | 4 |
| Правильные локальные оси рига и вес | 4 |
| Анимация рига | 8 |
| Дополнительная анимация | 3 |
| Наличие развертки | 2 |
| Швы развертки расположены по правилам | 3 |
| Острова развертки оптимизированы по размеру | 3 |
| Растяжение развертки отсутствует или минимально | 2 |
| Наличие одного материала для всего объекта; коэффициенты: 0,5 от 2,0 до 4,0, если более 5. | 4 |
| Наличие текстуры цвета | 6 |
| Размер текстуры (до 2048 рх) | 4 |
| Отсутствие альфа-канала на текстуре | 2 |
| Формат текстуры адаптирован под веб | 3 |
| Наличие карты нормалей | 2 |
| Размер карты нормалей (до 2048 рх) | 2 |
Отсутствие альфа-канала на карте нормалей Формат нормалей адаптирован под веб |
2 |
Формат нормалей адаптирован под веб; коэффициенты:
|
1 |
В gltf-файле нет лишних объектов |
3 |
Текстуры и материалы есть в gltf-файле (запечены при необходимости) |
2 |
| Для объектов (всего 3) | |
| Количество триссов \(\le\) 2000 | 30 |
| Адаптированная сетка под статичный объект | 6 |
| Правильное направление нормалей | 6 |
| Отсутствие двойной геометрии и артефактов полигонов | 6 |
| Правильные значения трансформации | 6 |
| Объект соотносится с аватаром | 15 |
| Наличие развертки | 9 |
| Острова развертки оптимизированы | 3 |
| Растяжение развертки отсутствует или минимально | 3 |
Наличие одного материала для всего объекта:
|
12 |
Наличие текстуры цвета:
|
12 |
| Размер текстуры (до 2048 рх, иначе вводится коэффициент 0,5) | 9 |
| Отсутствие альфа-канала на текстуре | 6 |
| Формат текстуры адаптирован под веб | 6 |
| Наличие карты нормалей | 6 |
| Размер карты нормалей (до 2048 рх) | 3 |
| Отсутствие альфа-канала на карте нормалей | 3 |
| Формат нормалей адаптирован под веб | 3 |
В gltf-файле нет лишних объектов |
3 |
Текстуры и материалы есть в gltf-файле (запечены при необходимости) |
3 |
| Итого баллов | 250 |
| Штрафные баллы | до –3 |
| ИТОГО НОРМИРОВАННОЕ | 22,5 |
Штрафные баллы предусмотрены:
- за использование автоматизированных решений (прим. Avaturn),
- за частично или полностью сгенерированные и/или заимствованные из интернета модели,
- за модели, представляющие собой примитивы без внесения значительных изменений.
| Критерии оценки | Максимум баллов |
|---|---|
| Схема взаимодействия | 2 |
| Концепт БД | 3 |
| Представлены все разработанные модели | 2 |
| Единый стиль слайдов | 1 |
| Описаны участники команды и их роли | 1 |
| Демонстрационный видеоролик (отражает полноценный сценарий взаимодействия, краткая версия работы с приложением, отдельные фрагменты) | 2 |
| Соблюдение регламента защиты | 1 |
| Ответы на вопросы (корректные ответы, неуверенные ответы) | 2 |
| Дополнительные баллы | 2 |
| Итого баллов | 16 |
| ИТОГО НОРМИРОВАННОЕ | 10 |
Общая оценка решения инженерного тура формируется по формуле:
Для каждой части реализации веб-ресурса: \[0{,}225 \times\text{баллы за реализацию} + 0{,}1 \times\text{баллы за презентацию проекта жюри}.\]
При верном (задание проверяется на соответствие требованиям), полном (выполнены все пункты задания) и качественном (экспертная критериальная оценка) выполнении всех заданий профиля участники могут набрать 100 баллов.
В случае, если ни одна команда участников заключительного этапа не набирает максимально возможного балла, к 100 нормируется результат лучшей команды, соответствующим образом пересчитываются результаты остальных команд.
В качестве вводных данных и выбора темы для контекста создаваемого продукта командам рекомендованы следующие ученые и направления исследования их деятельности:
- гидробиология: Стом Алина Дэвардовна;
- геология нефти и газа: Андреева Юлия Сергеевна;
- динамическая геология: Рассказов Сергей Васильевич;
- зоология: Матвеев Аркадий Николаевич;
- орнитология: Фефелов Игорь Владимирович;
- астрофизика: Танаев Андрей Борисович;
- метеорология: Латышева Инна Валентиновна;
- экология: Карнаухов Дмитрий;
- генетика: Дроздова Полина;
- гидробиология: Верещагина Ксения Петровна.
По каждому ученому приводится краткая биография, направление исследования, достижения и фотография. Пример представлен ниже.
Зоология: Матвеев Аркадий Николаевич
Матвеев Аркадий Николаевич — доктор биологических наук, профессор, декан биолого-почвенного факультета Иркутского государственного университета, заведующий кафедрой зоологии позвоночных и экологии. Отвечает за учебные программы кафедры водных ресурсов ЮНЕСКО.
Родился 2 октября 1957 года в Иркутске. В 1980 году окончил биолого-почвенный факультет ИГУ по специальности «Ихтиология». С 1980 по 1996 года работал лаборантом, старшим лаборантом и инженером на кафедре зоологии позвоночных.
В 1993 году защитил кандидатскую диссертацию на тему «Структура рыбного населения литорали Северного Байкала», а в 2006 году стал доктором наук, защитив работу «Структура рыбного населения горных водоемов Байкальской рифтовой зоны».
С 1998 года занимает должность заведующего кафедрой зоологии позвоночных и экологии, а в декабре 2008 года был избран деканом Биолого-почвенного факультета.
Основные научные интересы связаны:
- с исследованием гидробионтов озера Байкал и горных водоемов Байкальской рифтовой зоны,
- с изучением структуры и динамики популяций рыб, их питания и трофической структуры биоценозов, а также оценкой воздействия на гидробиоценозы.
А. Н. Матвеев является автором и соавтором множества научных статей и монографий, посвященных экологии и ихтиологии. Среди публикаций можно выделить работы:
- «Гидроэнергетика и состояние экосистемы озера Байкал» (1999 г.),
- «Биота Витимского заповедника: структура биоты водных экосистем». (2006 г.).
После того как выбрана тема, работа в команде может вестись параллельно по четырем направлениям:
- блок 1. AR-составляющая;
- блок 2. веб-разработка;
- блок 3. ИИ-составляющая;
- блок 4. 3D-моделирование.
Описание решения задачи далее будет представлено в соответствии с данными направлениями.
После выбора тематики необходимо описать общую концепцию разрабатываемого приложения и сценарий взаимодействия пользователя с ним.
Постановка задачи: AR-бот для ознакомления с зоологией в экосистеме Байкала от лица Матвеева Аркадия Николаевича.
WrbAR-сервис с интеллектуальным ассистентом-ученым для ответов на вопросы по теме его исследования. Пользователи могут:
- узнать, чем именно занимается ученый,
- расспросить его об интересующих моментах,
- уточнить определения,
- задать общие вопросы об озере Байкал.
Бот отвечает на вопрос пользователя, предоставляя краткие описания, и отражает тематическую визуализацию в зависимости от контекста общения. Для большей интерактивности и интереса пользователей в ресурс интегрирована фотобудка.
Пример алгоритма работы (взаимодействия) AR-бота:
- При наведении на QR-код на экране устройства отображается 3D-модель аватара ученого, с которым общается пользователь, отображается приветствие и описание возможных тематик общения чат-бота.
Пользователь может:
- ввести вопрос в отображаемый интерфейс, например, «Что такое зоология?» и нажать кнопку для отправки вопроса;
- выбрать пункт в меню «расскажите о себе», «расскажите о гидробионтах», «10 интересных фактах о рыбах Байкала» или «Байкальская рифтовая зона»;
- выбрать пункт «фотобудка» для перехода в интерактивную страницу сервиса.
Бот делает запрос в базу знаний:
- при нахождении ответа выводит информацию на экран устройства, а также отображает 3D-модель объекта, о котором идет речь, если она была создана;
- если информация не найдена в базе знаний, то пользователю выводится информация об этом и предлагается выбрать тематику общения из предложенных ранее (см. п. 2.2).
Для защиты следует представить алгоритм в виде схемы в свободной форме. Ниже приводятся два примера (см. рис. 7.2–7.3).
Для отображения дополненной реальности потребуется тематический маркер.
Пример маркера из решения команды ARTeam_PSh_MaGN_V2.
Алгоритм его создания.
Выбор изображения для маркера
(примечание: рекомендуется выбирать контрастные изображения с четкими границами, чтобы AR.js мог легко их распознать).
Советы по выбору маркера:
- используйте изображения с четкими деталями, избегайте размытия;
- предпочитайте черно-белую палитру или минимализм;
- избегайте слишком сложных узоров, которые могут путать алгоритм распознавания.
Создание маркера через AR.js Studio.
Используйте AR.js Studio Marker Generator: https://jeromeetienne.github.io/AR.js/three.js/examples/marker-training/examples/generator.html.
Рис. 7.5.- загрузите изображение (формат: PNG или JPEG);
- настройте параметры генерации, такие как размер маркера;
- скачайте сгенерированные файлы:
.patt(шаблон маркера) и изображение маркера для печати.
Подключение маркера к сцене.
Для работы с разработанным маркером поместите
.pattфайл в папку проекта и используйте компонентa-markerдля добавления маркера.Для того чтобы проверить готовый маркер, воспользуйтесь кодом, приведенным ниже.
HTML<!DOCTYPE html> <html> <head> <script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script> <script src="https://raw.githack.com/AR-js-org/AR.js/master/ aframe/build/aframe-ar.js"></script> </head> <body style="margin: 0; overflow: hidden;"> <a-scene embedded arjs> <!-- Маркер с кастомным шаблоном --> <a-marker type="pattern" url="path/to/your-marker.patt"> <!-- 3D-объект, привязанный к маркеру --> <a-box position="0 0.5 0" material="color: red;"></a-box> </a-marker> <!-- Камера для AR --> <a-entity camera></a-entity> </a-scene> </body> </html>
Для удобства использования сервиса рекомендуется добавить следующие составляющие:
- Оптимизация текстового ответа от бота под пространство сцены для того, чтобы длинные ответы от чат-бота помещались на экран устройства пользователя.
- AR-сцена не исчезает, когда маркер пропадает с камеры для реализации сценариев с плохой детекцией маркера (недостаточной освещенностью, неудобным расположением маркера, скоплением людей возле него и т. п.). В таком случае оверлей отвязывается от марки и продолжает отображаться, а также сохраняет возможность взаимодействия с ним.
- Интерфейс для ввода вопроса разными способами, а именно: текстовым и голосовым.
- Отображения «состояния» сервиса: «передача запроса», «формирование ответа» и т. п.
./src/css/style.css
html, body { margin:0; padding:0; overflow:hidden; }
.ar-control {
position:absolute; top:10px; left:10px;
background:rgba(255,255,255,0.8);
padding:6px 10px; border-radius:4px; font-size:14px;
z-index:100;
}
#chat-container {
position:absolute; top:10px; right:10px;
width:320px; max-width:90%;
background:rgba(255,255,255,0.9);
border-radius:8px; display:flex; flex-direction:column;
z-index:100;
}
#chat-messages { flex:1; padding:8px; overflow-y:auto; height:200px; }
#chat-input { display:flex; padding:8px; }
#chat-input input { flex:1; padding:4px; margin-right:4px; }
.error { border:1px solid red; }
#history-modal, #error-modal {
display:none; position:fixed; top:50%; left:50%;
transform:translate(-50%,-50%);
background:rgba(0,0,0,0.8); color:white;
padding:10px; border-radius:8px; z-index:200;
max-width:90%;
}
#history-messages { max-height:300px; overflow-y:auto; margin-bottom:8px; }
.microphone-button {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
transition: transform 0.1s ease;
}
.microphone-button.recording {
color: red;
transform: scale(1.2);
}
.chat-status {
padding: 4px 8px;
font-size: 14px;
color: #555;
background: rgba(255, 255, 255, 0.8);
border-radius: 4px;
margin: 4px;
text-align: center;
}
./src/js/ar-component.js
AFRAME.registerComponent('marker-persistence', {
schema: {
worldPos: { type: 'vec3', default: { x:0, y:0, z:0 } },
worldRot: { type: 'vec3', default: { x:0, y:0, z:0 } }
},
init: function() {
this.worldEntity = document.createElement('a-entity');
this.worldEntity.id = 'world-entity';
this.worldEntity.setAttribute('visible', false);
if (this.el.sceneEl) {
this.el.sceneEl.appendChild(this.worldEntity);
}
this.modelEntity = document.querySelector('#model-entity');
this.el.addEventListener('markerFound', this.onMarkerFound.bind(this));
this.el.addEventListener('markerLost', this.onMarkerLost.bind(this));
this.markerVisible = false;
},
onMarkerFound: function() {
this.worldEntity.setAttribute('visible', false);
this.modelEntity.setAttribute('visible', true);
this.markerVisible = true;
},
onMarkerLost: function() {
updateModelPosition();
if (!this.worldEntity.hasChildNodes()) {
const clone = this.modelEntity.cloneNode(true);
clone.id = 'world-model';
this.worldEntity.appendChild(clone);
}
this.worldEntity.setAttribute('visible', true);
this.markerVisible = false;
}
});
./scr/js/chat.js
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('MyForm');
form.addEventListener('submit', async e => {
/*
здесь реализована логика обработки ошибок
*/
...
const statusEl = document.getElementById('chat-status');
statusEl.textContent = 'Запрос отправлен… Ждем ответа бота';
statusEl.style.display = 'block';
try {
/*
здесь реализована логика отправки запросов
*/
...
statusEl.textContent = 'Ответ получен';
setTimeout(() => { statusEl.style.display = 'none'; }, 1500);
...
/*
здесь реализована логика сохранения истории запросов
*/
} catch {
statusEl.textContent = 'Ошибка сервиса. Попробуйте еще раз';
statusEl.style.display = 'block';
errMod.style.display = 'block';
}
});
/*
остальная логика работы с сообщениями
*/
...
window.addEventListener('offline', ()=> {
inputChat.placeholder = 'Нет соединения';
});
window.addEventListener('online', ()=> {
inputChat.placeholder = 'Введите сообщение...';
});
...
./src/js/main.js
document.addEventListener('DOMContentLoaded', () => {
const marker = document.getElementById('marker');
const modelEnt = document.getElementById('model-entity');
const chatCont = document.getElementById('chat-container');
const slider = document.getElementById('scale-slider');
const centerPos = { x:0, y:0, z:0 };
let markerVisible = false;
let boolModel = false;
marker.addEventListener('markerFound', () => {
chatCont.style.display = 'flex';
modelEnt.setAttribute('visible', true);
markerVisible = true;
modelEnt.setAttribute('animation-mixer', boolModel ? 'timeScale:1' : 'timeScale:0');
updateCurrentModelScale();
});
marker.addEventListener('markerLost', () => {
markerVisible = false;
modelEnt.setAttribute('position', centerPos);
modelEnt.setAttribute('animation-mixer', boolModel ? 'timeScale:1' : 'timeScale:0');
updateCurrentModelScale();
});
function updateModelPosition() {
if (markerVisible) {
const wp = new THREE.Vector3();
marker.object3D.getWorldPosition(wp);
modelEnt.object3D.position.copy(wp);
}
requestAnimationFrame(updateModelPosition);
}
updateModelPosition();
<!DOCTYPE html>
<html lang="ru">
<head>
#погрузка библиотек и дополнительных скриптов
<script src="src/js/ar-components.js" defer></script>
<script src="src/js/main.js" defer></script>
</head>
<body>
<a-scene>
</a-scene>
<div id="chat-container">
<div id="chat-status" class="chat-status"></div>
<div id="chat-messages"></div>
<div id="chat-input">
<form id="MyForm">
<input id="input-chat" type="text" placeholder="Введите сообщение..." />
<button id="send-button" type="submit">Отправить</button>
<button id="history-button" type="button">История</button>
</form>
<button id="mic-button" class="microphone-button" aria-label="Начать запись">
????
</button>
</div>
<div id="history-modal">
<div id="history-messages"></div>
<button id="close-history">Закрыть</button>
</div>
<div id="error-modal">
<span>Ошибка соединения</span>
<button class="close-modal">OK</button>
</div>
</div>
</body>
Кроме этого, в данный блок входило еще несколько задач:
- Анимирование оверлея.
- Добавление интерактивности оверлея (например, нажатия).
- Адаптивность под различные устройства.
./src/css/styles.css
/* ----------------------------------------------------------------
Планшеты (<=768px)
-----------------------------------------------------------------*/
@media (max-width: 768px) {
/* Чат чуть шире, опускаем вниз */
#chat-container {
width: 80%;
top: auto;
bottom: 10px;
right: 10px;
}
/* Масштаб-контрол сбрасываем вниз */
.ar-control {
top: auto;
bottom: 60px;
left: 10px;
}
}
/* ------------------------------------------------------------------
Мобильные (<=480px)
-------------------------------------------------------------------*/
@media (max-width: 480px) {
/* Чат занимает всю ширину внизу */
#chat-container {
width: 100%;
left: 0;
right: 0;
top: auto;
bottom: 0;
border-radius: 0;
max-height: 40%;
background: rgba(255,255,255,0.95);
}
/* Вход и кнопки в столбик */
#chat-input {
flex-direction: column;
}
#chat-input input {
width: 100%;
margin: 0 0 4px 0;
}
#chat-input button {
width: 100%;
}
/* Скрываем контрол масштабирования на телефоне */
#scale-control {
display: none;
}
/* Уменьшаем размер текста */
#chat-messages, .chat-status {
font-size: 14px;
}
}
./index.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>AR + Chat</title>
<!--
Подгрузка необходимых остальных скриптов
-->
<script src="src/js/ar-components.js" defer></script>
<script src="src/js/main.js" defer></script>
<script src="src/js/chat.js" defer></script>
<script src="src/js/voice.js" defer></script>
<script src="src/js/gesture_texture.js" defer></script>
<script src="src/js/components/rotate-on-add.js" defer></script>
</head>
<body>
<a-scene
id="scene"
embedded
arjs="sourceType: webcam; debugUIEnabled: false; trackingMethod: best;">
<a-assets>
<a-asset-item id="yourModel" src="assets/models/character.glb"></a-asset-item>
<canvas id="gestureCanvas" style="display:none;"></canvas>
</a-assets>
<a-plane
position="0 1.5 -2"
width="1.6" height="0.9"
gesture-texture>
</a-plane>
<a-marker
id="marker"
type="pattern"
url="assets/patts/pattern-marker.patt"
marker-persistence>
<a-entity
id="model-entity"
gltf-model="#yourModel"
scale="1 1 1"
animation-mixer>
</a-entity>
<a-entity
id="model-object"
gltf-model="#model2"
scale="1 1 1"
rotate-on-add="speed: 60; axis: y">
</a-entity>
</a-marker>
<a-entity camera></a-entity>
</a-scene>
./src/js/main.js
document.addEventListener('DOMContentLoaded', () => {
. . .
// выше инициализация остальных переменных
const sceneEl = document.getElementById('scene');
sceneEl.addEventListener('click', () => {
sceneEl.querySelectorAll('[animation-mixer]').forEach(ent => {
ent.setAttribute('animation-mixer', 'timeScale: 1');
});
});
// дальнейшая логика
. . .
});
./scr/js/rotate_odd_add.js
document.addEventListener('DOMContentLoaded', () => {
AFRAME.registerComponent('rotate-on-add', {
schema: {
speed: { type: 'number', default: 30 }, // градусы в секунду
axis: { type: 'string', default: 'y' } // 'x', 'y' или 'z'
},
init() {
// Переводим скорость из градусов в радианы на миллисекунду
this.radPerMs = THREE.Math.degToRad(this.data.speed) / 1000;
this.elapsed = 0;
},
tick(time, timeDelta) {
// Вращаем вокруг выбранной оси
const obj3d = this.el.object3D;
obj3d.rotation[this.data.axis] += this.radPerMs * timeDelta;
}
});
});
Основная задача данного блока состоит в том, чтобы настроить правильный routing выходов бота и их интеграцию на клиентскую сторону (настройка отображения и обеспечение корректного взаимодействия клиента и бота). Для этого необходимо решить следующие задачи:
- Настройка передачи запроса боту.
- Написание логики клиента для отображения вывода бота (вывод ответа бота пользователю).
- Обработка возможных ошибок в выводе бота: объявление стандартизации того, как конкретно должен бот общаться с клиентом с точки зрения объектной модели.
Решение данного блока представлено в виде фрагментов кода к соответствующим задачам.
Обработка взаимодействия с агентом (обработка ошибок)
function validateBotResponse(data) {
if (
!data ||
!Array.isArray(data.outputs) ||
data.outputs.length === 0 ||
!Array.isArray(data.outputs[0].outputs) ||
data.outputs[0].outputs.length === 0 ||
!data.outputs[0].outputs[0].results ||
!data.outputs[0].outputs[0].results.message ||
typeof data.outputs[0].outputs[0].results.message.text !== 'string'
) {
throw new Error('Неправильная структура ответа от бота');
}
const text = data.outputs[0].outputs[0].results.message.text.trim();
if (!text) {
throw new Error('Пустой ответ от бота');
}
return text;
}
Обработка ответа от бота
const form = document.getElementById('MyForm');
const inputChat = document.getElementById('input-chat');
const btnSend = document.getElementById('send-button');
const btnHist = document.getElementById('history-button');
const histMod = document.getElementById('history-modal');
const histMsgs = document.getElementById('history-messages');
const closeHist = document.getElementById('close-history');
const errMod = document.getElementById('error-modal');
const closeErr = document.querySelector('.close-modal');
form.addEventListener('submit', async e => {
e.preventDefault();
const text = inputChat.value.trim();
if (!text) {
inputChat.classList.add('error');
return;
}
inputChat.value = '';
const statusEl = document.getElementById('chat-status');
statusEl.textContent = 'Запрос отправлен… Ждем ответа бота';
statusEl.style.display = 'block';
try {
const resp = await fetch(
"http://10.10.2.30:7860/api/v1/run/ f2ad3722-8930-406c-b06d-54ddd58b8a18?stream=false", {
method: "POST",
headers: {"x-api-key":"sk-…", "Content-Type":"application/json"},
body: JSON.stringify({
input_value: text,
session_id: sessionId,
input_type:"chat",
output_type:"chat",
tweaks:{}
})
});
if (!resp.ok) throw new Error(resp.status);
const data = await resp.json();
statusEl.textContent = 'Ответ получен';
setTimeout(() => { statusEl.style.display = 'none'; }, 1500);
const botText = validateBotResponse(data);
// Успех
statusEl.textContent = 'Ответ получен';
setTimeout(() => { statusEl.style.display = 'none'; }, 1500);
addMessage('Вы', text);
addMessage('Бот', botText);
speakBotResponse(botText);
saveHistory(text, botText);
} catch {
statusEl.textContent = 'Ошибка сервиса. Попробуйте еще раз';
statusEl.style.display = 'block';
errMod.style.display = 'block'; }
});
function addMessage(who, msg) {
const d = document.createElement('div');
d.innerHTML = `<strong>${who}:</strong> ${msg}<hr/>`;
document.getElementById('chat-messages').appendChild(d);
}
btnHist.addEventListener('click', displayHistory);
closeHist.addEventListener('click', ()=> histMod.style.display='none');
closeErr .addEventListener('click', ()=> errMod.style.display='none');
function saveHistory(u,b) {
const h = JSON.parse(localStorage.getItem('chatHistory')||'[]');
h.push({user:u,bot:b});
localStorage.setItem('chatHistory', JSON.stringify(h));
}
function displayHistory() {
const h = JSON.parse(localStorage.getItem('chatHistory')||'[]');
histMsgs.innerHTML = h.length
? h.map(e=>`<strong>Вы:</strong> ${e.user}<br><strong>Бот:</strong> ${e.bot}<hr/>`).join('')
: 'История пуста';
histMod.style.display = 'block';
}
window.addEventListener('offline', ()=> {
inputChat.placeholder = 'Нет соединения';
});
window.addEventListener('online', ()=> {
inputChat.placeholder = 'Введите сообщение...';
});
Под каждого пользователя / сессию создается отдельный чат
./src/main.js
document.addEventListener('DOMContentLoaded', () => {
// Инициализируем sessionId (для трекинга)
let sessionId = localStorage.getItem('sessionId');
if (!sessionId) {
sessionId = 'chat-' + Date.now();
localStorage.setItem('sessionId', sessionId);
}
...
Разработка концепции внедрения голосовых запросов и ответов и последующая реализация
./src/js/voice.js
document.addEventListener('DOMContentLoaded', () => {
if (!('SpeechRecognition' in window) && !('webkitSpeechRecognition' in window)) {
console.warn('SpeechRecognition не поддерживается в этом браузере');
return;
}
console.log('SpeechRecognition доступен:', window.SpeechRecognition || window.webkitSpeechRecognition);
let recognition, maleVoice;
window.speechSynthesis.onvoiceschanged = () => {
maleVoice = speechSynthesis.getVoices().find(v => /Male|мужской/i.test(v.name)) || speechSynthesis.getVoices()[0];
};
const micBtn = document.querySelector('.microphone-button');
const inputChat = document.getElementById('input-chat');
micBtn.addEventListener('mousedown', startRec);
micBtn.addEventListener('mouseup', stopRec);
micBtn.addEventListener('touchstart',startRec);
micBtn.addEventListener('touchend', stopRec);
function startRec() {
if (!('SpeechRecognition' in window) && !('webkitSpeechRecognition' in window)) return;
micBtn.classList.add('recording');
recognition = new (window.SpeechRecognition||window.webkitSpeechRecognition)();
recognition.lang = 'ru-RU'; recognition.interimResults = false;
recognition.onresult = e => {
inputChat.value = e.results[0][0].transcript;
};
recognition.onend = () => micBtn.classList.remove('recording');
recognition.start();
}
function stopRec() {
recognition?.stop();
micBtn.classList.remove('recording');
}
});
./index.html
<div id="chat-container">
<div id="chat-status" class="chat-status"></div>
<div id="chat-messages"></div>
<div id="chat-input">
<form id="MyForm">
<input id="input-chat" type="text" placeholder="Введите сообщение..." />
<button id="send-button" type="submit">Отправить</button>
<button id="history-button" type="button">История</button>
</form>
<button id="mic-button" class="microphone-button" aria-label="Начать запись">
||????||
</button>
</div>
<div id="history-modal">
<div id="history-messages"></div>
<button id="close-history">Закрыть</button>
</div>
<div id="error-modal">
<span>Ошибка соединения</span>
<button class="close-modal">OK</button>
</div>
</div>
Расширение интерфейса взаимодействия с AR-контентом через внедрение жестов
./src/js/gestures.js
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('gestureCanvas');
const ctx = canvas.getContext('2d');
function onResults(results) {
// синхронизируем размеры с видео
canvas.width = results.image.width;
canvas.height = results.image.height;
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(results.image, 0, 0, canvas.width, canvas.height);
// отрисовка коннекторов/точек
if (results.multiHandLandmarks) {
for (const landmarks of results.multiHandLandmarks) {
drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color:'#00FF00', lineWidth:4});
drawLandmarks(canvasCtx, landmarks, {color:'#FF0000', lineWidth:2});
// пример: детектим «пинч» …
// пример: детектим «пинч» …
const [thumbTip, indexTip] = [landmarks[4], landmarks[8]];
const dx = (thumbTip.x - indexTip.x) * canvasElement.width;
const dy = (thumbTip.y - indexTip.y) * canvasElement.height;
const dist = Math.hypot(dx, dy);
if (dist < 30) {
const chat = document.getElementById('chat-container');
chat.style.display = (chat.style.display === 'none') ? 'flex' : 'none';
}
// теперь вычисляем ориентацию руки и поворачиваем 3D‑модель
// берем вектор между запястьем (landmarks[0]) и основанием указательного (landmarks[5])
const wrist = landmarks[0];
const indexMCP = landmarks[5];
// в координатах канваса (X вправо, Y вниз) считаем угол
const vx = (indexMCP.x - wrist.x);
const vy = (indexMCP.y - wrist.y);
// atan2(dx, dy) даст угол поворота вокруг Y
const angleY = Math.atan2(vx, vy);
// применяем к модели
const modelEl = document.getElementById('model-entity');
if (modelEl && modelEl.object3D) {
modelEl.object3D.rotation.y = -angleY;
}
}
ctx.restore();
}
}
const hands = new Hands({locateFile: f=>`https://cdn.jsdelivr.net/npm/@mediapipe/hands/${f}`});
hands.setOptions({maxNumHands:1, minDetectionConfidence:0.8, minTrackingConfidence:0.5});
hands.onResults(onResults);
const camera = new Camera(document.createElement('video'), {
onFrame: ()=>hands.send({image: canvas}), width:640, height:480
});
camera.start();
});
./src/js/gestures_texture.js
document.addEventListener('DOMContentLoaded', () => {
AFRAME.registerComponent('gesture-texture', {
init() {
const canvas = document.getElementById('gestureCanvas');
// создаем три.js текстуру из canvas
this.texture = new THREE.CanvasTexture(canvas);
// ждем, когда данный entity будет иметь меш
this.el.addEventListener('model-loaded', () => {
const mesh = this.el.getObject3D('mesh');
mesh.material = new THREE.MeshBasicMaterial({
map: this.texture,
transparent: true
});
});
},
tick() {
// каждую итерацию обновляем текстуру
if (this.texture) this.texture.needsUpdate = true;
}
});
});
Полное решение по описанным выше задачам можно посмотреть здесь: https://disk.360.yandex.ru/d/NoysxxGXYuQ78w/ar_chat_project.zip.
После выбора темы необходимо собрать дополнительные данные по тематике исследования ученого, чтобы в дальнейшем его аватар мог ответить на соответствующие вопросы, а затем сформировать из найденной информации базу знаний для будущего чат-бота.
База знаний не должна быть перегружена большими количеством информации, иначе бот в ней долго будет искать ответы и путаться. Кроме того, она должна иметь структуру заголовков (желательно несколько уровней). Дополнительно можно выделить специальными символами важные моменты (например **).
Фрагмент базы знаний с информацией по нерпам представлен ниже.
# Байкальская нерпа
Байкальская нерпа (лат. Pusa sibirica) - единственный в мире пресноводный тюлень, эндемик озера Байкал. Это одно из самых загадочных животных, поскольку до сих пор точно неизвестно, как она оказалась в этом изолированном водоеме.
# Внешний вид и особенности байкальская нерпа
Нерпы достигают размеров от 1,1 до 1,4 м в длину и весят порядка 50-130 кг. Обычно они темно-серого или серовато-бурого окраса, иногда с небольшими светлыми пятнами. Нерпы имеют круглую голову с большими выразительными глазами. Живут эти замечательные животные около 50-55 лет, что делает их одними из долгожителей среди тюленей.
# Интересные факты о байкальской нерпе
## Физические способности
- Глубоководное погружение:
Нерпы могут погружаться на глубину **до 300 метров**.
- Для сравнения: максимальная глубина погружения человека с использованием газовых смесей составляет 100 м.
- Нерпа способна задерживать дыхание до 25 мин.
- Скорость передвижения:
Несмотря на кажущуюся неповоротливость, нерпа может разгоняться в открытом Байкале до 25 км/ч.
## Особенности зрения
- У нерп невероятно развитое зрение: они видят под водой **лучше, чем на суше.
## Питание
- Миф: нерпа питается омулем.
- Реальность:
- Основной рацион: голомянка и другие виды широколобок и рачки гаммариды.
- Установлено благодаря находке отолитов (твердых образований из ушных проходов рыб) в кишечнике нерп.
- Скорость нерпы недостаточна для охоты на быстрого омуля.
- **Хитрость нерп**: ночью голомянка поднимается ближе к поверхности воды, что облегчает охоту.
## Жировая прослойка и метаболизм
- Жировая прослойка нерпы достигает 14 см.
- Быстрый метаболизм позволяет эффективно использовать пищу, поэтому желудок нерп часто оказывается пустым при исследовании.
## Размножение и развитие детенышей
- Период размножения: конец января - начало февраля.
- Логовища: нерпы строят их внутри торосов на замерзшем Байкале.
- Детеныши:
- Вес новорожденного нерпенка: **3-4 кг** (как у человеческого ребенка).
- За первые 2-2,5 месяца нерпенок набирает 25 кг.
- Молоко нерпы: жирность около 60% (можно резать ножом).
##Эволюционный механизм эмбриональной диапаузы
- Миф: нерпа может «заморозить» беременность до следующего брачного периода.
- Реальность:
- Существует механизм **эмбриональной диапаузы**, который позволяет разнести по времени процессы оплодотворения и активного развития эмбриона.
- Продолжительность диапаузы: 1-1,5 месяца.
- Версия о вынашивании сразу двух детенышей не имеет научного обоснования.
## Культурное значение
- Байкальская нерпа - единственный эндемик млекопитающего озера Байкал.
- Легенды и мифы вокруг нерпы делают ее популярным объектом исследований и лекций среди студентов.
Для презентации результатов необходимо отразить концепт базы знаний — изображение с краткой информацией, содержащейся в базе знаний. В качестве примера концепции приведено решение команды RTX (см. рис. 7.6).
Далее необходимо составить структуру чат-бота, соответствующую сценарию взаимодействия, в том числе обработку ответов вне тематики базы знаний бота (см. рис. 7.7–7.9).
После этого — дополнить базовую схему дополнительными элементами, а именно:
Для правильной интерпретация темы или объекта воспользоваться следующей логикой:
- погрузить все файлы сформированной базы данных;
- переконвертировать их с помощью специального блока в векторные данные;
- с помощью метрики сравнить полученные векторы косинусного расстояния с вопросом пользователя;
- полученной метрикой выбрать контекст, на основе которого бот должен выдать ответ пользователю;
- загрузить контекст и шаблон агенту и получить ответ.
Рис. 7.10.Рис. 7.11.Для запоминания контекста общения (в рамках одной сессии) следует использовать специальный блок, который сохраняет нужную историю сообщений, и последующие вопросы обрабатываются с учетом этой информации (в рамках одной сессии).
Рис. 7.12.- Чтобы бот предложил дополнительные вопросы для продолжения общения необходимо, обработать вывод после вывода агента, т. е. в случае, если он честно признается, что не может ответить пользователю, необходимо вывести информацию о том, на какие темы он может вести повествование; для простоты решения рекомендуется сохранять часть шаблонной информации в архитектуру бота.
Ссылка на итоговую версию чат-бота: https://disk.360.yandex.ru/d/NoysxxGXYuQ78w/Chat-bot.json.
Ссылки на решение:
- Аватар: https://disk.360.yandex.ru/d/NoysxxGXYuQ78w/avatar_nto_solution.glb.
- Объект 1: https://disk.360.yandex.ru/d/NoysxxGXYuQ78w/nto_fish.glb.
- Объект 2: https://disk.360.yandex.ru/d/NoysxxGXYuQ78w/nto_gubka.glb.
- Объект 3: https://disk.360.yandex.ru/d/NoysxxGXYuQ78w/nto_shrimp.glb.
Участникам необходимо изменить представленные модели (https://disk.360.yandex.ru/d/Qbnff7wr7NMALA) в соответствии с референсами личностей и анимировать аватаров. Помимо аватара необходимо также создать три объекта, соотносящихся с выбранной личностью по тематике и соответствующих требованиям.
Аватар создается с помощью изменения одного из готовых мешей на выбор, либо сбора своего аватара из представленных частей (см. рис. 7.14).
Далее следует адаптировать аватара для веба и добавить меш одежды. Суммарное количество треугольников у модели не должно превышать 20000.
В первую очередь аватар изменяется в соответствии с предоставленными справочными материалами и референсами выбранной личности (см. рис. 7.15).
Количество полигонов уменьшается. В эталонном решении задействован модификатор Decimate и ручная доработка. После этого добавляются меши одежды и волос. Итоговый внешний вид эталонного решения без материалов и текстур (см. рис. 7.16).
После создания базы аватара осуществляется проверка аватара на наличие проблем с трансформациями и направлением нормалей. Модель должна быть окрашена при проверке в синий цвет, не должно быть красных полигонов; значения поворота равны (0, 0, 0), масштаба (1, 1, 1) (см. рис. 7.17).
После этого создается развертка, благодаря которой на аватара накладываются материалы. В оценке развертки учитывается:
- плотность (соотношение пустого места к занятому на текстуре),
- пропорциональность размеров островов (голова должна занимать основную часть текстуры),
- растяжение текстуры (допустимо минимальное количество зеленых оттенков или теплее).
Для всего объекта должна быть предусмотрена общая развертка (рис. 7.18), создание отдельной развертки, текстуры и материала под каждый объект снижает общий балл.
Для переноса детализации на текстуры необходимо осуществить запекание, сохраняющее детализацию на карте нормалей (см. рис. 7.19–7.20).
Это позволяет сохранить внешний вид модели, а также адаптировать ее для дальнейшего использования (см. рис. 7.21).
После добавления нормалей накладывается карта цвета, содержащая основную информацию об объекте (см. рис. 7.22). Карта может быть создана из референсов, вручную, с помощью нейросети и т. д. Выходной формат — webp, обеспечивающий сохранение качества и малый размер.
В эталонном решении предусмотрено две текстуры:
- текстура цвета,
- текстура нормалей,
а также один материал для всего объекта, даже если объект разделен на несколько частей.
После создания основы аватара добавляется риг. Учитывается способ создания рига (вручную, автоматически, сторонним приложением), а также распределение веса при деформации объекта. В эталонном решении риг создан вручную, веса откорректированы и при деформации элементов они не искажают соседние (см. рис. 7.23).
Завершением работы над аватаром является добавление анимаций. Оценивается наличие анимации (см. рис. 7.24), а также ее качество и соответствие концепции. Дополнительные анимации оцениваются отдельным критерием.
По окончании работы аватар экспортируется в формате gltf/glb с включенными в файл текстурами и анимациями. Дополнительные объекты (референсы, источники освещения, камеры) не должны попасть в финальный файл.
Для предметов применяется аналогичный подход. Требования по количеству триссов — не более 2000.
Создается меш выбранного объекта (см. рис. 7.25), проверяются его значения трансформации и направление нормалей. В эталонном решении используется меш ленка с 1176 триссами.
Далее создается UV-развертка объекта (см. рис. 7.26), заполняется максимальная область текстуры с минимумом пустот.

Формируется более детализированная версия объекта, и детали переносятся с помощью запекания на изображение текстуры нормалей (см. рис. 7.27).
В заключение, накладывается текстура цвета (см. рис. 7.28). Для всего объекта используется одна текстура цвета, одна текстура нормалей и один материал. Все текстуры сохранены в формате .webp.
Итоговый результат экспортируется в формате gltf/glb, с выделением только объекта, без посторонних элементов (камера, свет, референс и т. д.).



























