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,107 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user