Files
alexandClaude Opus 5 17b7d49d2f hlamingo — помощник «где что лежит»
Веб-помощник для домашнего склада: помнит, в какой коробке и в каком месте
лежит вещь, и находит её по названию, тегам и маркировке. Открывается
с телефона, интеллект — локальная Ollama.

Основное:
- структурный ввод «место → ёмкость → предмет» и кнопки
  Положить / Забрать / Переместить / Удалить;
- иерархия мест (Гараж → Стеллаж А → Полка 2), ёмкости с кодом «буква+цифра»,
  состояние «на руках» для взятых вещей;
- поиск по словам с учётом русских окончаний, теги от модели расширяют
  словарь предмета, смысловой поиск — вспомогательный (замеры в search.py);
- фото к вещам и коробкам: съёмка и выбор из галереи;
- правка и удаление всех сущностей, удаление коробки не теряет её содержимое;
- работает и без Ollama: отключаются только разбор фраз, теги и поиск
  по смыслу.

Данные (data/, static/photos/) и локальный конфиг (.env) в репозиторий
не попадают. Тесты работают только в песочнице: путь к данным берётся
из окружения, tests/guard.py отказывается запускаться на живых данных.

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

108 lines
4.6 KiB
Python
Raw Permalink 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.
# -*- coding: utf-8 -*-
"""Рисует иконки приложения и пишет их в static/.
Свой минимальный PNG-энкодер вместо Pillow: иконки генерируются один раз,
и тащить зависимость в проект ради этого не хочется.
Запуск: venv\\Scripts\\python.exe tools\\make_icons.py
"""
import os
import zlib
import struct
from pathlib import Path
OUT = Path(__file__).resolve().parent.parent / "static"
BG = (0xFF, 0x7A, 0x9C) # розовый акцент интерфейса
BOX = (0xFF, 0xFF, 0xFF) # передняя стенка
RIM = (0xEC, 0xE2, 0xE6) # борта, видимые сверху — чуть темнее для объёма
INSIDE = (0xC9, 0x3B, 0x63) # внутренность: коробка открыта, туда и кладут
# Иконку на Android обрезают по маске (круг, скруглённый квадрат), поэтому всё
# значимое держим внутри центральных ~80%: 10% с каждой стороны могут срезать.
SAFE = 0.80
def png(path, size, pixels):
"""Пишет RGB-PNG размером size×size из плоского списка (r,g,b)."""
raw = bytearray()
for y in range(size):
raw.append(0) # фильтр строки: none
for x in range(size):
raw += bytes(pixels[y * size + x])
def chunk(tag, data):
body = tag + data
return (struct.pack(">I", len(data)) + body
+ struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF))
ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # 8 бит, RGB
with open(str(path), "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n")
f.write(chunk(b"IHDR", ihdr))
f.write(chunk(b"IDAT", zlib.compress(bytes(raw), 9)))
f.write(chunk(b"IEND", b""))
def draw(size):
"""Коробка с откинутыми клапанами — узнаётся даже в 32 пикселя."""
px = [BG] * (size * size)
def rect(x0, y0, x1, y1, color):
# Координаты в долях от размера: одна отрисовка работает для всех иконок.
ax, ay = int(x0 * size), int(y0 * size)
bx, by = int(x1 * size), int(y1 * size)
for y in range(max(0, ay), min(size, by)):
row = y * size
for x in range(max(0, ax), min(size, bx)):
px[row + x] = color
def trapezoid(y0, y1, top_l, top_r, bot_l, bot_r, color):
"""Заливка трапеции: боковые края интерполируются по высоте.
Именно она даёт перспективу — без неё коробка выглядит плоским окном.
"""
ay, by = int(y0 * size), int(y1 * size)
if by <= ay:
return
for y in range(max(0, ay), min(size, by)):
t = (y - ay) / float(by - ay)
xl = int((top_l + (bot_l - top_l) * t) * size)
xr = int((top_r + (bot_r - top_r) * t) * size)
row = y * size
for x in range(max(0, xl), min(size, xr)):
px[row + x] = color
m = (1.0 - SAFE) / 2.0 # отступ безопасной зоны
s = SAFE
def f(v):
return m + v * s
rim_top, rim_bot = f(0.13), f(0.44) # верхние борта, видимые сверху
body_bot = f(0.90)
# Борта: сверху уже, снизу шире — взгляд сверху под углом.
trapezoid(rim_top, rim_bot, f(0.24), f(0.76), f(0.09), f(0.91), RIM)
# Внутренность — вырез в бортах. Виден проём: коробка открыта.
trapezoid(rim_top + 0.045 * s, rim_bot - 0.035 * s,
f(0.31), f(0.69), f(0.19), f(0.81), INSIDE)
# Передняя стенка.
rect(f(0.09), rim_bot, f(0.91), body_bot, BOX)
# Тонкая тень под кромкой — отделяет стенку от бортов.
rect(f(0.09), rim_bot, f(0.91), rim_bot + 0.02 * s, RIM)
return px
def main():
OUT.mkdir(parents=True, exist_ok=True)
for size, name in ((512, "icon-512.png"), (192, "icon-192.png"),
(180, "apple-touch-icon.png"), (32, "favicon-32.png")):
png(OUT / name, size, draw(size))
print(" %s %d×%d %d байт" % (name, size, size, (OUT / name).stat().st_size))
if __name__ == "__main__":
main()