feat: make AI background removal optional with resize-only upload and batch detour selection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+67
-40
@@ -59,7 +59,6 @@ function blobToBase64(blob: Blob): Promise<string> {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string
|
||||
// Supprimer le préfixe "data:image/jpeg;base64,"
|
||||
const base64 = result.split(',')[1]
|
||||
resolve(base64)
|
||||
}
|
||||
@@ -83,24 +82,21 @@ export function useImages() {
|
||||
async (id: string, ref: string) => {
|
||||
update(id, { status: 'searching' })
|
||||
try {
|
||||
// 1. Cherche d'abord dans Google Sheets (col C = UGS → col A = Shopify ID)
|
||||
const sheetsRes = await fetch(`/api/products/search-sheets?ref=${encodeURIComponent(ref)}`)
|
||||
const sheetsData = await sheetsRes.json()
|
||||
if (sheetsData.found) {
|
||||
update(id, {
|
||||
status: 'ready',
|
||||
status: 'converted',
|
||||
shopifyProductId: sheetsData.shopifyId,
|
||||
shopifyProductTitle: sheetsData.title,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Fallback : recherche directe dans Shopify par handle/titre
|
||||
const res = await fetch(`/api/products/search?ref=${encodeURIComponent(ref)}`)
|
||||
const data = await res.json()
|
||||
if (data.found) {
|
||||
update(id, {
|
||||
status: 'ready',
|
||||
status: 'converted',
|
||||
shopifyProductId: data.shopifyId,
|
||||
shopifyProductTitle: data.title,
|
||||
})
|
||||
@@ -114,31 +110,40 @@ export function useImages() {
|
||||
[update],
|
||||
)
|
||||
|
||||
// Pipeline complet pour un item (avec ref)
|
||||
const runPipeline = useCallback(
|
||||
async (id: string, blob: Blob, ref: string, feather: number, rotation: number, alphaThreshold: number) => {
|
||||
// Pipeline léger : conversion HEIC + recherche produit (sans détourage IA)
|
||||
const runConvertAndSearch = useCallback(
|
||||
async (id: string, blob: Blob, ref: string) => {
|
||||
const { convertHeicToJpeg } = await import('@/lib/client/heicConverter')
|
||||
const { removeBackgroundRaw, compositeOnWhite } = await import('@/lib/client/bgRemover')
|
||||
const { rotateBlob } = await import('@/lib/client/imageRotator')
|
||||
|
||||
let jpegBlob = blob
|
||||
|
||||
if (blob.type === 'image/heic' || blob.type === 'image/heif' || blob.type === '') {
|
||||
try {
|
||||
update(id, { status: 'converting' })
|
||||
jpegBlob = await convertHeicToJpeg(blob)
|
||||
// Met à jour originalBlob avec le JPEG converti (Chrome ne peut pas afficher HEIC)
|
||||
update(id, { originalBlob: jpegBlob })
|
||||
} catch (err) {
|
||||
update(id, { status: 'error', error: `Conversion HEIC : ${(err as Error).message}` })
|
||||
return
|
||||
}
|
||||
} else {
|
||||
update(id, { status: 'searching' })
|
||||
}
|
||||
|
||||
await searchProduct(id, ref)
|
||||
},
|
||||
[update, searchProduct],
|
||||
)
|
||||
|
||||
// Pipeline IA complet : détourage + composite + rotation
|
||||
const runBgRemoval = useCallback(
|
||||
async (id: string, blob: Blob, feather: number, rotation: number, alphaThreshold: number) => {
|
||||
const { removeBackgroundRaw, compositeOnWhite } = await import('@/lib/client/bgRemover')
|
||||
const { rotateBlob } = await import('@/lib/client/imageRotator')
|
||||
|
||||
let rawPng: Blob
|
||||
try {
|
||||
update(id, { status: 'processing', progress: null })
|
||||
rawPng = await removeBackgroundRaw(jpegBlob, (p) => update(id, { progress: p }))
|
||||
rawPng = await removeBackgroundRaw(blob, (p) => update(id, { progress: p }))
|
||||
} catch (err) {
|
||||
update(id, { status: 'error', error: `Détourage IA : ${(err as Error).message}` })
|
||||
return
|
||||
@@ -153,10 +158,9 @@ export function useImages() {
|
||||
return
|
||||
}
|
||||
|
||||
update(id, { rawPngBlob: rawPng, processedBlob: finalBlob })
|
||||
await searchProduct(id, ref)
|
||||
update(id, { rawPngBlob: rawPng, processedBlob: finalBlob, status: 'ready' })
|
||||
},
|
||||
[update, searchProduct],
|
||||
[update],
|
||||
)
|
||||
|
||||
// Recomposite rapide (feather, alphaThreshold ou rotation changés) — ne relance pas l'IA
|
||||
@@ -201,15 +205,13 @@ export function useImages() {
|
||||
error: null,
|
||||
progress: null,
|
||||
}))
|
||||
// Recalcule la ref depuis le nom du fichier (plus fiable que le nom du dossier ZIP)
|
||||
// item.filename = "DOSSIER/32263-.heic" → on prend la partie après le dernier "/"
|
||||
const itemsWithRef = newItems.map(item => {
|
||||
const basename = item.filename.split('/').pop() ?? item.filename
|
||||
return { ...item, ref: refFromFilename(basename) }
|
||||
})
|
||||
dispatch({ type: 'ADD', items: itemsWithRef })
|
||||
for (const item of itemsWithRef) {
|
||||
queueRef.current.enqueue(() => runPipeline(item.id, item.originalBlob, item.ref, item.feather, item.rotation, item.alphaThreshold))
|
||||
queueRef.current.enqueue(() => runConvertAndSearch(item.id, item.originalBlob, item.ref))
|
||||
}
|
||||
} else {
|
||||
const ref = refFromFilename(file.name)
|
||||
@@ -231,11 +233,35 @@ export function useImages() {
|
||||
progress: null,
|
||||
}
|
||||
dispatch({ type: 'ADD', items: [newItem] })
|
||||
queueRef.current.enqueue(() => runPipeline(newItem.id, newItem.originalBlob, newItem.ref, newItem.feather, newItem.rotation, newItem.alphaThreshold))
|
||||
queueRef.current.enqueue(() => runConvertAndSearch(newItem.id, newItem.originalBlob, newItem.ref))
|
||||
}
|
||||
}
|
||||
},
|
||||
[runPipeline],
|
||||
[runConvertAndSearch],
|
||||
)
|
||||
|
||||
// Lancer le détourage IA sur une image (depuis statut converted ou error)
|
||||
const detour = useCallback(
|
||||
(id: string) => {
|
||||
const item = items.find((i) => i.id === id)
|
||||
if (!item) return
|
||||
queueRef.current.enqueue(() =>
|
||||
runBgRemoval(item.id, item.originalBlob, item.feather, item.rotation, item.alphaThreshold)
|
||||
)
|
||||
},
|
||||
[items, runBgRemoval],
|
||||
)
|
||||
|
||||
// Relancer le pipeline complet (conversion + détourage) sur une image déjà traitée
|
||||
const reprocess = useCallback(
|
||||
(id: string) => {
|
||||
const item = items.find((i) => i.id === id)
|
||||
if (!item) return
|
||||
queueRef.current.enqueue(() =>
|
||||
runBgRemoval(item.id, item.originalBlob, item.feather, item.rotation, item.alphaThreshold)
|
||||
)
|
||||
},
|
||||
[items, runBgRemoval],
|
||||
)
|
||||
|
||||
const setFeather = useCallback(
|
||||
@@ -270,12 +296,23 @@ export function useImages() {
|
||||
const uploadOne = useCallback(
|
||||
async (id: string) => {
|
||||
const item = items.find((i) => i.id === id)
|
||||
if (!item || item.status !== 'ready' || !item.processedBlob || !item.shopifyProductId) return
|
||||
if (!item) return
|
||||
const canUpload = item.status === 'ready' || item.status === 'converted'
|
||||
if (!canUpload || !item.shopifyProductId) return
|
||||
|
||||
update(id, { status: 'uploading' })
|
||||
|
||||
try {
|
||||
const base64 = await blobToBase64(item.processedBlob)
|
||||
// Si détouré : utilise processedBlob ; sinon : redimensionne à 1000×1000
|
||||
let uploadBlob: Blob
|
||||
if (item.processedBlob) {
|
||||
uploadBlob = item.processedBlob
|
||||
} else {
|
||||
const { resizeToSquare } = await import('@/lib/client/imageResizer')
|
||||
uploadBlob = await resizeToSquare(item.originalBlob)
|
||||
}
|
||||
|
||||
const base64 = await blobToBase64(uploadBlob)
|
||||
const basename = item.filename.split('/').pop()?.replace(/\.[^.]+$/, '') ?? item.ref
|
||||
const filename = `${basename}.jpg`
|
||||
|
||||
@@ -288,7 +325,7 @@ export function useImages() {
|
||||
|
||||
let res = await doUpload(item.shopifyProductId)
|
||||
|
||||
// 404 = ID périmé (produit supprimé/recréé) → relancer la recherche par ref
|
||||
// 404 = ID périmé → relancer la recherche par ref
|
||||
if (res.status === 404) {
|
||||
const searchRes = await fetch(`/api/products/search?ref=${encodeURIComponent(item.ref)}`)
|
||||
const searchData = await searchRes.json()
|
||||
@@ -309,10 +346,9 @@ export function useImages() {
|
||||
)
|
||||
|
||||
const uploadAll = useCallback(async () => {
|
||||
const ready = items.filter((i) => i.status === 'ready')
|
||||
const ready = items.filter((i) => i.status === 'ready' || i.status === 'converted')
|
||||
for (const item of ready) {
|
||||
await uploadOne(item.id)
|
||||
// Pause de 500ms entre chaque upload pour éviter le rate limit Shopify
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
}
|
||||
}, [items, uploadOne])
|
||||
@@ -323,21 +359,12 @@ export function useImages() {
|
||||
|
||||
const assignProduct = useCallback(
|
||||
(id: string, shopifyProductId: string, shopifyProductTitle: string) => {
|
||||
update(id, { shopifyProductId, shopifyProductTitle, status: 'ready' })
|
||||
},
|
||||
[update],
|
||||
)
|
||||
|
||||
const reprocess = useCallback(
|
||||
(id: string) => {
|
||||
const item = items.find((i) => i.id === id)
|
||||
if (!item) return
|
||||
queueRef.current.enqueue(() =>
|
||||
runPipeline(item.id, item.originalBlob, item.ref, item.feather, item.rotation, item.alphaThreshold)
|
||||
)
|
||||
const newStatus = item?.processedBlob ? 'ready' : 'converted'
|
||||
update(id, { shopifyProductId, shopifyProductTitle, status: newStatus })
|
||||
},
|
||||
[items, runPipeline],
|
||||
[items, update],
|
||||
)
|
||||
|
||||
return { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess }
|
||||
return { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess, detour }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user