Files
hlamingo/README.md
T
alexandClaude Opus 5 1ae98b0b6f Экспорт, импорт и копии вне компьютера
Все 40 резервных копий лежали на том же диске, что и оригинал, а фотографии
не дублировались нигде: смерть диска уносила и 59 записей, и 69 снимков.

Что появилось:
- «📦 Скачать архив» — zip с базой и всеми фотографиями (13 МБ на текущих
  данных). Работает и с телефона, годится для переноса на другой компьютер.
- «📥 Загрузить архив» — восстановление. Сначала показывает, что внутри
  (сколько вещей в архиве против текущих), и только после подтверждения
  заменяет данные. Прежнее состояние откладывается в data/backups —
  и база, и папка со старыми фотографиями.
- HLAMINGO_BACKUP_DIR — путь, куда сама уходит копия базы и новых фотографий.
  Не чаще раза в час и только если путь доступен: сетевой диск отваливается
  регулярно, а терять запись о вещи из-за этого недопустимо. Состояние
  и кнопка «Скопировать сейчас» — на вкладке настроек.

Имена файлов из архива как пути не используются: только базовое имя
и расширение из белого списка. Иначе архив со строкой «../../» писал бы
куда угодно — тест это проверяет отдельно.

Попутно исправлены два дефекта:
- предел загрузки был 8 МБ, и архив в 13 МБ не прошёл бы. Разделён на общий
  предел запроса и отдельный предел для одного снимка;
- настройки нельзя было менять по одной: непереданные поля приходили как null
  и валились на проверке. Теперь null означает «не трогать», а пустая строка —
  осознанное «очистить».

Про защиту копий на сервере. Задумывался .htaccess, но сайты обслуживает
nginx, который его игнорирует, — файл создавал бы ложное чувство защиты.
Настоящая защита в расположении: папка лежит рядом с корнем сайта, а не внутри,
и nginx за пределы своего root не выпускает. Проверено запросами извне:
и сама папка, и db.json, и фотографии дают 404, включая попытку выйти
через «/../». .htaccess положен рядом как подстраховка на случай переезда
на Apache, о чём честно написано внутри самого файла.

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

31 KiB
Raw Blame History

hlamingo — помощник «где что лежит»

Раскладывать хлам по коробкам бесполезно, если потом не можешь найти нужное: проще заказать деталь заново, чем искать. hlamingo помнит, что и куда положено, и находит по названию, синонимам и маркировке — с телефона, стоя у полки.

Что нужно

  • Windows и Python 3.9+ (python.org, при установке отметить «Add python.exe to PATH»).
  • Ollama — на этом же компьютере или на любой машине в локальной сети. Без него приложение тоже работает: остаются кнопки, места, коробки, фото и поиск по названию и тегам; отключаются только разбор свободных фраз, автоматические теги и поиск по смыслу.

Модели (на машине с 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 иногда присылает объект вместо списка тегов — разбор это переживает, но модель, совсем не держащая формат, будет постоянно сваливаться на регулярки.
  • Эмбеддер обязан быть многоязычным — см. раздел про поиск.

Модель выбирается в настройках (вкладка ) из реально установленных. После смены у записанных вещей теги остаются прежними; обновить их можно кнопкой «🔄 Пересоздать теги» в карточке.

Пересоздание учитывает фотографию. Если у вещи есть снимок, приложение сначала читает с него надписи и отдаёт их модели как достоверную маркировку. Разница на живом примере — вещь «Светодиодная лампа LED (навигатор)»:

без фото:  «лампа для навигатора», «лампа для GPS-навигатора»   ← модель
                                     решила, что это лампа для навигатора
с фото:    «лампа Navigator», «встроенный светодиодный светильник»,
           «лампа DOWNLIGHT», «лампа 10 Вт»

С коробки прочиталось Navigator, DOWNLIGHT, 10Вт, IP44, 90°, холодный белый свет — и выяснилось, что это вообще не лампочка, а встраиваемый светильник. На упаковке написана настоящая маркировка, гадать по названию не нужно.

Установка

Положите 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_BACKUP_DIR папка для копий вне компьютера; пусто — выключено
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/ он ушёл бы как обычный файл, и браузер мог не признать его манифестом.

Резервные копии и перенос

Копии внутри программы лежат на том же диске, что и оригинал. Диск умрёт — пропадут и записи, и фотографии. Поэтому есть два способа держать данные ещё где-то, оба на вкладке .

Архив

«📦 Скачать архив» — zip с базой и всеми фотографиями (у 59 вещей это около 13 МБ). Работает и с телефона: файл уходит в загрузки. Годится и для переноса на другой компьютер.

«📥 Загрузить архив» — восстановление. Сначала показывает, что внутри (сколько вещей в архиве против текущих), и только после подтверждения заменяет данные. Прежнее состояние при этом откладывается в data\backups: db-before-import-*.json и папка с прежними фотографиями — вернуться можно всегда.

Имена файлов из архива как пути не используются: берётся только базовое имя и расширение из белого списка. Иначе архив со строкой ../../ внутри писал бы куда угодно на диске.

Внешняя папка

HLAMINGO_BACKUP_DIR — путь, куда само уходит копия базы и новых фотографий: сетевой диск, шара, что угодно. Пусто — выключено.

Копирование идёт не чаще раза в час и не мешает работе, если путь недоступен: сетевой диск отваливается регулярно, а терять запись о вещи из-за этого недопустимо. Состояние (путь, доступен ли, когда была последняя копия) видно на вкладке ⚙, там же кнопка «Скопировать сейчас».

Если папка лежит на веб-сервере — проверьте, не отдаётся ли она наружу. В нашем случае копии кладутся в sambadata\hlamingo-backup, рядом с корнем сайта, но не внутри него, поэтому nginx до них не дотягивается — проверено запросами извне, все дают 404. Файл .htaccess там тоже лежит, но nginx его игнорирует: это подстраховка на случай переезда на Apache, а не текущая защита. Полагаться на .htaccess при nginx нельзя.

Тесты

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, выбранную модель и доступность.