Warn on small picture candidates

This commit is contained in:
2026-08-06 00:24:03 +02:00
parent 0987edd112
commit b5a69050e0
6 changed files with 33 additions and 8 deletions
+1
View File
@@ -186,6 +186,7 @@ There are three selectable data sources:
- Source discovery: use manufacturer mapping from Local scrape DB and show exactly which source produced each candidate. - Source discovery: use manufacturer mapping from Local scrape DB and show exactly which source produced each candidate.
- Browser scraping: use the configured browser engine per source and keep the UI responsive while scraping runs. - Browser scraping: use the configured browser engine per source and keep the UI responsive while scraping runs.
- Image extraction: collect original image URLs, dimensions, mime type, file size and source page URL. - Image extraction: collect original image URLs, dimensions, mime type, file size and source page URL.
- Image quality: show a clear warning for any picture candidate smaller than `800 x 800`; small thumbnails must remain visible for review but must be marked as not production-ready.
- Duplicate detection: calculate `sha256` for exact duplicates and perceptual hash for visually similar images. - Duplicate detection: calculate `sha256` for exact duplicates and perceptual hash for visually similar images.
- Target assignment: propose whether pictures belong to `color`, `size`, `combination` or `manual` target, then learn from user corrections. - Target assignment: propose whether pictures belong to `color`, `size`, `combination` or `manual` target, then learn from user corrections.
- Persistence: save picture blobs, candidates, hashes, source metadata and learned rules only into Local scrape DB. - Persistence: save picture blobs, candidates, hashes, source metadata and learned rules only into Local scrape DB.
+12 -3
View File
@@ -61,6 +61,7 @@ const darkContrastSelect = document.querySelector("#darkContrastSelect");
const catalogScreen = document.querySelector(".catalog-screen"); const catalogScreen = document.querySelector(".catalog-screen");
const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle"); const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle");
const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle"); const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle");
const MIN_PRODUCT_PICTURE_SIZE = 800;
let sidebarUserClosed = false; let sidebarUserClosed = false;
let mappingLoadToken = 0; let mappingLoadToken = 0;
const mappingCacheKey = "catalog-maker:mapping-cache"; const mappingCacheKey = "catalog-maker:mapping-cache";
@@ -906,12 +907,20 @@ function renderPictureCandidates(items) {
dimensions.className = "picture-candidate-dimensions"; dimensions.className = "picture-candidate-dimensions";
dimensions.textContent = "Size: loading..."; dimensions.textContent = "Size: loading...";
image.addEventListener("load", () => { image.addEventListener("load", () => {
dimensions.textContent = image.naturalWidth && image.naturalHeight if (!image.naturalWidth || !image.naturalHeight) {
? `Size: ${image.naturalWidth} x ${image.naturalHeight}px` dimensions.textContent = "Size: unknown";
: "Size: unknown"; row.classList.remove("is-too-small");
return;
}
const isTooSmall = image.naturalWidth < MIN_PRODUCT_PICTURE_SIZE || image.naturalHeight < MIN_PRODUCT_PICTURE_SIZE;
row.classList.toggle("is-too-small", isTooSmall);
dimensions.textContent = isTooSmall
? `Size: ${image.naturalWidth} x ${image.naturalHeight}px - too small, min ${MIN_PRODUCT_PICTURE_SIZE} x ${MIN_PRODUCT_PICTURE_SIZE}`
: `Size: ${image.naturalWidth} x ${image.naturalHeight}px`;
}); });
image.addEventListener("error", () => { image.addEventListener("error", () => {
dimensions.textContent = "Size: image preview failed"; dimensions.textContent = "Size: image preview failed";
row.classList.add("is-too-small");
}); });
const assignment = document.createElement("span"); const assignment = document.createElement("span");
assignment.className = "picture-candidate-target"; assignment.className = "picture-candidate-target";
+13 -3
View File
@@ -61,6 +61,7 @@ const darkContrastSelect = document.querySelector("#darkContrastSelect");
const catalogScreen = document.querySelector(".catalog-screen"); const catalogScreen = document.querySelector(".catalog-screen");
const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle"); const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle");
const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle"); const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle");
const MIN_PRODUCT_PICTURE_SIZE = 800;
let sidebarUserClosed = false; let sidebarUserClosed = false;
let mappingLoadToken = 0; let mappingLoadToken = 0;
const mappingCacheKey = "catalog-maker:mapping-cache"; const mappingCacheKey = "catalog-maker:mapping-cache";
@@ -985,12 +986,21 @@ function renderPictureCandidates(items) {
dimensions.className = "picture-candidate-dimensions"; dimensions.className = "picture-candidate-dimensions";
dimensions.textContent = "Size: loading..."; dimensions.textContent = "Size: loading...";
image.addEventListener("load", () => { image.addEventListener("load", () => {
dimensions.textContent = image.naturalWidth && image.naturalHeight if (!image.naturalWidth || !image.naturalHeight) {
? `Size: ${image.naturalWidth} x ${image.naturalHeight}px` dimensions.textContent = "Size: unknown";
: "Size: unknown"; row.classList.remove("is-too-small");
return;
}
const isTooSmall = image.naturalWidth < MIN_PRODUCT_PICTURE_SIZE || image.naturalHeight < MIN_PRODUCT_PICTURE_SIZE;
row.classList.toggle("is-too-small", isTooSmall);
dimensions.textContent = isTooSmall
? `Size: ${image.naturalWidth} x ${image.naturalHeight}px - too small, min ${MIN_PRODUCT_PICTURE_SIZE} x ${MIN_PRODUCT_PICTURE_SIZE}`
: `Size: ${image.naturalWidth} x ${image.naturalHeight}px`;
}); });
image.addEventListener("error", () => { image.addEventListener("error", () => {
dimensions.textContent = "Size: image preview failed"; dimensions.textContent = "Size: image preview failed";
row.classList.add("is-too-small");
}); });
const assignment = document.createElement("span"); const assignment = document.createElement("span");
+1 -1
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -228,6 +228,7 @@
.source-status.is-ok { @apply bg-emerald-100 text-emerald-700; } .source-status.is-ok { @apply bg-emerald-100 text-emerald-700; }
.source-status.is-muted { @apply bg-slate-200 text-slate-600; } .source-status.is-muted { @apply bg-slate-200 text-slate-600; }
.picture-candidate { @apply grid grid-cols-[72px_minmax(0,1fr)_auto] items-center gap-2 border-b border-slate-200 p-2 text-[10px]; } .picture-candidate { @apply grid grid-cols-[72px_minmax(0,1fr)_auto] items-center gap-2 border-b border-slate-200 p-2 text-[10px]; }
.picture-candidate.is-too-small { @apply bg-amber-50; }
.picture-candidate-preview { @apply flex h-16 w-[72px] items-center justify-center bg-white; } .picture-candidate-preview { @apply flex h-16 w-[72px] items-center justify-center bg-white; }
.picture-candidate img { @apply max-h-16 w-[72px] object-contain; } .picture-candidate img { @apply max-h-16 w-[72px] object-contain; }
.picture-candidate-meta { @apply grid min-w-0 gap-0.5; } .picture-candidate-meta { @apply grid min-w-0 gap-0.5; }
@@ -235,6 +236,7 @@
.picture-candidate-dimensions, .picture-candidate-dimensions,
.picture-candidate-target, .picture-candidate-target,
.picture-candidate-duplicate { @apply min-w-0 truncate text-slate-500; } .picture-candidate-duplicate { @apply min-w-0 truncate text-slate-500; }
.picture-candidate.is-too-small .picture-candidate-dimensions { @apply font-semibold text-amber-700; }
.picture-candidate-save { @apply min-w-[76px]; } .picture-candidate-save { @apply min-w-[76px]; }
@media (max-width: 980px) { @media (max-width: 980px) {
.catalog-screen { @apply flex flex-col pl-0; } .catalog-screen { @apply flex flex-col pl-0; }
@@ -364,10 +366,12 @@
html.dark .source-row { @apply border-slate-700 bg-slate-900 text-slate-100 hover:bg-slate-800; } html.dark .source-row { @apply border-slate-700 bg-slate-900 text-slate-100 hover:bg-slate-800; }
html.dark .suggested-row, html.dark .suggested-row,
html.dark .picture-candidate { @apply border-slate-700; } html.dark .picture-candidate { @apply border-slate-700; }
html.dark .picture-candidate.is-too-small { @apply bg-amber-950/40; }
html.dark .picture-candidate-preview { @apply bg-slate-800; } html.dark .picture-candidate-preview { @apply bg-slate-800; }
html.dark .picture-candidate-title { @apply text-slate-100; } html.dark .picture-candidate-title { @apply text-slate-100; }
html.dark .picture-candidate-dimensions, html.dark .picture-candidate-dimensions,
html.dark .picture-candidate-target, html.dark .picture-candidate-target,
html.dark .picture-candidate-duplicate { @apply text-slate-400; } html.dark .picture-candidate-duplicate { @apply text-slate-400; }
html.dark .picture-candidate.is-too-small .picture-candidate-dimensions { @apply text-amber-300; }
html.dark .suggested-row:nth-child(even) { @apply bg-slate-800; } html.dark .suggested-row:nth-child(even) { @apply bg-slate-800; }
} }
+2 -1
View File
@@ -229,7 +229,8 @@ test("picture candidates show source, dimensions, target assignment and save act
await expect(page.locator("#sourceModal")).toBeVisible(); await expect(page.locator("#sourceModal")).toBeVisible();
await expect(page.locator(".picture-candidate-title").first()).toHaveText("Hudy picture #1"); await expect(page.locator(".picture-candidate-title").first()).toHaveText("Hudy picture #1");
await expect(page.locator(".picture-candidate-dimensions").first()).toContainText("Size:"); await expect(page.locator(".picture-candidate-dimensions").first()).toContainText("too small, min 800 x 800");
await expect(page.locator(".picture-candidate").first()).toHaveClass(/is-too-small/);
await expect(page.locator(".picture-candidate-target").first()).toContainText("Target: color Carbon/Malibu Blue"); await expect(page.locator(".picture-candidate-target").first()).toContainText("Target: color Carbon/Malibu Blue");
await expect(page.locator(".picture-candidate-save")).toHaveCount(2); await expect(page.locator(".picture-candidate-save")).toHaveCount(2);
await expect(page.locator("#sourceBatchActionButton")).toBeVisible(); await expect(page.locator("#sourceBatchActionButton")).toBeVisible();