# hlamingo — помощник «где что лежит» Раскладывать хлам по коробкам бесполезно, если потом не можешь найти нужное: проще заказать деталь заново, чем искать. hlamingo помнит, что и куда положено, и находит по названию, синонимам и маркировке — с телефона, стоя у полки. ## Что нужно - **Windows** и **Python 3.9+** ([python.org](https://www.python.org/downloads/), при установке отметить «Add python.exe to PATH»). - **[Ollama](https://ollama.com)** — на этом же компьютере или на любой машине в локальной сети. Без него приложение тоже работает: остаются кнопки, места, коробки, фото и поиск по названию и тегам; отключаются только разбор свободных фраз, автоматические теги и поиск по смыслу. Модели (на машине с 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` иногда присылает объект вместо списка тегов — разбор это переживает, но модель, совсем не держащая формат, будет постоянно сваливаться на регулярки. - **Эмбеддер обязан быть многоязычным** — см. раздел про поиск. Модель выбирается в настройках (вкладка **⚙**) из реально установленных. После смены у записанных вещей теги остаются прежними; обновить их можно кнопкой **«🔄 Пересоздать теги»** в карточке. ## Установка Положите `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, в котором опции «Камера» нет вообще — только готовые файлы. Одним полем оба сценария не покрыть, поэтому в разметке два ``: с `capture` и без. Снимок ужимается в браузере до 1280 px, файлы лежат в `static\photos\`. Новое фото заменяет прежнее; «🗑 Фото» убирает снимок и удаляет файл с диска. `getUserMedia` не используется — он требует HTTPS и по локальному адресу `http://192.168.0.x:5000` просто не запустился бы. ## Как работает поиск Три сигнала, по убыванию надёжности: 1. **По словам** — основа. Запрос и карточка режутся на слова, слова грубо приводятся к основе, так что «стабилизаторы», «стабилизатора» и «стабилизатор» — одно и то же. Работает всегда, даже без Ollama. 2. **Синонимы от модели.** При записи новой вещи модель придумывает альтернативные названия: «изолента» → «изоляционная лента», «ПВХ-лента»; «Стабилизатор 7805» → «LM7805», «стабилизатор 5 вольт», «КР142ЕН5А». Они лежат в карточке и расширяют словарь предмета. 3. **По смыслу (эмбеддинги)** — только если по словам ничего не нашлось, с высоким порогом и с пометкой «похоже по смыслу». Эмбеддер — **`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`: тот ризонер, пишет `` прямо в ответ и медленнее. ## Устройство ``` 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, выбранную модель и доступность.