Fixed the rest
All checks were successful
Build Docker image / build (push) Successful in 2m29s
Build Docker image / deploy (push) Successful in 4s
Build Docker image / verify (push) Successful in 40s

This commit is contained in:
Roman Krček
2025-08-06 15:08:45 +02:00
parent c6cc9c6658
commit 6ed1f985e0
5 changed files with 52 additions and 66 deletions

View File

@@ -5,15 +5,17 @@
import StepColumnMap from './wizard/StepColumnMap.svelte'; import StepColumnMap from './wizard/StepColumnMap.svelte';
import StepRowFilter from './wizard/StepRowFilter.svelte'; import StepRowFilter from './wizard/StepRowFilter.svelte';
import StepCardDetails from './wizard/StepCardDetails.svelte'; import StepCardDetails from './wizard/StepCardDetails.svelte';
// import StepGallery from './wizard/StepGallery.svelte'; import StepGallery from './wizard/StepGallery.svelte';
// import StepGenerate from './wizard/StepGenerate.svelte'; import StepGenerate from './wizard/StepGenerate.svelte';
const steps = [ const steps = [
StepAuth, StepAuth,
StepSheetSearch, StepSheetSearch,
StepColumnMap, StepColumnMap,
StepRowFilter, StepRowFilter,
StepCardDetails StepCardDetails,
StepGallery,
StepGenerate
]; ];
const stepTitles = [ const stepTitles = [
@@ -21,7 +23,9 @@
'Select Sheet', 'Select Sheet',
'Map Columns', 'Map Columns',
'Filter Rows', 'Filter Rows',
'Enter Card Details' 'Enter Card Details',
'Preview Gallery',
'Generate Cards'
]; ];
</script> </script>

View File

@@ -28,7 +28,6 @@
console.error('Failed to save to localStorage:', error); console.error('Failed to save to localStorage:', error);
} }
$cardDetails = { homeSection, validityStart }; $cardDetails = { homeSection, validityStart };
$currentStep++;
} }
</script> </script>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { columnMapping, filteredSheetData, currentStep, pictures, cropRects } from '$lib/stores'; import { columnMapping, sheetData, currentStep, pictures, cropRects } from '$lib/stores';
import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google'; import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google';
import Navigator from './subcomponents/Navigator.svelte'; import Navigator from './subcomponents/Navigator.svelte';
import PhotoCard from './subcomponents/PhotoCard.svelte'; import PhotoCard from './subcomponents/PhotoCard.svelte';
@@ -61,8 +61,6 @@
} }
async function processPhotosInParallel() { async function processPhotosInParallel() {
if (isProcessing) return;
console.log('Starting processPhotos with queues...'); console.log('Starting processPhotos with queues...');
isProcessing = true; isProcessing = true;
processedCount = 0; processedCount = 0;
@@ -73,6 +71,7 @@
console.log('Cleared IndexedDB.'); console.log('Cleared IndexedDB.');
} catch (e) { } catch (e) {
console.error('Could not clear IndexedDB:', e); console.error('Could not clear IndexedDB:', e);
return;
} }
// Initialize queues with more conservative concurrency // Initialize queues with more conservative concurrency
@@ -95,7 +94,7 @@
} }
}); });
const validRows = $filteredSheetData.filter((row) => row._isValid); const validRows = $sheetData.filter((row) => row._valid);
const photoUrls = new Set<string>(); const photoUrls = new Set<string>();
const photoMap = new Map<string, any[]>(); const photoMap = new Map<string, any[]>();
@@ -130,16 +129,12 @@
// Initialize detector and process photos // Initialize detector and process photos
onMount(() => { onMount(() => {
console.log('StepGallery mounted');
initializeDetector(); // Start loading model initializeDetector(); // Start loading model
if ($filteredSheetData.length > 0 && $columnMapping.pictureUrl !== undefined) { if ($sheetData.length > 0 && $columnMapping.pictureUrl !== undefined) {
console.log('Processing photos for gallery step'); console.log('Processing photos for gallery step');
processPhotosInParallel(); processPhotosInParallel();
} else { } else {
console.log('No data to process:', { console.log('No data to process: !');
dataLength: $filteredSheetData.length,
pictureUrlMapping: $columnMapping.pictureUrl
});
} }
}); });
@@ -148,7 +143,6 @@
if (!isRetry) { if (!isRetry) {
photo.status = 'loading'; photo.status = 'loading';
// No need to reassign photos array with $state reactivity
} }
try { try {
@@ -285,14 +279,6 @@
imageTensor = tf.browser.fromPixels(img); imageTensor = tf.browser.fromPixels(img);
const predictions = await detector.estimateFaces(imageTensor, false); const predictions = await detector.estimateFaces(imageTensor, false);
// Log memory usage for debugging
const memInfo = tf.memory();
console.log(`TensorFlow.js memory after face detection for ${photo.name}:`, {
numTensors: memInfo.numTensors,
numDataBuffers: memInfo.numDataBuffers,
numBytes: memInfo.numBytes
});
if (predictions.length > 0) { if (predictions.length > 0) {
const getProbability = (p: number | tf.Tensor) => const getProbability = (p: number | tf.Tensor) =>
typeof p === 'number' ? p : p.dataSync()[0]; typeof p === 'number' ? p : p.dataSync()[0];
@@ -518,23 +504,6 @@
<div class="text-gray-600">Failed</div> <div class="text-gray-600">Failed</div>
</div> </div>
</div> </div>
{#if photos.filter((p) => p.status === 'error').length > 0}
<div class="mt-4 rounded border border-yellow-200 bg-yellow-50 p-3">
<p class="text-sm text-yellow-800">
<strong>Note:</strong> Cards will only be generated for photos that load successfully.
</p>
</div>
{/if}
{#if !canProceed() && photos.filter((p) => p.status === 'success').length > 0}
<div class="mt-4 rounded border border-blue-200 bg-blue-50 p-3">
<p class="text-sm text-blue-800">
<strong>Tip:</strong> All photos need to be cropped before proceeding. Face detection runs
automatically.
</p>
</div>
{/if}
</div> </div>
{/if} {/if}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { filteredSheetData, currentStep, pictures, cropRects } from '$lib/stores'; import { sheetData, currentStep, pictures, cropRects } from '$lib/stores';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import * as fontkit from 'fontkit'; import * as fontkit from 'fontkit';
import { clear } from 'idb-keyval'; import { clear } from 'idb-keyval';
@@ -30,11 +30,12 @@
url: string | null; url: string | null;
size: number | null; size: number | null;
error: string | null; error: string | null;
downloadName?: string;
}; };
const initialFiles: GeneratedFile[] = [ const initialFiles: GeneratedFile[] = [
{ {
name: 'people_data.pdf', name: 'esncards_text.pdf',
displayName: 'Text PDF', displayName: 'Text PDF',
state: 'idle', state: 'idle',
url: null, url: null,
@@ -42,7 +43,7 @@
error: null error: null
}, },
{ {
name: 'people_photos.pdf', name: 'esncards_photos.pdf',
displayName: 'Photos PDF', displayName: 'Photos PDF',
state: 'idle', state: 'idle',
url: null, url: null,
@@ -71,11 +72,11 @@
onMount(() => { onMount(() => {
// Add event listener for page unload // Add event listener for page unload
window.addEventListener('beforeunload', handleBeforeUnload); window.addEventListener('beforeunload', handleBeforeUnload);
// Start generation automatically when the component mounts // Start generation automatically when the component mounts
handleGenerate('people_data.pdf'); handleGenerate('esncards_text.pdf');
handleGenerate('people_photos.pdf'); handleGenerate('esncards_photos.pdf');
// Cleanup function when component unmounts // Cleanup function when component unmounts
return () => { return () => {
window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload);
@@ -97,6 +98,17 @@
} }
} }
// Create a formatted timestamp string
function getTimestamp(): string {
const d = new Date();
const year = d.getFullYear();
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const hours = d.getHours().toString().padStart(2, '0');
const minutes = d.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day}-${hours}-${minutes}`;
}
// Crop image using canvas // Crop image using canvas
async function cropImage( async function cropImage(
imageBlob: Blob, imageBlob: Blob,
@@ -163,7 +175,7 @@
try { try {
const pdfBytes = const pdfBytes =
fileName === 'people_data.pdf' ? await generateTextPDF() : await generatePhotoPDF(); fileName === 'esncards_text.pdf' ? await generateTextPDF() : await generatePhotoPDF();
const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const blob = new Blob([pdfBytes], { type: 'application/pdf' });
@@ -176,6 +188,10 @@
fileToUpdate.size = pdfBytes.length; fileToUpdate.size = pdfBytes.length;
fileToUpdate.state = 'done'; fileToUpdate.state = 'done';
const timestamp = getTimestamp();
const baseName = fileName.replace('.pdf', '');
fileToUpdate.downloadName = `${baseName}_${timestamp}.pdf`;
// Check if both PDFs are done, then clear sensitive data // Check if both PDFs are done, then clear sensitive data
const allDone = files.every((f) => f.state === 'done' || f.state === 'error'); const allDone = files.every((f) => f.state === 'done' || f.state === 'error');
if (allDone) { if (allDone) {
@@ -214,7 +230,7 @@
let currentRow = 0; let currentRow = 0;
let currentCol = 0; let currentCol = 0;
const validRows = $filteredSheetData.filter((row) => row._isValid); const validRows = $sheetData.filter((row) => row._valid);
for (let i = 0; i < validRows.length; i++) { for (let i = 0; i < validRows.length; i++) {
const row = validRows[i]; const row = validRows[i];
@@ -224,11 +240,10 @@
const cellY_mm = PAGE_SETTINGS.margin + currentRow * gridLayout.cellHeight; const cellY_mm = PAGE_SETTINGS.margin + currentRow * gridLayout.cellHeight;
// Get field values // Get field values
const name = row.name || row.Name || ''; const name = row.name;
const surname = row.surname || row.Surname || row.lastname || row.LastName || ''; const surname = row.surname;
const nationality = row.nationality || row.Nationality || row.country || row.Country || ''; const nationality = row.nationality;
const birthday = const birthday = row.birthday;
row.birthday || row.Birthday || row.birthdate || row.Birthdate || row.birth_date || '';
// Draw name // Draw name
const namePos = getAbsolutePositionPt( const namePos = getAbsolutePositionPt(
@@ -319,7 +334,7 @@
let currentRow = 0; let currentRow = 0;
let currentCol = 0; let currentCol = 0;
const validRows = $filteredSheetData.filter((row) => row._isValid); const validRows = $sheetData.filter((row) => row._valid);
for (let i = 0; i < validRows.length; i++) { for (let i = 0; i < validRows.length; i++) {
const row = validRows[i]; const row = validRows[i];
@@ -336,7 +351,7 @@
PHOTO_FIELD_LAYOUT.photo PHOTO_FIELD_LAYOUT.photo
); );
const pictureUrl = row.pictureUrl || row.picture_url || row.Picture || row.PictureUrl; const pictureUrl = row.pictureUrl;
const pictureInfo = $pictures[pictureUrl]; const pictureInfo = $pictures[pictureUrl];
const cropData = $cropRects[pictureUrl]; const cropData = $cropRects[pictureUrl];
@@ -397,8 +412,8 @@
} }
// Draw name // Draw name
const name = row.name || row.Name || ''; const name = row.name;
const surname = row.surname || row.Surname || row.lastname || row.LastName || ''; const surname = row.surname;
const namePos = getAbsolutePositionPt( const namePos = getAbsolutePositionPt(
cellX_mm, cellX_mm,
cellY_mm, cellY_mm,
@@ -430,7 +445,7 @@
if (!file.url) return; if (!file.url) return;
const link = document.createElement('a'); const link = document.createElement('a');
link.href = file.url; link.href = file.url;
link.download = file.name; link.download = file.downloadName || file.name;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
@@ -443,10 +458,10 @@
} }
}); });
files = JSON.parse(JSON.stringify(initialFiles)); files = JSON.parse(JSON.stringify(initialFiles));
// Clear sensitive data when starting over // Clear sensitive data when starting over
clearSensitiveData(); clearSensitiveData();
currentStep.set(0); currentStep.set(0);
} }
@@ -478,7 +493,7 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-gray-900"> <div class="text-2xl font-bold text-gray-900">
{$filteredSheetData.filter((row) => row._isValid).length} {$sheetData.filter((row) => row._valid).length}
</div> </div>
<div class="text-gray-600">Records to Process</div> <div class="text-gray-600">Records to Process</div>
</div> </div>
@@ -590,7 +605,7 @@
<!-- Navigation --> <!-- Navigation -->
<div class="flex justify-between"> <div class="flex justify-between">
<button <button
onclick={() => currentStep.set(5)} onclick={() => currentStep.set(6)}
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
> >
← Back to Gallery ← Back to Gallery

View File

@@ -38,7 +38,6 @@ export interface RowData {
// Picture storage and metadata // Picture storage and metadata
export interface PictureBlobInfoType { export interface PictureBlobInfoType {
id: string; id: string;
blob: Blob;
url: string; url: string;
downloaded: boolean; downloaded: boolean;
faceDetected: boolean; faceDetected: boolean;
@@ -87,7 +86,7 @@ export const sheetData = writable<RowData[]>([]);
export const pictures = writable<Record<string, PictureBlobInfoType>>({}); export const pictures = writable<Record<string, PictureBlobInfoType>>({});
// Store and hold the crop rectangles from face detection // Store and hold the crop rectangles from face detection
export const CropRects = writable<Record<string, CropType>>({}); export const cropRects = writable<Record<string, CropType>>({});
// Store and hold the selected sheet // Store and hold the selected sheet
export const selectedSheet = writable<SheetInfoType>({ id: '', name: '', webViewLink: '' }); export const selectedSheet = writable<SheetInfoType>({ id: '', name: '', webViewLink: '' });