Фото выбрано в форме — приложение показывает его модели и предлагает название
с прочитанными надписями. «Подставить» переносит в поля, «Не надо» прячет.
Само не подставляется, и это принципиально: модели домысливают марки
и артикулы, когда надписей не видно. В замере клипсу для кабеля модель назвала
«конструктором LEGO Technic, 8376, 401 шт.». Запрет на выдумывание в промпте
помог: та же вещь стала «красной щёткой для посуды».
Смотрит gemma3:4b. Выбрана замером на настоящих снимках из базы:
gemma3:4b 3.5 с, отвечает всегда, надписи читает верно
(«Лампа светодиодная C35, Нейтральный белый, 9 Вт LED, E14»);
qwen3-vl:8b 11-32 с и часто пустой ответ — это ризонер, ему нужен бюджет
в 2000+ токенов, и уменьшение картинки не спасает.
Проверялась и гипотеза, что дело в размере снимка: у gemma3 разницы между
1280 и 512 px почти нет, у qwen3-vl время падает с 62 до 41 с, но ответ
всё равно пуст. Картинка всё же уменьшается до 768 px — незачем гонять лишнее.
Генерация тегов чинилась по ходу: на «Стабилизатор 7805 (TO-220)» модель
отвечала массивом объектов с пояснениями, не укладывалась в 256 токенов,
и JSON обрывался на полуслове — теги выходили пустыми. Бюджет поднят до 500,
промпт требует плоский массив строк, а из битого ответа строки теперь
вытаскиваются регуляркой: лучше несколько живых тегов, чем ничего.
Pillow добавлен в зависимости — для уменьшения картинки перед распознаванием.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
392 lines
27 KiB
Markdown
392 lines
27 KiB
Markdown
# 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, в котором опции «Камера» нет вообще — только готовые файлы.
|
||
Одним полем оба сценария не покрыть, поэтому в разметке два `<input>`:
|
||
с `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`: тот ризонер, пишет `<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, выбранную модель и доступность.
|