В последнее время появляется все больше инструментов и приложений, которые решают различные задачи при помощи ИИ. Некоторые из них, можно было решить и раньше, но не так качественно (различные решения для извлечения данных из документов), а некоторые, вероятно, можно решить только сейчас. Один из таких кейсов был и у меня - я хотел написать приложение, чтобы было удобно делить чек по фотографии.
В принципе, задачу можно решить и без ИИ - для распознавания текста уже есть OCR-технологии, дальше, по-сути, самый обычный CRUD. Но, после первых экспериментов я чуть было не оставил эту задачу насовсем - OCR хоть и работает, но качество распознавания оставляет желать лучшего. Чтобы улучшить качество, нужно применять фильтры, чтобы текст был максимально контрастным. Да и в целом - качество фотографии влияет очень сильно, и это я еще не дошел до фильтрации текста (в чеке, очевидно, не только список товаров).
Тогда я и решил попробовать для распознавания текста LLM, тем более, что этим подходом можно “убить двух зайцев” - с фильтрацией современные модели точно справятся. В принципе, решение уже было рабочее, но делиться я им не спешил - теперь нужно было платить не только за виртуальный сервер, но и за запросы к API. Тогда я и начал искать решение - хотелось бы иметь полностью статичный клиент и вообще обойтись без сервера, но тогда за распознавание должен отвечать пользователь. Тут решение тоже есть - у нас уже есть стандарт для описания навыков для ИИ-агентов, т.е. задачу можно описать в виде инструкции и отдать агенту, а на клиенте оставить только взаимодействие с пользователем.
Как мне показалось, вариант отличный, но приложение должно работать в первую очередь на телефоне, а как там с запуском ИИ-агентов? На самом деле, такая возможность есть, причем модель в этом случае работает локально - нужно приложение Edge Gallery от Google. В приложении выбираем модель для работы с агентскими сценариями и после загрузки можно начинать пользоваться. Сейчас доступны несколько предустановленных навыков, но мне было интересно попробовать написать свой - для своей задачи.
Пишем навык для приложения
В первую очередь я решил разобраться с документацией - там уже описан небольшой пример, как писать свои навыки. Навыки могут быть простыми, т.е. текстовыми или с поддержкой JS, в таком случае у разработчика есть возможность работать с окружением, доступным внутри WebView и описывать методы, которые будут доступны агенту. И это ровно то, что нужно - я хотел написать приложение вообще без сервера, так что мне нужен какой-то способ передавать результат распознавания из ответа модели в приложение, и это все-таки лучше доверить какому-нибудь алгоритму.
Структура навыка выглядит так:
my-js-skill/
├── SKILL.md
└── scripts/
└── index.html
На самом деле не важно, с чего начинать, но нужно учесть, что агенту нужно явно указать - ему нужно вызвать инструмент run_js:
---
name: split-bill-rzdeli
description: Split the bill using the rzdeli.ru service
---
# Split bill from image (rzdeli.ru)
## Instructions
Call the `run_js` tool with the following exact parameters:
- script name: index.html
- data: A JSON string with the items from the check with format `Array<{ title: string, price: number, count: number }>`
- If there are no items in the check, pass an empty array.
- The item names can be multiple lines long.
- The price field can be named "Сумма".
Тут можно заметить, что я явно указал формат данных, в котором модель должна вызывать JS-метод, и некоторые другие инструкции, которые я вывел, пока тестировал распознавание на локальной модели.
А теперь можно описать и сам метод - его нужно явно в виде асинхронного метода под именем ai_edge_gallery_get_result, именно такое имя ожидает инструмент run_js:
<!doctype html>
<html lang="en">
<head></head>
<body>
<script>
window['ai_edge_gallery_get_result'] = async (data) => {
try {
const url = new URL('https://rzdeli.ru/list')
return JSON.stringify({
result: 'Here is the interactive view.',
webview: {
url: url,
aspectRatio: 1.0,
},
})
} catch (e) {
return JSON.stringify({
error: `Failed: ${e.message}`,
})
}
}
</script>
</body>
</html>
Тут так же важно выделить формат ответа - от него будет зависеть, как ответ отобразится для пользователя.
interface Result {
error?: string // если в результате работы есть ошибка, достаточно вернуть только это поле с текстом ошибки
result: string // в случае успеха возвращается текстовый ответ
webview?: { // нужно для интерактивного превью - в чате будет небольшое окно с возможностью открыть страницу на полный экран
url: string, // можно указать как относительный путь, так и сторонний сервис
aspectRatio: number // по умолчанию 1.0
}
image?: { // очевидно, для отрисовки изображения в чате
base64: string // строка в формате base64
}
}
Сейчас уже можно протестировать навык, нужно лишь добавить его в приложение. Для импорта у нас есть три варианта:
- Импорт из списка community-навыков. Вероятно, ревью проводит Google и просто так туда не попасть.
- Импорт из исходников - можно просто загрузить все нужные файлы на устройство и импортировать навык из файловой системы. Можно пользоваться, но неудобно отлаживать.
- Импорт по URL - самый простой вариант, но нужно учесть, что приложение не может загрузить навык напрямую из Github и требует, чтобы навык хостился на настоящем сервере.
Проблему с хостингом очень легко решить - весь репозиторий статичный и достаточно загрузить это все на Github pages, после этого приложение успешно загрузит навык.
Дополнительно отмечу, что в документации упоминается раздел “Native Skills”, который позволяет вызывать нативные API, однако в этом случае разработчику предполагается самостоятельно расширять доступный API.
Пишем клиентское приложение
На этом моменте у нас почти все готово - модель умеет распознавать текст из чека и умеет передавать его в JS-скрипт, осталось только отобразить это все пользователю.
Я решил не усложнять и написал приложение на React и shadcn, а список товаров получать из query-параметра. Для передачи списка позиций в чеке я выбрал base64, но он не умеет работать с UTF-8, поэтому пришлось применить пару хаков на кодировании и декодировании, но главное, что все работает.
Теперь нужно лищь добавить в навык метод для формирования корректной base64 строки и возвращать пользователю полную ссылку:
function utf8_to_b64(str) {
return window.btoa(unescape(encodeURIComponent(str)));
}
Выводы
Да, скорее всего, не получится решить любую задачу подобным образом - вынести вычисления на устройство пользователя и предоставить только полностью статичный клиент, но, скорее всего, задач, которые можно решить подобным образом, не мало. Нужно лишь учитывать чувствительность данных и мощности локальных моделей, которые сейчас не очень высокие, но, тем не менее позволяют решать и такие задачи.
Стоит отметить отдельно, что такие решения не предполагают создание приложения, которое можно продать пользователю - это всего лишь один из вариантов решения моей (или вашей) проблемы и в этом случае важно сделать решение максимально дешевым. Надеюсь, это небольшое руководство поможет решить и несколько ваших задач.