Подсказка названия по фотографии

Фото выбрано в форме — приложение показывает его модели и предлагает название
с прочитанными надписями. «Подставить» переносит в поля, «Не надо» прячет.
Само не подставляется, и это принципиально: модели домысливают марки
и артикулы, когда надписей не видно. В замере клипсу для кабеля модель назвала
«конструктором LEGO Technic, 8376, 401 шт.». Запрет на выдумывание в промпте
помог: та же вещь стала «красной щёткой для посуды».

Смотрит gemma3:4b. Выбрана замером на настоящих снимках из базы:
  gemma3:4b     3.5 с, отвечает всегда, надписи читает верно
                («Лампа светодиодная C35, Нейтральный белый, 9 Вт LED, E14»);
  qwen3-vl:8b   11-32 с и часто пустой ответ — это ризонер, ему нужен бюджет
                в 2000+ токенов, и уменьшение картинки не спасает.
Проверялась и гипотеза, что дело в размере снимка: у gemma3 разницы между
1280 и 512 px почти нет, у qwen3-vl время падает с 62 до 41 с, но ответ
всё равно пуст. Картинка всё же уменьшается до 768 px — незачем гонять лишнее.

Генерация тегов чинилась по ходу: на «Стабилизатор 7805 (TO-220)» модель
отвечала массивом объектов с пояснениями, не укладывалась в 256 токенов,
и JSON обрывался на полуслове — теги выходили пустыми. Бюджет поднят до 500,
промпт требует плоский массив строк, а из битого ответа строки теперь
вытаскиваются регуляркой: лучше несколько живых тегов, чем ничего.

Pillow добавлен в зависимости — для уменьшения картинки перед распознаванием.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 01:24:49 +03:00
co-authored by Claude Opus 5
parent 01d7f17f89
commit bae5116d77
9 changed files with 329 additions and 8 deletions
+44
View File
@@ -209,6 +209,7 @@
<button class="act ghost small" id="formPhotoClear" style="flex:0 0 26%" hidden>Убрать</button>
</div>
<div id="formPhotoPreview" class="photo" hidden></div>
<div id="guessBox" class="card" hidden></div>
</div>
<div class="mt"><button class="act" id="goBtn">Положить</button></div>
@@ -510,11 +511,53 @@ async function takeFormPhoto(input) {
img.src = URL.createObjectURL(pendingPhoto);
preview.appendChild(img);
$('#formPhotoClear').hidden = false;
recognizePending(); // фото есть — спросим модель, что на нём
} catch (e) {
say('Не смог прочитать это изображение: ' + e);
clearPendingPhoto();
}
}
// Подсказка по фото: показываем, что увидела модель, и даём подставить это
// в поле названия. Само не подставляется — модель домысливает бренды,
// когда надписей не видно.
async function recognizePending() {
if (!pendingPhoto) return;
const box = $('#guessBox');
box.hidden = false;
box.innerHTML = '<div class="meta">Смотрю на фото…</div>';
try {
const fd = new FormData();
fd.append('photo', pendingPhoto, 'photo.jpg');
const d = await api('/api/recognize', {method: 'POST', body: fd});
if (!d.ok) { box.hidden = true; return; }
const g = d.guess;
box.innerHTML = '<div class="meta">На фото похоже на:</div>'
+ '<h3>' + esc(g.name) + '</h3>'
+ (g.text ? '<div class="meta">Надписи: ' + esc(g.text) + '</div>' : '')
+ (g.sure ? '' : '<div class="meta">Модель не уверена — проверьте.</div>');
const bar = document.createElement('div');
bar.className = 'acts';
const use = document.createElement('button');
use.textContent = '✓ Подставить';
use.onclick = () => {
$('#itemName').value = g.name;
if (g.text && !$('#itemNote').value) $('#itemNote').value = g.text;
box.hidden = true;
$('#itemName').focus();
};
const skip = document.createElement('button');
skip.textContent = 'Не надо';
skip.onclick = () => { box.hidden = true; };
bar.appendChild(use);
bar.appendChild(skip);
box.appendChild(bar);
} catch (e) {
box.hidden = true;
}
}
$('#formCamInput').onchange = () => takeFormPhoto($('#formCamInput'));
$('#formGalInput').onchange = () => takeFormPhoto($('#formGalInput'));
@@ -524,6 +567,7 @@ function clearPendingPhoto() {
$('#formPhotoPreview').hidden = true;
$('#formPhotoPreview').innerHTML = '';
$('#formPhotoClear').hidden = true;
$('#guessBox').hidden = true;
}
// Отправляет уже сжатый blob к готовой записи. Возвращает имя файла или null.