diff --git a/app/[locale]/referenzen/page.tsx b/app/[locale]/referenzen/page.tsx index 5748c867f..522f9f704 100644 --- a/app/[locale]/referenzen/page.tsx +++ b/app/[locale]/referenzen/page.tsx @@ -9,6 +9,7 @@ import { SITE_URL } from '@/lib/schema'; import Image from 'next/image'; import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap'; import { defaultLocations, minorLocations } from '@/lib/map-data'; +import { getImageForProject } from '@/lib/image-matcher'; interface PageProps { params: Promise<{ @@ -59,7 +60,7 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca if (ref) { return { ...loc, - featuredImage: ref.frontmatter.featuredImage, + featuredImage: ref.frontmatter.featuredImage || getImageForProject(ref.slug, loc.description), details: [ ref.frontmatter.client || 'Kunde', ref.frontmatter.dateString || new Date(ref.frontmatter.date).getFullYear().toString(), @@ -67,7 +68,10 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca }; } } - return loc; + return { + ...loc, + featuredImage: loc.featuredImage || getImageForProject(loc.id, loc.description) + }; }); return ( @@ -99,18 +103,12 @@ export default async function ReferenzenOverview(props: { params: Promise<{ loca
{/* Image Section */}
- {ref.frontmatter.featuredImage ? ( - {ref.frontmatter.title} - ) : ( -
- E-TIB Logo -
- )} + l.id === ref.slug)?.description || 'fiber')} + alt={ref.frontmatter.title} + fill + className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%]" + />
{/* Location Badge */} diff --git a/components/blocks/ReferencesSlider.tsx b/components/blocks/ReferencesSlider.tsx index 675751263..02e3b6ac9 100644 --- a/components/blocks/ReferencesSlider.tsx +++ b/components/blocks/ReferencesSlider.tsx @@ -4,8 +4,9 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import Link from 'next/link'; import Image from 'next/image'; -import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay'; +import { HoverShineOverlay } from "@/components/ui/HoverShineOverlay"; import { useTranslations, useLocale } from 'next-intl'; +import { getImageForProject } from '@/lib/image-matcher'; export interface Reference { id: string; @@ -55,13 +56,15 @@ export function ReferencesSlider(props: ReferencesSliderProps) { if (!references || references.length === 0) return null; - // Fallback images pool if CMS doesn't provide one - const fallbacks = [ - '/assets/photos/DSC01123.JPG', - '/assets/photos/DSC00850.JPG', - '/assets/photos/DSC01129.JPG', - '/assets/photos/DSC00010.JPG', - ]; + // Map the display category back to our internal image matcher categories + const mapCategoryToInternal = (cat: string) => { + const lower = cat.toLowerCase(); + if (lower.includes('pv') || lower.includes('solar')) return 'pv'; + if (lower.includes('wind')) return 'wind'; + if (lower.includes('trasse') || lower.includes('spannung') || lower.includes('power')) return 'power'; + if (lower.includes('batterie') || lower.includes('bess') || lower.includes('speicher')) return 'battery'; + return 'fiber'; // fallback for Tiefbau / Breitband + }; const onMouseDown = (e: React.MouseEvent) => { if (!containerRef.current) return; @@ -119,7 +122,9 @@ export function ReferencesSlider(props: ReferencesSliderProps) { {references.map((ref, i) => { const imgSrc = (ref.image && typeof ref.image === 'object' && ref.image.url) ? ref.image.url - : fallbacks[i % fallbacks.length]; + : typeof ref.image === 'string' + ? ref.image + : getImageForProject(ref.slug || ref.id, mapCategoryToInternal(ref.category)); return ( ; + +// Create a deterministic pseudo-random number based on a string +function hashString(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash); +} + +/** + * Returns a fitting, highly-rated image for a specific project type. + * It is deterministic based on the seed (e.g. project ID). + */ +export function getImageForProject(seed: string, category: string): string { + const allImages = Object.values(metadata); + + // 1. Filter by category + let candidates = allImages.filter(img => img.category === category); + + // If no candidates for category, fallback to fiber (Kabeltiefbau is safe default for ETIB) + if (candidates.length === 0) { + candidates = allImages.filter(img => img.category === 'fiber'); + } + + // Still empty? Just get anything highly rated + if (candidates.length === 0) { + candidates = allImages; + } + + // 2. Sort by quality score (descending) + candidates.sort((a, b) => b.quality_score - a.quality_score); + + // 3. For PV and Wind, strongly prefer 'freies_feld' + if (category === 'pv' || category === 'wind') { + const fieldCandidates = candidates.filter(img => img.environment === 'freies_feld' && img.quality_score >= 6); + if (fieldCandidates.length > 0) { + candidates = fieldCandidates; + } + } else { + // For others, just take top 30% quality to have some variation + candidates = candidates.filter(img => img.quality_score >= 6); + if (candidates.length === 0) { + candidates = allImages.filter(img => img.category === category); + } + } + + if (candidates.length === 0) { + return '/assets/logo.png'; // Ultimate fallback + } + + // 4. Deterministically pick one based on the seed + const hash = hashString(seed); + const index = hash % candidates.length; + + return candidates[index].file; +} diff --git a/public/assets/image-metadata.json b/public/assets/image-metadata.json new file mode 100644 index 000000000..20ae6b756 --- /dev/null +++ b/public/assets/image-metadata.json @@ -0,0 +1,464 @@ +{ + "/assets/photos/2025-09-23_12.11.43.jpg": { + "file": "/assets/photos/2025-09-23_12.11.43.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien" + }, + "/assets/photos/2025-09-23_12.12.37.jpg": { + "file": "/assets/photos/2025-09-23_12.12.37.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DJI_0037.JPG": { + "file": "/assets/photos/DJI_0037.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DJI_0048.JPG": { + "file": "/assets/photos/DJI_0048.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DJI_0243.JPG": { + "file": "/assets/photos/DJI_0243.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DJI_0301.JPG": { + "file": "/assets/photos/DJI_0301.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00010.JPG": { + "file": "/assets/photos/DSC00010.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00033.JPG": { + "file": "/assets/photos/DSC00033.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00373.jpg": { + "file": "/assets/photos/DSC00373.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00375.jpg": { + "file": "/assets/photos/DSC00375.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC00388.jpg": { + "file": "/assets/photos/DSC00388.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solaranlage im Freien" + }, + "/assets/photos/DSC00400.jpg": { + "file": "/assets/photos/DSC00400.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarmodule auf einem Feld" + }, + "/assets/photos/DSC00430.jpg": { + "file": "/assets/photos/DSC00430.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00440.jpg": { + "file": "/assets/photos/DSC00440.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00452.jpg": { + "file": "/assets/photos/DSC00452.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00808.JPG": { + "file": "/assets/photos/DSC00808.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00830.JPG": { + "file": "/assets/photos/DSC00830.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC00850.JPG": { + "file": "/assets/photos/DSC00850.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01021.JPG": { + "file": "/assets/photos/DSC01021.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01022.JPG": { + "file": "/assets/photos/DSC01022.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC01103.JPG": { + "file": "/assets/photos/DSC01103.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC01119.JPG": { + "file": "/assets/photos/DSC01119.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01122.JPG": { + "file": "/assets/photos/DSC01122.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01123.JPG": { + "file": "/assets/photos/DSC01123.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01129.JPG": { + "file": "/assets/photos/DSC01129.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01137.JPG": { + "file": "/assets/photos/DSC01137.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01163.JPG": { + "file": "/assets/photos/DSC01163.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01168.JPG": { + "file": "/assets/photos/DSC01168.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01174.JPG": { + "file": "/assets/photos/DSC01174.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01178.JPG": { + "file": "/assets/photos/DSC01178.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark auf freiem Feld" + }, + "/assets/photos/DSC01200.JPG": { + "file": "/assets/photos/DSC01200.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC01567.JPG": { + "file": "/assets/photos/DSC01567.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC01572.JPG": { + "file": "/assets/photos/DSC01572.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02028.jpg": { + "file": "/assets/photos/DSC02028.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02057.jpg": { + "file": "/assets/photos/DSC02057.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02069.jpg": { + "file": "/assets/photos/DSC02069.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02083.jpg": { + "file": "/assets/photos/DSC02083.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarmodule auf einem Lkw" + }, + "/assets/photos/DSC02089.jpg": { + "file": "/assets/photos/DSC02089.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02114.jpg": { + "file": "/assets/photos/DSC02114.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02135.jpg": { + "file": "/assets/photos/DSC02135.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC02150.jpg": { + "file": "/assets/photos/DSC02150.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark mit PV-Modulen auf einem Freigelände" + }, + "/assets/photos/DSC02152.jpg": { + "file": "/assets/photos/DSC02152.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02156.jpg": { + "file": "/assets/photos/DSC02156.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02164.jpg": { + "file": "/assets/photos/DSC02164.jpg", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC02639.JPG": { + "file": "/assets/photos/DSC02639.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02642.JPG": { + "file": "/assets/photos/DSC02642.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02643.JPG": { + "file": "/assets/photos/DSC02643.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC02656.JPG": { + "file": "/assets/photos/DSC02656.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC02676.JPG": { + "file": "/assets/photos/DSC02676.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC02690.JPG": { + "file": "/assets/photos/DSC02690.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark" + }, + "/assets/photos/DSC02691.JPG": { + "file": "/assets/photos/DSC02691.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark mit PV-Modulen auf einem Freigelände" + }, + "/assets/photos/DSC02693.JPG": { + "file": "/assets/photos/DSC02693.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark" + }, + "/assets/photos/DSC02700.JPG": { + "file": "/assets/photos/DSC02700.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien Feld" + }, + "/assets/photos/DSC03612.JPG": { + "file": "/assets/photos/DSC03612.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC08626.JPG": { + "file": "/assets/photos/DSC08626.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarmodul im Freien" + }, + "/assets/photos/DSC08641.JPG": { + "file": "/assets/photos/DSC08641.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC08645.JPG": { + "file": "/assets/photos/DSC08645.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC08649.JPG": { + "file": "/assets/photos/DSC08649.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark im Freien" + }, + "/assets/photos/DSC08653.JPG": { + "file": "/assets/photos/DSC08653.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark" + }, + "/assets/photos/DSC08744.JPG": { + "file": "/assets/photos/DSC08744.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC08753.JPG": { + "file": "/assets/photos/DSC08753.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solaranlage im Freien" + }, + "/assets/photos/DSC09945.JPG": { + "file": "/assets/photos/DSC09945.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC09951.JPG": { + "file": "/assets/photos/DSC09951.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 8, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC09953.JPG": { + "file": "/assets/photos/DSC09953.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC09955.JPG": { + "file": "/assets/photos/DSC09955.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark in freiem Feld" + }, + "/assets/photos/DSC09960.JPG": { + "file": "/assets/photos/DSC09960.JPG", + "category": "pv", + "environment": "freies_feld", + "quality_score": 7, + "description": "Solarpark" + } +} \ No newline at end of file diff --git a/scripts/classify-images.ts b/scripts/classify-images.ts new file mode 100644 index 000000000..888e22ba2 --- /dev/null +++ b/scripts/classify-images.ts @@ -0,0 +1,135 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const OLLAMA_API = 'http://127.0.0.1:11434/api/generate'; +const MODEL = "llava:latest"; + +const SYSTEM_PROMPT = `Du bist ein KI-Assistent zur Klassifizierung von Bildern für ein Infrastruktur-Unternehmen. +Bitte analysiere das Bild und antworte AUSSCHLIESSLICH mit einem validen JSON-Objekt, das folgendem Schema entspricht: +{ + "category": "pv" | "wind" | "fiber" | "power" | "battery" | "unknown", + "environment": "freies_feld" | "urban" | "wald" | "indoor" | "unknown", + "quality_score": number, // 1-10, wie gut eignet sich das Bild als Hero/Titelbild? (Schärfe, Ästhetik, Belichtung) + "description": "string" // Kurze deutsche Beschreibung, max 10 Wörter +} + +Erklärung der Kategorien: +- "pv": Photovoltaik-Anlagen, Solarparks, Solarmodule +- "wind": Windräder, Windparks +- "fiber": Kabeltiefbau, Spülbohrtechnik, Bagger bei Kabelverlegung, Glasfaser +- "power": Umspannwerke, Hochspannungstrassen, Strommasten +- "battery": Batterie-Speichersysteme (BESS), Container-Speicher + +Erklärung der Umgebung: +- "freies_feld": Offenes Feld, Wiese, Natur (vom Kunden stark bevorzugt für PV und Wind!) +- "urban": Stadt, Straße, Siedlung +- "wald": Im Wald, viele Bäume nah dran +- "indoor": Innenräume + +WICHTIG: Gib NUR JSON zurück. Keinen Markdown-Code-Block. Nichts anderes. +`; + +interface ImageMeta { + file: string; + category: 'pv' | 'wind' | 'fiber' | 'power' | 'battery' | 'unknown'; + environment: 'freies_feld' | 'urban' | 'wald' | 'indoor' | 'unknown'; + quality_score: number; + description: string; +} + +async function classifyImage(filePath: string): Promise { + try { + const ext = path.extname(filePath).toLowerCase(); + if (!['.jpg', '.jpeg', '.png', '.webp', '.avif'].includes(ext)) { + return null; + } + + const imageBuffer = await fs.readFile(filePath); + const base64Image = imageBuffer.toString('base64'); + + console.log(`Analyzing ${path.basename(filePath)}...`); + + const response = await fetch(OLLAMA_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: MODEL, + prompt: "Bitte klassifiziere dieses Bild gemäß den Anweisungen und antworte nur mit JSON.", + system: SYSTEM_PROMPT, + images: [base64Image], + stream: false, + format: "json", + options: { + temperature: 0.1, + num_predict: 200, + } + }) + }); + + if (!response.ok) { + console.error(`Ollama error for ${filePath}: ${response.status} ${response.statusText}`); + return null; + } + + const result = await response.json(); + let responseText = result.response.trim(); + + // Fallback if model wraps in code blocks + if (responseText.startsWith('\`\`\`json')) { + responseText = responseText.replace(/^\`\`\`json/m, '').replace(/\`\`\`$/m, '').trim(); + } + + const parsed = JSON.parse(responseText); + + return { + file: '/assets/photos/' + path.basename(filePath), + category: parsed.category || 'unknown', + environment: parsed.environment || 'unknown', + quality_score: parsed.quality_score || 5, + description: parsed.description || '' + }; + + } catch (error) { + console.error(`Error processing ${filePath}:`, error); + return null; + } +} + +async function main() { + const photosDir = path.join(process.cwd(), 'public', 'assets', 'photos'); + const files = await fs.readdir(photosDir); + const metadataMap: Record = {}; + + const metadataPath = path.join(process.cwd(), 'public', 'assets', 'image-metadata.json'); + + // Load existing to skip if needed, or to resume + let existing: Record = {}; + try { + const existingData = await fs.readFile(metadataPath, 'utf-8'); + existing = JSON.parse(existingData); + } catch (e) { + // Ignore if not exists + } + + for (const file of files) { + const filePath = path.join(photosDir, file); + const relativePath = '/assets/photos/' + file; + + if (existing[relativePath] && existing[relativePath].category !== 'unknown') { + console.log(`Skipping ${file}, already analyzed.`); + metadataMap[relativePath] = existing[relativePath]; + continue; + } + + const meta = await classifyImage(filePath); + if (meta) { + metadataMap[relativePath] = meta; + // Save incrementally so we don't lose progress if it crashes + await fs.writeFile(metadataPath, JSON.stringify(metadataMap, null, 2)); + } + } + + console.log('Classification complete!'); +} + +main().catch(console.error);