This commit is contained in:
seb
2026-10-10 17:10:03 +02:00
parent 6a8716a6ef
commit 29057e8d7d
4 changed files with 89 additions and 2 deletions

8
package-lock.json generated
View File

@@ -30,6 +30,7 @@
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"selfsigned": "^5.5.0",
"sharp": "^0.35.5",
"skelehash-wasm": "^0.1.0",
"swagger-ui-dist": "^5.33.1",
"vm2": "^3.12.2"
},
@@ -66,7 +67,6 @@
}
},
"../llmorbiter": {
"name": "llmorbiter",
"version": "0.1.0",
"license": "0BSD",
"dependencies": {
@@ -5474,6 +5474,12 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/skelehash-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.0.tgz",
"integrity": "sha512-QdxF9YsJvKlT93O70dXFaWnsKVX7KDD0RgYf9cwgOT6kGnSMOdsO0rsdge+5Tg+vv6BjivbJ/2o5P1KcQsavEw==",
"license": "MIT"
},
"node_modules/slice-ansi": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.1.tgz",

View File

@@ -37,6 +37,7 @@
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"selfsigned": "^5.5.0",
"sharp": "^0.35.5",
"skelehash-wasm": "^0.1.0",
"swagger-ui-dist": "^5.33.1",
"vm2": "^3.12.2"
},

View File

@@ -571,6 +571,9 @@ export function register(app, basePath = '') {
onProgress: (phase, current, total, item) => {
sseWrite('progress', { phase, current, total, item });
},
onImage: (imageData) => {
sseWrite('image', imageData);
},
});
sseWrite('done', outcome);

View File

@@ -1,10 +1,40 @@
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
import { encodeRGBA } from 'skelehash-wasm';
import { getStore } from '../db/store.js';
import { createLogger } from '../util/logger.js';
const log = createLogger('import');
/**
* Computes SkeleHash base62 hash for an image buffer.
*
* @param {Buffer} buffer
* @returns {Promise<{base62: string, width: number, height: number, hashLength: number}|null>}
*/
async function computeSkeleHash(buffer) {
try {
const { data, info } = await sharp(buffer)
.rotate()
.resize({ width: 200, height: 200, fit: 'inside' })
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const result = await encodeRGBA(info.width, info.height, data);
return {
base62: result.base62,
width: info.width,
height: info.height,
hashLength: result.hash?.length ?? 0,
};
} catch (err) {
log.warn('Failed to compute skelehash for image:', err.message);
return null;
}
}
/**
* Parses RFC 4180 CSV text into an array of objects keyed by header.
*
@@ -166,6 +196,7 @@ export function extractVariantValue(parentName, childName, childSku, parentSku)
* @param {number} opts.companyId
* @param {string} opts.importDir
* @param {function} [opts.onProgress] - (phase, current, total, item) => void
* @param {function} [opts.onImage] - ({ type, current, total, sku, name, brand, categoryId, brandId, path, filename, skelehash, width, height, hashLength }) => void
* @returns {Promise<object>} Summary outcome
*/
export async function executeCompanyImport({
@@ -173,6 +204,7 @@ export async function executeCompanyImport({
companyId,
importDir,
onProgress = () => {},
onImage = () => {},
}) {
const store = await getStore();
@@ -306,6 +338,19 @@ export async function executeCompanyImport({
const imgBuf = fs.readFileSync(picPath);
await store.category.addImage(googleAuthId, dashCatId, imgBuf);
catPicCount++;
const skele = await computeSkeleHash(imgBuf);
onImage({
type: 'category',
categoryId: dashCatId,
name,
path: picRel,
filename: path.basename(picRel),
skelehash: skele?.base62 || null,
width: skele?.width || null,
height: skele?.height || null,
hashLength: skele?.hashLength || 0,
});
} catch (err) {
console.warn(`[Import] Category image error (${picRel}):`, err.message);
}
@@ -387,6 +432,19 @@ export async function executeCompanyImport({
const imgBuf = fs.readFileSync(picPath);
await store.brand.addImage(googleAuthId, brand.id, imgBuf);
brandPicCount++;
const skele = await computeSkeleHash(imgBuf);
onImage({
type: 'brand',
brandId: brand.id,
brand: brandName,
path: picRel,
filename: path.basename(picRel),
skelehash: skele?.base62 || null,
width: skele?.width || null,
height: skele?.height || null,
hashLength: skele?.hashLength || 0,
});
break; // 1 primary logo per brand
} catch (err) {
log.warn(`Brand image error for "${brandName}" (${picRel}):`, err.message);
@@ -742,18 +800,37 @@ export async function executeCompanyImport({
let totalImagesImported = 0;
for (let i = 0; i < imageTasks.length; i++) {
const { sku, relPath } = imageTasks[i];
const current = i + 1;
const total = imageTasks.length;
const filename = path.basename(relPath);
const imgPath = path.join(importDir, relPath);
let skele = null;
if (fs.existsSync(imgPath)) {
try {
const imgBuf = fs.readFileSync(imgPath);
await store.article.addImage(googleAuthId, sku, imgBuf);
totalImagesImported++;
skele = await computeSkeleHash(imgBuf);
} catch (err) {
// If already attached or format issue, log and continue
console.warn(`[Import] Image add failed for SKU ${sku} (${relPath}):`, err.message);
}
}
onProgress('images', i + 1, imageTasks.length, `${sku} (${path.basename(relPath)})`);
onImage({
type: 'article',
current,
total,
sku,
path: relPath,
filename,
skelehash: skele?.base62 || null,
width: skele?.width || null,
height: skele?.height || null,
hashLength: skele?.hashLength || 0,
});
}
console.log(`[Import] Completed article pictures: ${totalImagesImported} images processed`);