feat: AI image classification and dynamic fallback assignment
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 44s
Build & Deploy / 🧪 QA (push) Successful in 1m4s
Build & Deploy / 🏗️ Build (push) Successful in 1m54s
Build & Deploy / 🚀 Deploy (push) Successful in 28s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 43s
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-06-16 14:16:39 +02:00
parent e994d79895
commit f0ba5321bb
17 changed files with 695 additions and 35 deletions

View File

@@ -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
<div className="flex flex-col h-full relative">
{/* Image Section */}
<div className="relative h-64 w-full bg-[#f4f4f5] overflow-hidden shrink-0 border-b border-neutral-100">
{ref.frontmatter.featuredImage ? (
<Image
src={ref.frontmatter.featuredImage}
alt={ref.frontmatter.title}
fill
className="object-cover transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] filter grayscale-[20%]"
/>
) : (
<div className="absolute inset-0 bg-[#f4f4f5] flex items-center justify-center">
<Image src="/assets/logo.png" alt="E-TIB Logo" width={80} height={80} className="opacity-20 grayscale" />
</div>
)}
<Image
src={ref.frontmatter.featuredImage || getImageForProject(ref.slug, enrichedLocations.find(l => 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%]"
/>
<div className="absolute inset-0 bg-black/5 transition-colors duration-500" />
{/* Location Badge */}

View File

@@ -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 (
<motion.div

View File

@@ -4,7 +4,6 @@ client: "Energiekontor AG"
date: "2026-06-01"
dateString: "01/2025 bis 06/2026"
location: "18334 Dettmannsdorf"
featuredImage: "/assets/photos/DSC08653.JPG"
---
- 12.300 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,6 @@ client: "wpd GmbH"
date: "2026-03-01"
dateString: "seit 03/2026"
location: "24808 Jevenstedt"
featuredImage: "/assets/photos/DSC01137.JPG"
---
- 500 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,6 @@ client: "E.DIS Netz GmbH"
date: "2025-06-01"
dateString: "01/2025 bis 06/2025"
location: "14669 Ketzin"
featuredImage: "/assets/photos/DJI_0048.JPG"
---
- 1.600 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,6 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
date: "2026-03-01"
dateString: "02/2025 bis 03/2026"
location: "48346 Ostbevern"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
---
- 4.100 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,6 @@ client: "NaturStromAnlagen GmbH"
date: "2025-02-01"
dateString: "04/2024 bis 02/2025"
location: "15326 Petershagen"
featuredImage: "/assets/photos/DSC01123.JPG"
---
- 12.000 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,6 @@ client: "Trianel GmbH"
date: "2025-08-01"
dateString: "02/2025 bis 08/2025"
location: "15345 Prötzel"
featuredImage: "/assets/photos/DJI_0243.JPG"
---
- 2.600 m Kabelgraben ausheben und verfüllen

View File

@@ -4,7 +4,6 @@ client: "Energiekontor AG"
date: "2026-06-01"
dateString: "01/2025 to 06/2026"
location: "18334 Dettmannsdorf"
featuredImage: "/assets/photos/DSC08653.JPG"
---
- Excavation and backfilling of 12,300 m cable trench

View File

@@ -4,7 +4,6 @@ client: "wpd GmbH"
date: "2026-03-01"
dateString: "since 03/2026"
location: "24808 Jevenstedt"
featuredImage: "/assets/photos/DSC01137.JPG"
---
- Excavation and backfilling of 500 m cable trench

View File

@@ -4,7 +4,6 @@ client: "E.DIS Netz GmbH"
date: "2025-06-01"
dateString: "01/2025 to 06/2025"
location: "14669 Ketzin"
featuredImage: "/assets/photos/DJI_0048.JPG"
---
- Excavation and backfilling of 1,600 m cable trench

View File

@@ -4,7 +4,6 @@ client: "GP Joule Anlagenbau GmbH & Co. KG"
date: "2026-03-01"
dateString: "02/2025 to 03/2026"
location: "48346 Ostbevern"
featuredImage: "/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg"
---
- Excavation and backfilling of 4,100 m cable trench

View File

@@ -4,7 +4,6 @@ client: "NaturStromAnlagen GmbH"
date: "2025-02-01"
dateString: "04/2024 to 02/2025"
location: "15326 Petershagen"
featuredImage: "/assets/photos/DSC01123.JPG"
---
- Excavation and backfilling of 12,000 m cable trench

View File

@@ -4,7 +4,6 @@ client: "Trianel GmbH"
date: "2025-08-01"
dateString: "02/2025 to 08/2025"
location: "15345 Prötzel"
featuredImage: "/assets/photos/DJI_0243.JPG"
---
- Excavation and backfilling of 2,600 m cable trench

70
lib/image-matcher.ts Normal file
View File

@@ -0,0 +1,70 @@
import metadataJson from '../public/assets/image-metadata.json';
export interface ImageMeta {
file: string;
category: 'pv' | 'wind' | 'fiber' | 'power' | 'battery' | 'unknown';
environment: 'freies_feld' | 'urban' | 'wald' | 'indoor' | 'unknown';
quality_score: number;
description: string;
}
const metadata = metadataJson as Record<string, ImageMeta>;
// 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;
}

View File

@@ -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"
}
}

135
scripts/classify-images.ts Normal file
View File

@@ -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<ImageMeta | null> {
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<string, ImageMeta> = {};
const metadataPath = path.join(process.cwd(), 'public', 'assets', 'image-metadata.json');
// Load existing to skip if needed, or to resume
let existing: Record<string, ImageMeta> = {};
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);