diff --git a/.env.example b/.env.example index 9716867..b2d1f72 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,15 @@ # Your Google Cloud OAuth 2.0 Client ID VITE_GOOGLE_CLIENT_ID="YOUR_GOOGLE_CLIENT_ID_HERE" + +# Face Detection Crop Configuration +# Crop aspect ratio (width:height) - e.g., 1.0 for square, 1.5 for 3:2 ratio +VITE_CROP_RATIO=1.0 + +# Face offset from center (as percentage of crop dimensions) +# Positive values move the face toward bottom-right, negative toward top-left +VITE_FACE_OFFSET_X=0.0 +VITE_FACE_OFFSET_Y=-0.1 + +# Crop scale multiplier based on face width +# 1.0 = crop width equals face width, 2.0 = crop is 2x face width +VITE_CROP_SCALE=2.5 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a69ccc3..0e8ba3a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,7 +2,11 @@ - You are a helpful AI assistant that helps developers write code. - This code is written in Svelte 5 - It's important to only use modern Svelte 5 syntax, runes, and features. + - Do not use $:, do not use eventDispatching as they are both deprecated + - User $effect, $state, $derived + - Pass fucntions as props instead od dispatching events - Use styling from ".github/styling.md" for any UI components. - Refer to the ".github/core-instructions.md" for the overall structure of the application. - Generate ".github/done.md" file to see what is done and what is not. Check it when you start and finish a task. -- Remain consistent in styling and code structure. \ No newline at end of file +- Remain consistent in styling and code structure. +- Avoid unncessary iterations. If problems is mostly solved, stop. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 86a51c0..4ed9931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "esn-card-generator", "version": "0.0.1", "dependencies": { + "@tensorflow-models/blazeface": "^0.1.0", "@tensorflow/tfjs": "^4.22.0", "@tensorflow/tfjs-backend-webgl": "^4.22.0", "@types/gapi": "^0.0.47", @@ -1349,6 +1350,16 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tensorflow-models/blazeface": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@tensorflow-models/blazeface/-/blazeface-0.1.0.tgz", + "integrity": "sha512-Qc5Wii8/OE5beC7XfehkhF9SEFLaPbVKnxxalV0T9JXsUynXqvLommc9Eko7b8zXKy4SJ1BtVlcX2cmCzQrn9A==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-converter": "^4.10.0", + "@tensorflow/tfjs-core": "^4.10.0" + } + }, "node_modules/@tensorflow/tfjs": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.22.0.tgz", diff --git a/package.json b/package.json index 829bc70..56af0b8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "@tensorflow/tfjs": "^4.22.0", "@tensorflow/tfjs-backend-webgl": "^4.22.0", + "@tensorflow-models/blazeface": "^0.1.0", "@types/gapi": "^0.0.47", "@types/gapi.client.drive": "^3.0.15", "@types/gapi.client.sheets": "^4.0.20201031", diff --git a/src/app.html b/src/app.html index 1391f88..e810908 100644 --- a/src/app.html +++ b/src/app.html @@ -1,6 +1,7 @@ + ESN Card Generator diff --git a/src/lib/components/PhotoCard.svelte b/src/lib/components/PhotoCard.svelte new file mode 100644 index 0000000..b380650 --- /dev/null +++ b/src/lib/components/PhotoCard.svelte @@ -0,0 +1,394 @@ + + +
+
+ {personName} + + + {#if isDownloadingModel} +
+ + + + + Downloading AI Model... +
+
+
+
+ {:else if isModelLoading} +
+ + + + + Loading AI Model... +
+
+
+
+ {:else if isDetectingFace} +
+ + + + Detecting Face + + . + . + . + + +
+
+
+
+ {/if} + + {#if currentCrop} + +
+
+ +
+
+
+ {/if} + + + + + +
+ {#if faceDetectionError} +
+ Manual crop +
+ {:else if currentCrop && autoDetectedCrop && JSON.stringify(currentCrop) !== JSON.stringify(autoDetectedCrop)} +
+ Custom crop +
+ {:else if autoDetectedCrop} +
+ Auto-cropped +
+ {/if} +
+
+ +
+

{personName}

+ {#if isProcessing} +

Processing...

+ {:else if faceDetectionError} +

Using center crop

+ {:else if autoDetectedCrop} +

Face detected

+ {/if} +
+
+ +{#if showCropEditor} + +{/if} diff --git a/src/lib/components/PhotoCrop.svelte b/src/lib/components/PhotoCrop.svelte new file mode 100644 index 0000000..a47c697 --- /dev/null +++ b/src/lib/components/PhotoCrop.svelte @@ -0,0 +1,360 @@ + + +
+
+
+
+

+ Crop Photo - {personName} +

+ + +
+ +
+
+ +
+ +

+ Drag the crop area to move it, or drag the corner handles to resize. + The selected area will be used for the member card. +
+ Aspect Ratio: {cropRatio.toFixed(1)}:1 {cropRatio === 1.0 ? '(Square)' : cropRatio === 1.5 ? '(3:2)' : ''} +

+ +
+ + + +
+
+
+
+
diff --git a/src/lib/components/Wizard.svelte b/src/lib/components/Wizard.svelte index d6bbc2f..13af3c4 100644 --- a/src/lib/components/Wizard.svelte +++ b/src/lib/components/Wizard.svelte @@ -4,20 +4,23 @@ import StepSheetSearch from './wizard/StepSheetSearch.svelte'; import StepColumnMap from './wizard/StepColumnMap.svelte'; import StepRowFilter from './wizard/StepRowFilter.svelte'; + import StepGallery from './wizard/StepGallery.svelte'; // Additional steps to be added as they are implemented const steps = [ StepAuth, StepSheetSearch, StepColumnMap, - StepRowFilter + StepRowFilter, + StepGallery ]; const stepTitles = [ 'Authenticate', 'Select Sheet', 'Map Columns', - 'Filter Rows' + 'Filter Rows', + 'Review Photos' ]; function goToPreviousStep() { diff --git a/src/lib/components/wizard/StepGallery.svelte b/src/lib/components/wizard/StepGallery.svelte index 9696c76..ebb43bc 100644 --- a/src/lib/components/wizard/StepGallery.svelte +++ b/src/lib/components/wizard/StepGallery.svelte @@ -1,4 +1,402 @@ + +
-

Review Photos

-

Photo gallery and review functionality will be implemented here.

+
+
+

+ Review & Crop Photos +

+ +

+ Photos are automatically cropped using face detection. Click the pen icon to manually adjust the crop area. +

+
+ + + {#if isProcessing} +
+
+
+
+ + Processing photos... + +
+ + {processedCount} / {totalCount} + +
+ + {#if totalCount > 0} +
+
+
+ {/if} +
+ {:else if faceDetectionInProgress} +
+
+
+
+ + Detecting faces and auto-cropping... + +
+ + {faceDetectionCount.completed} / {faceDetectionCount.started} + +
+ + {#if faceDetectionCount.started > 0} +
+
+
+ {/if} +
+ {/if} + + + {#if !isProcessing && !faceDetectionInProgress && photos.length > 0} +
+

Processing Summary

+ +
+
+
{photos.length}
+
Total Photos
+
+ +
+
+ {photos.filter(p => p.status === 'success').length} +
+
Loaded
+
+ +
+
+ {photos.filter(p => p.faceDetectionStatus === 'completed').length} +
+
Auto-cropped
+
+ +
+
+ {photos.filter(p => p.cropData).length} +
+
Ready
+
+ +
+
+ {photos.filter(p => p.status === 'error').length} +
+
Failed
+
+
+ + {#if photos.filter(p => p.status === 'error').length > 0} +
+

+ Note: Cards will only be generated for photos that load successfully. +

+
+ {/if} + + {#if !canProceed() && photos.filter(p => p.status === 'success').length > 0} +
+

+ Tip: All photos need to be cropped before proceeding. Face detection runs automatically. +

+
+ {/if} +
+ {/if} + + +
+ {#if photos.length === 0 && !isProcessing} +
+ + + +

No photos found

+

+ Go back to check your column mapping and selected rows. +

+
+ {:else} +
+
+ {#each photos as photo, index} + {#if photo.status === 'loading'} +
+
+
+
+ Loading... +
+
+
+

{photo.name}

+ Processing photo... +
+
+ {:else if photo.status === 'success' && photo.objectUrl} + handleCropUpdate(index, e.detail)} + on:faceDetectionStarted={() => handleFaceDetectionStarted(index)} + on:faceDetectionCompleted={(e) => handleFaceDetectionCompleted(index, e.detail)} + /> + {:else if photo.status === 'error'} +
+
+
+ + + + Failed to load + +
+
+
+

{photo.name}

+ Failed to load +
+
+ {/if} + {/each} +
+
+ {/if} +
+ + +
+ + + +
+
+ diff --git a/src/lib/components/wizard/StepRowFilter.svelte b/src/lib/components/wizard/StepRowFilter.svelte index d3c0527..dcf74f8 100644 --- a/src/lib/components/wizard/StepRowFilter.svelte +++ b/src/lib/components/wizard/StepRowFilter.svelte @@ -1,5 +1,6 @@