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, [