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 ? (
-
- ) : (
-
-
-
- )}
+
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);