Files
hlamingo/README.md
T
alexandClaude Opus 5 bae5116d77 Подсказка названия по фотографии
Фото выбрано в форме — приложение показывает его модели и предлагает название
с прочитанными надписями. «Подставить» переносит в поля, «Не надо» прячет.
Само не подставляется, и это принципиально: модели домысливают марки
и артикулы, когда надписей не видно. В замере клипсу для кабеля модель назвала
«конструктором 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>
2026-07-31 01:24:49 +03:00

392 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` — 1132 с
и часто пустой ответ, потому что это ризонер, которому нужен бюджет
в 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, выбранную модель и доступность.