diff --git a/src/components/ContributionAssistantCropImageList.vue b/src/components/ContributionAssistantCropImageList.vue deleted file mode 100644 index 3e43368404c..00000000000 --- a/src/components/ContributionAssistantCropImageList.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/src/components/ContributionAssistantDrawCanvas.vue b/src/components/ContributionAssistantDrawCanvas.vue index 7e986e76b46..7459fe8de70 100644 --- a/src/components/ContributionAssistantDrawCanvas.vue +++ b/src/components/ContributionAssistantDrawCanvas.vue @@ -19,42 +19,42 @@ type: Image, default: null }, - seedCrops: { + boundingBoxesFromServer: { type: Array, default: null } }, - emits: ['croppedImages', 'loaded'], + emits: ['extractedLabels', 'loaded'], data() { return { isDrawing: false, startX: 0, startY: 0, scale: 1, - rectangles: [] + boundingBoxes: [] } }, watch: { - seedCrops() { - if (this.seedCrops) { - this.init(true) + boundingBoxesFromServer() { + if (this.boundingBoxesFromServer) { + this.initCanvas(true) } } }, mounted() { if (this.image.complete) { - this.init() + this.initCanvas() } else { - this.image.onload = this.init + this.image.onload = this.initCanvas } }, methods: { - init(keepRectangles=false) { + initCanvas(keepBoundingBoxes=false) { const canvas = this.$refs.canvas const ctx = canvas.getContext("2d") canvas.style.width = "100%" this.scale = canvas.offsetWidth / this.image.width - const preferedHeight = 400 + const preferedHeight = window.innerHeight - 250 if (preferedHeight < this.image.height * this.scale) { // Image will be too tall @@ -70,21 +70,22 @@ ctx.drawImage(this.image, 0, 0, newWidth, newHeight) - if (!keepRectangles) { - this.rectangles = [] // reset rectangles + if (!keepBoundingBoxes) { + this.boundingBoxes = [] // reset boundingBoxes } - if (this.seedCrops) { - this.rectangles = this.rectangles.concat(this.seedCrops.map(seedCrop => { + if (this.boundingBoxesFromServer) { + this.boundingBoxes = this.boundingBoxes.concat(this.boundingBoxesFromServer.map(boundingBox => { return { - startY: seedCrop[0] * this.image.height, - startX: seedCrop[1] * this.image.width, - endY: seedCrop[2] * this.image.height, - endX: seedCrop[3] * this.image.width + startY: boundingBox[0] * this.image.height, + startX: boundingBox[1] * this.image.width, + endY: boundingBox[2] * this.image.height, + endX: boundingBox[3] * this.image.width, + boundingSource: this.$t('ContributionAssistant.AutomaticBoundingBoxSource') } })) - this.cropImages() + this.extractLabels() } - this.drawRectangles(); // Draw previous rectangles after resizing + this.drawBoundingBoxes(); // Draw previous boundingBoxes after resizing this.$emit('loaded') }, startDrawing(event) { @@ -109,7 +110,7 @@ const ctx = canvas.getContext("2d") ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height); // Redraw image - this.drawRectangles(); // Redraw previous rectangles + this.drawBoundingBoxes(); // Redraw previous boundingBoxes const currentX = event.offsetX / this.scale const currentY = event.offsetY / this.scale @@ -129,27 +130,26 @@ } const endX = event.offsetX / this.scale const endY = event.offsetY / this.scale - this.rectangles.push({ startX: this.startX, startY: this.startY, endX, endY }) - this.cropImages() + this.boundingBoxes.push({ startX: this.startX, startY: this.startY, endX, endY, boundingSource: this.$t('ContributionAssistant.ManualBoundingBoxSource') }) + this.extractLabels() }, - drawRectangles() { + drawBoundingBoxes() { const ctx = this.$refs.canvas.getContext("2d") ctx.strokeStyle = "red" - this.rectangles.forEach(rect => { + this.boundingBoxes.forEach(rect => { const { startX, startY, endX, endY } = rect const width = endX - startX const height = endY - startY ctx.strokeRect(startX * this.scale, startY * this.scale, width * this.scale, height * this.scale) }); }, - async cropImages() { - let croppedImages = [] - let croppedBlobs = [] + async extractLabels() { + let extractedLabels = [] const originalCanvas = document.createElement("canvas") const ctx = originalCanvas.getContext("2d") - for (let i = 0; i < this.rectangles.length; i++) { - const rect = this.rectangles[i] - const { startX, startY, endX, endY } = rect + for (let i = 0; i < this.boundingBoxes.length; i++) { + const rect = this.boundingBoxes[i] + const { startX, startY, endX, endY, boundingSource } = rect const width = Math.abs(endX - startX) const height = Math.abs(endY - startY) @@ -157,19 +157,22 @@ originalCanvas.height = height ctx.drawImage(this.image, Math.min(startX, endX), Math.min(startY, endY), width, height, 0, 0, width, height) - croppedImages[i] = originalCanvas.toDataURL() - croppedBlobs[i] = await new Promise(resolve => originalCanvas.toBlob(resolve, 'image/webp')) + extractedLabels[i] = { + imageSrc: originalCanvas.toDataURL(), + blob: await new Promise(resolve => originalCanvas.toBlob(resolve, 'image/webp')), + boundingSource: boundingSource + } } - this.$emit('croppedImages', [croppedImages, croppedBlobs]) + this.$emit('extractedLabels', extractedLabels) }, - removeRectangle(index) { - this.rectangles.splice(index, 1) + removeBoundingBox(index) { + this.boundingBoxes.splice(index, 1) const canvas = this.$refs.canvas const ctx = canvas.getContext("2d") ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height) - this.drawRectangles() - this.cropImages() + this.drawBoundingBoxes() + this.extractLabels() } } } diff --git a/src/components/ContributionAssistantLabelList.vue b/src/components/ContributionAssistantLabelList.vue new file mode 100644 index 00000000000..4523d9ec331 --- /dev/null +++ b/src/components/ContributionAssistantLabelList.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/ContributionAssistantPriceFormCard.vue b/src/components/ContributionAssistantPriceFormCard.vue index 27df10600a8..71ff0bf0ca7 100644 --- a/src/components/ContributionAssistantPriceFormCard.vue +++ b/src/components/ContributionAssistantPriceFormCard.vue @@ -1,29 +1,25 @@ diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0759cdc0114..00dab126b30 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -288,6 +288,7 @@ "Type": "Type", "Upload": "Upload", "UploadMultipleImages": "Upload {count} image | Upload {count} images", + "UploadMultiplePrices": "Upload {count} price | Upload {count} prices", "URL": "URL", "URLInvalid": "Invalid URL", "User": "User", @@ -302,6 +303,33 @@ "PetFood": "Pet food", "Beauty": "Beauty" }, + "ContributionAssistant": { + "Steps": { + "ProofSelect": "1. Proof selection", + "LabelsExtraction": "2. Labels extraction", + "Cleanup": "3. Cleanup", + "Summary": "4. Summary" + }, + "BoundingBoxesFromServerWarning": "No labels could be automatically detected. Please manually draw squares around the labels or press the button below to try again.", + "FindBoundingBoxes": "Automatically find labels", + "FindBoundingBoxesRunning": "Automatic label detection is running. Please wait...", + "LabelsExtractionSteps": { + "DrawBoundingBoxes": "1. Draw squares around the labels", + "CheckLabels": "2. Check the readability of labels", + "SendLabels": "3. Send the labels for automatic processing", + "SendLabelsButton": "Send labels" + }, + "PriceAddConfirmationMessage": "{numberOfPricesAdded} price(s) will be added to an existing proof on the {date} at {locationName}", + "PriceAddProgress": "{numberOfPricesAdded} / {totalNumberOfPrices} prices added", + "WaitForUpload": "Please wait for upload", + "GoToDashboard": "Go to your dashboard", + "GoToProof": "Go to proof", + "AddNewProof": "Add a new proof", + "AutomaticBoundingBoxSource": "automatic", + "ManualBoundingBoxSource": "manual", + "DetectedBarcode": "Detected barcode: {barcode}", + "LabelProcessingErrorMessage": "Error: label automatic processing failed" + }, "Community": { "JoinUs": "Join us!", "HowToUseTheData": "Use the data" diff --git a/src/services/api.js b/src/services/api.js index 43b0a8fc9ee..a6797ca6adc 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -336,13 +336,13 @@ export default { return this.openstreetmapNominatimSearch(q) } }, - processWithGemini(croppedBlobs) { + processWithGemini(labels) { const store = useAppStore() const url = `${import.meta.env.VITE_OPEN_PRICES_API_URL}/proofs/process_with_gemini` const formData = new FormData() - croppedBlobs.forEach((blob) => { - formData.append('files', blob) + labels.forEach((label) => { + formData.append('files', label.blob) }); return fetch(url, { diff --git a/src/views/ContributionAssistant.vue b/src/views/ContributionAssistant.vue index dfa9744eebd..6be6df91251 100644 --- a/src/views/ContributionAssistant.vue +++ b/src/views/ContributionAssistant.vue @@ -2,16 +2,16 @@ - 1. Proof selection + {{ $t('ContributionAssistant.Steps.ProofSelect') }} - - 2. Image crop + + {{ $t('ContributionAssistant.Steps.LabelsExtraction') }} - 3. Cleanup + {{ $t('ContributionAssistant.Steps.Cleanup') }} - 4. Summary + {{ $t('ContributionAssistant.Steps.Summary') }} @@ -24,41 +24,41 @@ - + - - No labels could be automatically detected. Please manually draw squares around the labels or press the button below to try again. -
- - Automatically find labels + + {{ $t('ContributionAssistant.BoundingBoxesFromServerWarning') }} +
+ + {{ $t('ContributionAssistant.FindBoundingBoxes') }}
- - Automatic label detection is running. Please wait... - + + {{ $t('ContributionAssistant.FindBoundingBoxesRunning') }} +

- 1. Draw squares around the labels + {{ $t('ContributionAssistant.LabelsExtractionSteps.DrawBoundingBoxes') }}

- +

- 2. Check the readability of labels + {{ $t('ContributionAssistant.LabelsExtractionSteps.CheckLabels') }}

- +

- 3. Send the cropped images for automatic processing + {{ $t('ContributionAssistant.LabelsExtractionSteps.SendLabels') }}

- - Process Cropped Images + + {{ $t('ContributionAssistant.LabelsExtractionSteps.SendLabelsButton') }}
@@ -85,11 +85,11 @@ variant="outlined" >

- {{ productPriceForms.length }} price{{ productPriceForms.length > 1 ? 's' : '' }} will be added to an existing proof on the {{ proofForm.date }} at {{ locationName }}. + {{ $t('ContributionAssistant.PriceAddConfirmationMessage', { numberOfPricesAdded: productPriceForms.length, date: proofForm.date, locationName: locationName }) }}

- - {{ $t('Common.Upload') }} + + {{ $t('Common.UploadMultiplePrices', productPriceForms.length) }} @@ -100,7 +100,7 @@

- Please wait for upload + {{ $t('ContributionAssistant.WaitForUpload') }}

- {{ numberOfPricesAdded }} / {{ productPriceForms.length }} prices added + {{ $t('ContributionAssistant.PriceAddProgress', { numberOfPricesAdded: numberOfPricesAdded, totalNumberOfPrices: productPriceForms.length }) }} - Go to your dashboard + {{ $t('ContributionAssistant.GoToDashboard') }} - Go to proof + {{ $t('ContributionAssistant.GoToProof') }} - Add a new proof + {{ $t('ContributionAssistant.AddNewProof') }}
@@ -126,6 +126,13 @@
+ + {{ $t('ContributionAssistant.LabelProcessingErrorMessage') }} +