Теги учитывают надписи с фотографии

Алекс пересоздавал теги для вещи «Светодиодная лампа LED (навигатор)»
и получил «лампа для навигатора», «лампа для GPS-навигатора», «навигационная
лампа». Модель прочитала «навигатор» как назначение, а не как марку Navigator,
и по таким тегам вещь уже не найти.

Правкой промпта это лечилось лишь наполовину: правила и запреты убирали часть
мусора, но «лампа для навигатора» держалась. Написание марки латиницей
помогало, но переименовывать все вещи — не решение.

Настоящее решение подсказал сам Алекс: у вещи есть фотография, и на упаковке
написана настоящая маркировка. Теперь пересоздание тегов сначала читает
надписи с фото и отдаёт их модели как достоверные данные.

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

В промпт добавлены два разобранных примера: марка в названии — это марка,
а не назначение вещи; и не приписывать артикулы, которых никто не давал
(на «шуруповёрт Makita» модель выдумывала «Makita XA020, 18 В»).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 01:49:03 +03:00
co-authored by Claude Opus 5
parent bae5116d77
commit 99e392d777
3 changed files with 56 additions and 9 deletions
+15
View File
@@ -60,6 +60,21 @@ qwen3: изолента → «изоляционная лента»,
После смены у записанных вещей теги остаются прежними; обновить их можно
кнопкой **«🔄 Пересоздать теги»** в карточке.
**Пересоздание учитывает фотографию.** Если у вещи есть снимок, приложение
сначала читает с него надписи и отдаёт их модели как достоверную маркировку.
Разница на живом примере — вещь «Светодиодная лампа LED (навигатор)»:
```
без фото: «лампа для навигатора», «лампа для GPS-навигатора» ← модель
решила, что это лампа для навигатора
с фото: «лампа Navigator», «встроенный светодиодный светильник»,
«лампа DOWNLIGHT», «лампа 10 Вт»
```
С коробки прочиталось `Navigator, DOWNLIGHT, 10Вт, IP44, 90°, холодный белый
свет` — и выяснилось, что это вообще не лампочка, а встраиваемый светильник.
На упаковке написана настоящая маркировка, гадать по названию не нужно.
## Установка
Положите `install.cmd` в пустую папку и запустите — он скачает проект, создаст
+19 -7
View File
@@ -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/<code>", methods=["PATCH"])
+22 -2
View File
@@ -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"<think(?:ing)?>.*?</think(?:ing)?>", 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, [