From 99e392d7775f2b95f5577278a8447865faafd091 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 31 Jul 2026 01:49:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A2=D0=B5=D0=B3=D0=B8=20=D1=83=D1=87=D0=B8?= =?UTF-8?q?=D1=82=D1=8B=D0=B2=D0=B0=D1=8E=D1=82=20=D0=BD=D0=B0=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20=D1=81=20=D1=84=D0=BE=D1=82=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=84=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Алекс пересоздавал теги для вещи «Светодиодная лампа LED (навигатор)» и получил «лампа для навигатора», «лампа для GPS-навигатора», «навигационная лампа». Модель прочитала «навигатор» как назначение, а не как марку Navigator, и по таким тегам вещь уже не найти. Правкой промпта это лечилось лишь наполовину: правила и запреты убирали часть мусора, но «лампа для навигатора» держалась. Написание марки латиницей помогало, но переименовывать все вещи — не решение. Настоящее решение подсказал сам Алекс: у вещи есть фотография, и на упаковке написана настоящая маркировка. Теперь пересоздание тегов сначала читает надписи с фото и отдаёт их модели как достоверные данные. Результат на той же вещи: было: лампа для навигатора, лампа для GPS-навигатора стало: лампа Navigator, встроенный светодиодный светильник, лампа DOWNLIGHT, лампа 10 Вт, лампа с холодным белым светом С коробки прочиталось: Navigator, DOWNLIGHT, 10Вт, IP44, 90°, холодный белый свет — заодно выяснилось, что это не лампочка, а встраиваемый светильник. В промпт добавлены два разобранных примера: марка в названии — это марка, а не назначение вещи; и не приписывать артикулы, которых никто не давал (на «шуруповёрт Makita» модель выдумывала «Makita XA020, 18 В»). Co-Authored-By: Claude Opus 5 --- README.md | 15 +++++++++++++++ hlamingo/app.py | 26 +++++++++++++++++++------- hlamingo/brain.py | 24 ++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b2487f4..0d84db6 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,21 @@ qwen3: изолента → «изоляционная лента», После смены у записанных вещей теги остаются прежними; обновить их можно кнопкой **«🔄 Пересоздать теги»** в карточке. +**Пересоздание учитывает фотографию.** Если у вещи есть снимок, приложение +сначала читает с него надписи и отдаёт их модели как достоверную маркировку. +Разница на живом примере — вещь «Светодиодная лампа LED (навигатор)»: + +``` +без фото: «лампа для навигатора», «лампа для GPS-навигатора» ← модель + решила, что это лампа для навигатора +с фото: «лампа Navigator», «встроенный светодиодный светильник», + «лампа DOWNLIGHT», «лампа 10 Вт» +``` + +С коробки прочиталось `Navigator, DOWNLIGHT, 10Вт, IP44, 90°, холодный белый +свет` — и выяснилось, что это вообще не лампочка, а встраиваемый светильник. +На упаковке написана настоящая маркировка, гадать по названию не нужно. + ## Установка Положите `install.cmd` в пустую папку и запустите — он скачает проект, создаст diff --git a/hlamingo/app.py b/hlamingo/app.py index c8203e7..77da679 100644 --- a/hlamingo/app.py +++ b/hlamingo/app.py @@ -495,12 +495,22 @@ def api_retag_item(item_id): return jsonify({"ok": False, "reply": "Модель недоступна — теги придумывать некому."}), 503 - # Вторая попытка не роскошь: если только что распознавали фото, в памяти - # Ollama лежит vision-модель, и первый запрос уходит на её выгрузку - # и загрузку текстовой — он может вернуться пустым. - fresh = brain.aliases(item.get("name"), item.get("note")) + # Если у вещи есть фотография — сначала читаем надписи с неё. На упаковке + # настоящая маркировка, и модели не приходится гадать по названию. + seen = None + if item.get("photo") and config.VISION_MODEL: + path = PHOTOS_DIR / item["photo"] + if path.exists(): + guess = brain.describe_photo(path.read_bytes()) + if guess and guess.get("text"): + seen = guess["text"] + + # Вторая попытка не роскошь: после обращения к vision-модели в памяти + # Ollama лежит она, и первый запрос уходит на её выгрузку и загрузку + # текстовой — он может вернуться пустым. + fresh = brain.aliases(item.get("name"), item.get("note"), seen=seen) if not fresh: - fresh = brain.aliases(item.get("name"), item.get("note")) + fresh = brain.aliases(item.get("name"), item.get("note"), seen=seen) if not fresh: return jsonify({"ok": False, "reply": "Модель не вернула ни одного тега."}) @@ -517,8 +527,10 @@ def api_retag_item(item_id): item["emb"] = None # словарь предмета изменился search.ensure_embeddings(db) store.save(db) - return jsonify({"ok": True, "item": _card(db, item), - "reply": "Теги пересозданы: %s" % ", ".join(item["aliases"])}) + reply = "Теги пересозданы: %s" % ", ".join(item["aliases"]) + if seen: + reply += "\n(прочитано с фото: %s)" % seen + return jsonify({"ok": True, "item": _card(db, item), "seen": seen, "reply": reply}) @app.route("/api/containers/", methods=["PATCH"]) diff --git a/hlamingo/brain.py b/hlamingo/brain.py index ee07edf..ff2ec78 100644 --- a/hlamingo/brain.py +++ b/hlamingo/brain.py @@ -79,6 +79,16 @@ ALIAS_SYSTEM = """Ты придумываешь альтернативные н «Стабилизатор 7805» → ["LM7805","стабилизатор 5 вольт","линейный регулятор напряжения","КР142ЕН5А","стабилизатор питания"] «изолента» → ["изоляционная лента","синяя лента","ПВХ-лента","электроизоляционная лента"] + +Марка в названии — это МАРКА, а не назначение вещи: +«Светодиодная лампа LED (навигатор)» → ["лампа Navigator","светодиодная лампа","LED лампа","лампочка светодиодная","лампа навигатор"] +Здесь «навигатор» — марка Navigator. Вещь остаётся ЛАМПОЙ, поэтому «лампа для +навигатора» и «навигационная лампа» — НЕВЕРНО: это описания другой вещи. +Каждый тег обязан обозначать ту же самую вещь, только другими словами. + +Не приписывай вещи артикулы и характеристики, которых нет в её названии: +«шуруповёрт Makita» → ["шуруповерт Makita","шуруповерт Макита","аккумуляторный шуруповерт","электрошуруповерт"] +а не «Makita XA020» и не «Makita 18 В» — этих данных тебе никто не давал. """ _THINK_RE = re.compile(r".*?", re.IGNORECASE | re.DOTALL) @@ -338,13 +348,23 @@ def describe_photo(image_bytes): # ============================================================ # Синонимы # ============================================================ -def aliases(name, note=None, model=None): - """3-7 альтернативных названий. Пустой список, если модель недоступна.""" +def aliases(name, note=None, model=None, seen=None): + """3-7 альтернативных названий. Пустой список, если модель недоступна. + + seen — надписи, прочитанные с фотографии вещи. Это лучший источник тегов: + на упаковке написана настоящая маркировка, и модели не приходится гадать. + Без неё «Светодиодная лампа LED (навигатор)» превращалась в «лампу для + GPS-навигатора», хотя на коробке ясно написано Navigator. + """ name = (name or "").strip() if not name: return [] model = model or config.CHAT_MODEL + prompt = name + (" (" + note + ")" if note else "") + if seen: + prompt += ("\nНа фотографии этой вещи видны надписи: " + seen + + "\nИспользуй их: это настоящая маркировка, а не догадка.") # 500, а не 256: модель может ответить многословно (объектами вместо строк), # и тогда на 256 токенах JSON обрывается на полуслове — разбирать нечего. content = ollama.chat(model, [