hlamingo — помощник «где что лежит»
Веб-помощник для домашнего склада: помнит, в какой коробке и в каком месте лежит вещь, и находит её по названию, тегам и маркировке. Открывается с телефона, интеллект — локальная Ollama. Основное: - структурный ввод «место → ёмкость → предмет» и кнопки Положить / Забрать / Переместить / Удалить; - иерархия мест (Гараж → Стеллаж А → Полка 2), ёмкости с кодом «буква+цифра», состояние «на руках» для взятых вещей; - поиск по словам с учётом русских окончаний, теги от модели расширяют словарь предмета, смысловой поиск — вспомогательный (замеры в search.py); - фото к вещам и коробкам: съёмка и выбор из галереи; - правка и удаление всех сущностей, удаление коробки не теряет её содержимое; - работает и без Ollama: отключаются только разбор фраз, теги и поиск по смыслу. Данные (data/, static/photos/) и локальный конфиг (.env) в репозиторий не попадают. Тесты работают только в песочнице: путь к данным берётся из окружения, tests/guard.py отказывается запускаться на живых данных. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
# 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 спросит про брандмауэр — разрешить для частной
|
||||
сети.** Иначе телефон до сервера не достучится, а выглядеть это будет как
|
||||
«страница не грузится».
|
||||
|
||||
## Настройки
|
||||
|
||||
Все через `.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` | — | другая папка для данных; используется тестами |
|
||||
|
||||
## Как пользоваться
|
||||
|
||||
### Вкладка «Действие» — четыре кнопки
|
||||
|
||||
| Кнопка | Что делает |
|
||||
|---|---|
|
||||
| **Положить** | Выбираешь место, выбираешь коробку, вписываешь предмет |
|
||||
| **Забрать** | Вещь уходит «на руках» — больше не числится в коробке |
|
||||
| **Переместить** | Вещь переезжает в другую коробку или место |
|
||||
| **Удалить** | Убирает запись совсем |
|
||||
|
||||
Ввод структурный: **место → ёмкость → предмет**. Место и коробку выбираешь из
|
||||
списков, поэтому ИИ в самом ответственном месте не участвует и ошибиться
|
||||
разбором фразы негде. Новое место добавляется *внутрь* выбранного — так и
|
||||
строится «Гараж → Стеллаж А → Полка 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, выбранную модель и доступность.
|
||||
Reference in New Issue
Block a user