Files
hlamingo/README.md
T
alexandClaude Opus 5 9cbc7f02ac Обзор и поиск — плиткой, как витрина в магазине
Раньше карточки шли одна под другой, и коробка с двумя десятками вещей
превращалась в длинную колонку. Теперь сетка на CSS grid с auto-fill: сколько
колонок влезает в экран, столько и будет — на телефоне обычно две,
на планшете три-четыре.

Чтобы плитка была компактной:
- превью квадратные (object-fit: cover) — ряд выглядит ровным;
- кнопки только иконками, подписи в них не влезали; что означает какая,
  написано подсказкой под сеткой и в title;
- теги в плитке скрыты, они её распирали — видны при правке;
- карточка с открытой правкой растягивается на всю ширину, иначе поля
  ввода оказались бы в колонке шириной с ладонь.

Один результат сеткой не оформляется: выглядело бы как обрезанная витрина.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:03:17 +03:00

309 lines
20 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 qwen2.5:7b разбор фраз и теги
ollama pull nomic-embed-text поиск по смыслу (необязательно)
```
## Установка
Положите `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` | `qwen2.5:7b` | разбор фраз и теги |
| `HLAMINGO_EMBED_MODEL` | `nomic-embed-text:latest` | поиск по смыслу |
| `HLAMINGO_PORT` | `5000` | порт веб-интерфейса |
| `HLAMINGO_DATA_DIR` | — | другая папка для данных и `.env`; используется тестами |
Из браузера правятся только `OLLAMA_HOST` и две модели — по белому списку.
Произвольные ключи в `.env` через веб записать нельзя: этот файл читает
приложение при запуске. Адрес проверяется на формат, имена моделей — на
недопустимые символы, и если хоть одно значение неверно, не применяется
ни одно (иначе половина настроек принята, половина отвергнута).
## Как пользоваться
### Вкладка «Действие» — четыре кнопки
| Кнопка | Что делает |
|---|---|
| **Положить** | Выбираешь место, выбираешь коробку, вписываешь предмет |
| **Забрать** | Вещь уходит «на руках» — больше не числится в коробке |
| **Переместить** | Вещь переезжает в другую коробку или место |
| **Удалить** | Убирает запись совсем |
Найденное и содержимое коробок показывается плиткой, как витрина в магазине:
сколько колонок влезает в экран, столько и будет. Кнопки на карточках —
иконками: 📷 снять, 🖼 из галереи, ✏️ изменить, 🗑 убрать фото. При правке
карточка растягивается на всю ширину, чтобы поля были нормального размера.
Ввод структурный: **место → ёмкость → предмет**. Место и коробку выбираешь из
списков, поэтому ИИ в самом ответственном месте не участвует и ошибиться
разбором фразы негде. Новое место добавляется *внутрь* выбранного — так и
строится «Гараж → Стеллаж А → Полка 2». Коробки маркируются кодом «буква+цифра».
Кнопка **«Забрать»** важнее, чем кажется: без неё взятая и израсходованная
деталь продолжала бы числиться в коробке, и поиск отправлял бы копать пустое место.
### Правка
Всё можно поправить и удалить — предмет, ёмкость, место.
- **Предмет.** Кнопка **«✏️ Изменить»** на карточке: название, количество,
заметка и теги. Теги снимаются нажатием на «×», новые добавляются полем
внизу — модель регулярно придумывает мусор вроде «электротool Makita»,
и его надо убирать руками. По тегам вещь и находится, так что состав важен.
- **Ёмкость.** В «Обзоре» нажмите коробку → «✏️ Изменить»: код, тип, место,
заметка. **Смена кода безопасна** — ссылки вещей переезжают вместе с ней.
- **Место.** В «Обзоре» нажмите на название места (у него значок ✏️):
переименовать или перенести внутрь другого.
Кнопка **«🗑 Фото»** убирает снимок и удаляет файл с диска, чтобы папка
не копила мусор.
Удаление устроено так, чтобы записи о вещах не пропадали молча:
| Что удаляем | Что происходит |
|---|---|
| Предмет | Удаляется вместе со своим фото |
| Коробка | Удаляется, а вещи из неё остаются в её месте — не теряются |
| Место | Только пустое. Иначе покажет, что внутри, и не тронет |
Иерархия защищена от петель: место нельзя перенести внутрь вложенного в него
или внутрь самого себя — иначе сборка адреса зацикливалась бы.
### Свободная фраза
Под формой есть «Или просто напиши фразой» — для быстрого ввода:
```
положил стабилизаторы 7805, 12 штук, в коробку A3 на второй полке стеллажа А в гараже
переложил дрель из коробки A1 в коробку C2
где мультиметр
что в коробке A3
```
Модель разбирает фразу за ~1,5 с, места из неё создаются сами.
### Фото
Две кнопки: **«📷 Снять»** открывает камеру, **«🖼»** — готовые снимки в галерее.
Обе есть и в форме заведения вещи (с превью, фото прицепится к новой записи),
и на карточке любой вещи в «Найти» и «Обзоре». У коробки тоже.
**Почему две кнопки, а не одна.** С атрибутом `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. **По смыслу (эмбеддинги)** — только если по словам ничего не нашлось,
с высоким порогом и с пометкой «похоже по смыслу».
Почему семантика на третьем месте, а не на первом: замер на `nomic-embed-text`
показал, что на русском он почти не разделяет смыслы — «велосипедная камера»
давала мультиметру 0.689, а верное попадание на «паяльник» — 0.691. Порогом
такое не разделить, поэтому лучше честное «не нашёл», чем уверенно показанное
не то. Хочется настоящего смыслового поиска — нужен многоязычный эмбеддер:
```
ollama pull bge-m3
```
затем в `hlamingo\config.py` сменить `EMBED_MODEL`, обнулить `EMBED_DOC_PREFIX`
и `EMBED_QUERY_PREFIX` и **один раз пересчитать векторы**`search.reindex(db)`.
Пересчёт обязателен: векторы разных моделей несовместимы, и смешанная база
тихо испортит поиск, ничем себя не выдав.
## Что если Ollama выключен
Если Ollama на отдельной машине, она бывает выключена. Приложение это переживает:
наверху появляется предупреждение, а работает почти всё.
| | Ollama есть | Ollama нет |
|---|---|---|
| Кнопки, места, коробки, фото, обзор | да | да |
| Поиск по названию и синонимам | да | да |
| Синонимы для новых вещей | да | нет |
| Разбор свободных фраз | модель | упрощённо, регулярками |
| Поиск по смыслу | да | нет |
Модели: `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, выбранную модель и доступность.