Алекс пересоздавал теги для вещи «Светодиодная лампа LED (навигатор)»
и получил «лампа для навигатора», «лампа для GPS-навигатора», «навигационная
лампа». Модель прочитала «навигатор» как назначение, а не как марку Navigator,
и по таким тегам вещь уже не найти.
Правкой промпта это лечилось лишь наполовину: правила и запреты убирали часть
мусора, но «лампа для навигатора» держалась. Написание марки латиницей
помогало, но переименовывать все вещи — не решение.
Настоящее решение подсказал сам Алекс: у вещи есть фотография, и на упаковке
написана настоящая маркировка. Теперь пересоздание тегов сначала читает
надписи с фото и отдаёт их модели как достоверные данные.
Результат на той же вещи:
было: лампа для навигатора, лампа для GPS-навигатора
стало: лампа Navigator, встроенный светодиодный светильник, лампа DOWNLIGHT,
лампа 10 Вт, лампа с холодным белым светом
С коробки прочиталось: Navigator, DOWNLIGHT, 10Вт, IP44, 90°, холодный белый
свет — заодно выяснилось, что это не лампочка, а встраиваемый светильник.
В промпт добавлены два разобранных примера: марка в названии — это марка,
а не назначение вещи; и не приписывать артикулы, которых никто не давал
(на «шуруповёрт Makita» модель выдумывала «Makita XA020, 18 В»).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 KiB
hlamingo — помощник «где что лежит»
Раскладывать хлам по коробкам бесполезно, если потом не можешь найти нужное: проще заказать деталь заново, чем искать. hlamingo помнит, что и куда положено, и находит по названию, синонимам и маркировке — с телефона, стоя у полки.
Что нужно
- Windows и Python 3.9+ (python.org, при установке отметить «Add python.exe to PATH»).
- Ollama — на этом же компьютере или на любой машине в локальной сети. Без него приложение тоже работает: остаются кнопки, места, коробки, фото и поиск по названию и тегам; отключаются только разбор свободных фраз, автоматические теги и поиск по смыслу.
Модели (на машине с Ollama):
ollama pull qwen3:8b разбор фраз и теги
ollama pull bge-m3 поиск по смыслу
ollama pull gemma3:4b подсказка названия по фото (необязательно)
Какую модель выбрать
Обе задачи русские, и требование к модели одно: она должна думать по-русски, а не переводить с английского на ходу. Замеры на наших же задачах — разбор восьми фраз и генерация тегов для шести предметов:
| Модель | Размер | Разбор фраз | Русский в тегах | Скорость |
|---|---|---|---|---|
qwen3:8b |
5 ГБ | 8/8 | чистый | 2,5 с на предмет |
qwen2.5:7b |
4,7 ГБ | 8/8 | «электротool Makita» | 1,8 с |
Разница видна именно на тегах — по ним вещь потом и находится:
qwen2.5: шуруповёрт Makita → «электротool Makita», «удлинитель для шуруповерта»
мультиметр → «многофункциональный мера», «вoltage meter»
qwen3: изолента → «изоляционная лента», «лента для изоляции», «ПВХ-лента»
саморезы 4х50 → «шурупы по дереву», «крепёжный элемент»
У qwen2.5 в слове «вoltage» первая буква кириллическая, а «электротool» —
гибрид двух языков. В поиске такие теги бесполезны.
Что учесть, выбирая свою модель:
- Меньше 7B на русском обычно плохи — путают падежи и сваливаются
в английский.
qwen2.5-coder:1.5bдля этой задачи не годится. - Модели-ризонеры (
qwen3,deepseek-r1) тратят бюджет ответа на размышления и возвращают пустоту. Вollama.pyэто гаситсяthink=False; добавляя свою, проверьте, что она отвечает, а не молчит. - Формат важнее красноречия. Приложение ждёт строгий JSON.
qwen3иногда присылает объект вместо списка тегов — разбор это переживает, но модель, совсем не держащая формат, будет постоянно сваливаться на регулярки. - Эмбеддер обязан быть многоязычным — см. раздел про поиск.
Модель выбирается в настройках (вкладка ⚙) из реально установленных. После смены у записанных вещей теги остаются прежними; обновить их можно кнопкой «🔄 Пересоздать теги» в карточке.
Пересоздание учитывает фотографию. Если у вещи есть снимок, приложение сначала читает с него надписи и отдаёт их модели как достоверную маркировку. Разница на живом примере — вещь «Светодиодная лампа LED (навигатор)»:
без фото: «лампа для навигатора», «лампа для GPS-навигатора» ← модель
решила, что это лампа для навигатора
с фото: «лампа Navigator», «встроенный светодиодный светильник»,
«лампа DOWNLIGHT», «лампа 10 Вт»
С коробки прочиталось Navigator, DOWNLIGHT, 10Вт, IP44, 90°, холодный белый свет — и выяснилось, что это вообще не лампочка, а встраиваемый светильник.
На упаковке написана настоящая маркировка, гадать по названию не нужно.
Установка
Положите install.cmd в пустую папку и запустите — он скачает проект, создаст
виртуальное окружение, поставит зависимости, спросит адрес Ollama и сделает
ярлык на рабочем столе.
Если проект уже скачан:
setup.cmd один раз: создаёт venv и ставит Flask ВНУТРЬ него
start.cmd запускает сервер и печатает адреса для телефона
Глобальный Python не затрагивается — всё живёт в venv\ внутри проекта.
Запуск
start.cmd печатает адрес, который надо открыть на телефоне, — вида
http://192.168.1.50:5000. На самом компьютере — http://localhost:5000.
При первом запуске Windows спросит про брандмауэр — разрешить для частной сети. Иначе телефон до сервера не достучится, а выглядеть это будет как «страница не грузится».
Настройки
Проще всего — в браузере: вкладка ⚙. Там указывается адрес Ollama, кнопка «Проверить связь» говорит, отвечает ли он и сколько там моделей, а модели выбираются из списка реально установленных. Сохранённое применяется сразу, перезапуск не нужен. Если Ollama не отвечает, наверху появляется предупреждение со ссылкой прямо в настройки.
Инсталлятор спрашивает адрес в консоли при установке, но это можно пропустить (Enter) и задать потом в браузере.
Под капотом всё это — файл .env в корне проекта (образец — .env.example);
в репозиторий он не попадает. Переменные окружения имеют приоритет над файлом.
| Переменная | По умолчанию | Зачем |
|---|---|---|
OLLAMA_HOST |
http://localhost:11434 |
адрес Ollama |
HLAMINGO_CHAT_MODEL |
qwen3:8b |
разбор фраз и теги |
HLAMINGO_EMBED_MODEL |
bge-m3 |
поиск по смыслу |
HLAMINGO_VISION_MODEL |
gemma3:4b |
подсказка по фото; пусто — выключено |
HLAMINGO_PORT |
5000 |
порт веб-интерфейса |
HLAMINGO_DATA_DIR |
— | другая папка для данных и .env; используется тестами |
Из браузера правятся только OLLAMA_HOST и две модели — по белому списку.
Произвольные ключи в .env через веб записать нельзя: этот файл читает
приложение при запуске. Адрес проверяется на формат, имена моделей — на
недопустимые символы, и если хоть одно значение неверно, не применяется
ни одно (иначе половина настроек принята, половина отвергнута).
Как пользоваться
Вкладка «Действие» — четыре кнопки
| Кнопка | Что делает |
|---|---|
| Положить | Выбираешь место, выбираешь коробку, вписываешь предмет |
| Забрать | Вещь уходит «на руках» — больше не числится в коробке |
| Переместить | Вещь переезжает в другую коробку или место |
| Удалить | Убирает запись совсем |
Найденное и содержимое коробок показывается плиткой, как витрина в магазине: сколько колонок влезает в экран, столько и будет. Кнопки на карточках — иконками: 📷 снять, 🖼 из галереи, ✏️ изменить, 🗑 убрать фото. При правке карточка растягивается на всю ширину, чтобы поля были нормального размера.
Ввод структурный: место → ёмкость → предмет. Место и коробку выбираешь из списков, поэтому ИИ в самом ответственном месте не участвует и ошибиться разбором фразы негде. Новое место добавляется внутрь выбранного — так и строится «Гараж → Стеллаж А → Полка 2». Коробки маркируются кодом «буква+цифра».
Кнопка «Забрать» важнее, чем кажется: без неё взятая и израсходованная деталь продолжала бы числиться в коробке, и поиск отправлял бы копать пустое место.
Правка
Всё можно поправить и удалить — предмет, ёмкость, место.
- Предмет. Кнопка «✏️ Изменить» на карточке: название, количество, заметка и теги. Теги снимаются нажатием на «×», новые добавляются полем внизу — модель регулярно придумывает мусор вроде «электротool Makita», и его надо убирать руками. По тегам вещь и находится, так что состав важен.
- Ёмкость. В «Обзоре» нажмите коробку → «✏️ Изменить»: код, тип, место, заметка. Смена кода безопасна — ссылки вещей переезжают вместе с ней.
- Место. В «Обзоре» нажмите на название места (у него значок ✏️): переименовать или перенести внутрь другого.
Кнопка «🗑 Фото» убирает снимок и удаляет файл с диска, чтобы папка не копила мусор.
Удаление устроено так, чтобы записи о вещах не пропадали молча:
| Что удаляем | Что происходит |
|---|---|
| Предмет | Удаляется вместе со своим фото |
| Коробка | Удаляется, а вещи из неё остаются в её месте — не теряются |
| Место | Только пустое. Иначе покажет, что внутри, и не тронет |
Иерархия защищена от петель: место нельзя перенести внутрь вложенного в него или внутрь самого себя — иначе сборка адреса зацикливалась бы.
Свободная фраза
Под формой есть «Или просто напиши фразой» — для быстрого ввода:
положил стабилизаторы 7805, 12 штук, в коробку A3 на второй полке стеллажа А в гараже
переложил дрель из коробки A1 в коробку C2
где мультиметр
что в коробке A3
Модель разбирает фразу за ~1,5 с, места из неё создаются сами.
Подсказка по фотографии
Когда фото выбрано в форме заведения вещи, приложение показывает его модели и предлагает название с прочитанными надписями:
фото коробки лампы → «Лампочка LED E14»
надписи: Лампа светодиодная C35, Нейтральный белый, 9 Вт LED, E14
Кнопка «✓ Подставить» переносит это в поля, «Не надо» прячет. Само оно не подставляется, и это принципиально: модели домысливают марки и артикулы, когда надписей не видно. В замере клипсу для кабеля модель назвала «конструктором LEGO Technic, 8376, 401 шт.». Поэтому — только подсказка, решает человек.
Смотрит gemma3:4b (HLAMINGO_VISION_MODEL, 3,3 ГБ). Выбрана замером
на настоящих снимках: 3,5 с и всегда отвечает, тогда как qwen3-vl:8b — 11–32 с
и часто пустой ответ, потому что это ризонер, которому нужен бюджет
в 2000+ токенов. Перед отправкой картинка уменьшается до 768 px: снимок
разворачивается в токены, а для чтения крупных надписей столько не нужно.
Распознавание отключается пустым HLAMINGO_VISION_MODEL — тогда блок
подсказки просто не появляется.
Фото
Две кнопки: «📷 Снять» открывает камеру, «🖼» — готовые снимки в галерее. Обе есть и в форме заведения вещи (с превью, фото прицепится к новой записи), и на карточке любой вещи в «Найти» и «Обзоре». У коробки тоже.
Почему две кнопки, а не одна. С атрибутом capture телефон открывает
камеру, но галерея недоступна. Без capture Android 13+ показывает системный
Photo Picker, в котором опции «Камера» нет вообще — только готовые файлы.
Одним полем оба сценария не покрыть, поэтому в разметке два <input>:
с capture и без.
Снимок ужимается в браузере до 1280 px, файлы лежат в static\photos\.
Новое фото заменяет прежнее; «🗑 Фото» убирает снимок и удаляет файл с диска.
getUserMedia не используется — он требует HTTPS и по локальному адресу
http://192.168.0.x:5000 просто не запустился бы.
Как работает поиск
Три сигнала, по убыванию надёжности:
- По словам — основа. Запрос и карточка режутся на слова, слова грубо приводятся к основе, так что «стабилизаторы», «стабилизатора» и «стабилизатор» — одно и то же. Работает всегда, даже без Ollama.
- Синонимы от модели. При записи новой вещи модель придумывает альтернативные названия: «изолента» → «изоляционная лента», «ПВХ-лента»; «Стабилизатор 7805» → «LM7805», «стабилизатор 5 вольт», «КР142ЕН5А». Они лежат в карточке и расширяют словарь предмета.
- По смыслу (эмбеддинги) — только если по словам ничего не нашлось, с высоким порогом и с пометкой «похоже по смыслу».
Эмбеддер — bge-m3, многоязычный. Прежний nomic-embed-text на русском
смыслы почти не разделял: «велосипедная камера» набирала мультиметру 0.704 —
больше верного ответа на «паяльник» (0.691). Порогом такое не разделить.
С bge-m3 разделение появилось: мусор ушёл ниже 0.46, верные держатся выше 0.53.
Но и с ним семантика остаётся догадкой, а не основой. Замер на настоящей базе
из шести десятков вещей честнее лабораторного: абстрактное «чем посветить»
набирало лампам всего 0.411, а посторонняя «детская коляска» — 0.535. Полностью
они не разделяются, поэтому SEMANTIC_MIN в search.py выставлен так, чтобы
отсечь весь мусор ценой слабых догадок: помощник, уверенно показывающий не то,
хуже промолчавшего. Точных запросов это не касается — они находятся по словам
и тегам.
При смене эмбеддера поменять EMBED_MODEL в config.py, перемерить порог
(у каждой модели своя шкала) и обязательно пересчитать векторы:
venv\Scripts\python.exe tools\reindex.py
Пересчёт не опция: векторы разных моделей несовместимы, и смешанная база тихо испортит поиск, ничем себя не выдав. Скрипт сам делает копию базы перед работой.
Что если Ollama выключен
Приложение работает и без Ollama вообще — можно поставить и пользоваться, не устанавливая её. Наверху появится предупреждение со ссылкой в настройки, а всё основное останется на месте.
| Ollama есть | Ollama нет | |
|---|---|---|
| Кнопки, места, коробки, фото, обзор | да | да |
| Поиск по названию и синонимам | да | да |
| Синонимы для новых вещей | да | нет |
| Разбор свободных фраз | модель | упрощённо, регулярками |
| Поиск по смыслу | да | нет |
Ожидания это не добавляет. Если машина с Ollama выключена, пакеты уходят
в никуда, и без защиты каждый вызов честно ждал бы свой таймаут: замер показал
92 секунды на запись одной вещи и 154 на разбор фразы — выглядит как зависание.
Поэтому в ollama.py стоит предохранитель: перед дорогим запросом идёт быстрая
проверка соединения (полторы секунды вместо двух минут), а после неудачи
приложение полминуты вообще не тревожит сервер. Замер после: те же операции
отвечают мгновенно. Смена адреса в настройках сбрасывает паузу сразу.
Модели: qwen2.5:7b для разбора фраз (в конфиге — CHAT_MODEL).
Не omnicoder-9b: тот ризонер, пишет <think> прямо в ответ и медленнее.
Устройство
hlamingo\
config.py адрес Ollama, модели, пути, порог поиска
ollama.py клиент: chat / embeddings / проверка доступности
store.py места (иерархия), ёмкости, предметы; атомарная запись базы
search.py поиск по словам + смысловая догадка
brain.py разбор фразы моделью, синонимы, фолбэк на регулярки
app.py Flask: маршруты
templates\index.html вся страница
data\db.json база (в .gitignore)
static\photos\ фотографии (в .gitignore)
Три сущности вместо плоского «предмет → строка места»:
- место — узел иерархии, ссылается на родителя;
- ёмкость — коробка с кодом, стоит в каком-то месте;
- предмет — лежит либо в ёмкости, либо прямо в месте (лопата в углу), либо «на руках».
Полный адрес собирается подъёмом по родителям: Гараж → Стеллаж А → Полка 2 → коробка A3. У каждого предмета хранится история перемещений.
Мелочи, которые заметно экономят нервы:
- Коды коробок терпимы к раскладке. Коробка подписана от руки «А3» кириллицей, а в поиске набирается «A3» латиницей — для hlamingo это одна и та же коробка.
- Резервные копии. Перед каждой перезаписью прошлое состояние базы уходит
в
data\backups\db-ГГГГММДД-ЧЧММ.json, хранятся последние 40. Откат — просто скопировать нужный файл обратно вdata\db.json. Полноценный бэкап папки они не заменяют, но спасают от затёртой или испорченной базы: однажды она уже потерялась целиком, и откатываться было некуда. - Запись базы атомарная (временный файл + подмена): обрыв на середине не уничтожит всё, что записано про вещи.
- Битая база не роняет приложение — запускается с пустой, файл не трогает.
- Имена файлов фото генерируются сервером, имя от клиента не используется.
Ограничения
- Аутентификации нет: помощник рассчитан на домашнюю локальную сеть.
- Количество не пересчитывается при «забрать» — состояние вещи целиком «в коробке» или «на руках».
- Фото не распознаётся, это иллюстрация. Автоопределение содержимого потребует
vision-модели (
llava,qwen2.5-vl) — на сервере такой сейчас нет.
Ярлык на телефоне
Откройте страницу и в меню браузера выберите «Добавить на главный экран».
Ярлык получит иконку коробки, а приложение откроется без адресной строки
(display: standalone в манифесте).
Иконки лежат в static\ и рисуются генератором — при желании перерисовать:
venv\Scripts\python.exe tools\make_icons.py
tools\make_icons.py пишет PNG своим минимальным энкодером, без Pillow:
иконки нужны один раз, и зависимость ради них в проекте лишняя. Содержимое
держится внутри центральных 80 % — Android обрезает иконку по маске
(круг, скруглённый квадрат), и края могут срезаться.
Что зачем нужно:
| Файл | Кому |
|---|---|
manifest.webmanifest |
Android: имя, иконка ярлыка, запуск без адресной строки |
apple-touch-icon.png |
iOS: иконка ярлыка (манифест там не используется) |
icon-192.png, icon-512.png |
иконки из манифеста |
favicon-32.png |
вкладка браузера и запрос /favicon.ico |
Манифест отдаётся отдельным маршрутом с типом application/manifest+json:
из static/ он ушёл бы как обычный файл, и браузер мог не признать его
манифестом.
Тесты
tests\run.cmd
56 проверок: иерархия мест, ёмкости, поиск, правка, фото, разбор фраз. Часть требует Ollama — без него они пропускаются, а не падают.
Тесты работают только в песочнице. tests\run.cmd выставляет
HLAMINGO_DATA_DIR на временную папку, а tests\guard.py отказывается
запускаться, если приложение всё же смотрит на настоящие данные.
Это не перестраховка: однажды тест начинался с удаления рабочей папки data,
и один прогон уничтожил реальную базу с вещами. Восстановить её не удалось —
os.replace не оставляет следов, в корзину rmtree не кладёт. Уцелели только
фотографии, потому что их файлы тест не трогал. Поэтому путь к данным теперь
берётся из окружения, и никакой тест не может дотянуться до живой базы.
Отладка
Живо ли Ollama и какие модели установлены (подставьте свой адрес):
curl http://localhost:11434/api/tags
Что об этом думает само приложение: http://localhost:5000/api/health —
отдаёт адрес Ollama, выбранную модель и доступность.