From a8978373e07c32b65543684347603e39a3ca8b40 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 19 Jun 2025 01:59:36 +0100 Subject: [PATCH] chore: add color analysis scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - analyze-image-colors.ts: Test color extraction on local images - check-photo-colors.ts: Verify color data in database - find-image-colors.ts: Extract colors from Cloudinary URLs - reanalyze-colors.ts: Bulk reprocess colors for all media šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- scripts/analyze-image-colors.ts | 135 ++++++++++++++++++++++++++ scripts/check-photo-colors.ts | 63 ++++++++++++ scripts/find-image-colors.ts | 89 +++++++++++++++++ scripts/reanalyze-colors.ts | 167 ++++++++++++++++++++++++++++++++ 4 files changed, 454 insertions(+) create mode 100644 scripts/analyze-image-colors.ts create mode 100644 scripts/check-photo-colors.ts create mode 100644 scripts/find-image-colors.ts create mode 100755 scripts/reanalyze-colors.ts diff --git a/scripts/analyze-image-colors.ts b/scripts/analyze-image-colors.ts new file mode 100644 index 0000000..3d47187 --- /dev/null +++ b/scripts/analyze-image-colors.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env tsx + +import { PrismaClient } from '@prisma/client' +import { selectBestDominantColor, isGreyColor, analyzeColor } from '../src/lib/server/color-utils' + +const prisma = new PrismaClient() + +async function analyzeImage(filename: string) { + try { + // Find the image by filename + const media = await prisma.media.findFirst({ + where: { + filename: { + contains: filename + } + }, + select: { + id: true, + filename: true, + url: true, + dominantColor: true, + colors: true, + width: true, + height: true + } + }) + + if (!media) { + console.log(`Media not found with filename: ${filename}`) + return + } + + console.log('\n=== Image Analysis ===') + console.log(`Filename: ${media.filename}`) + console.log(`URL: ${media.url}`) + console.log(`Current dominant color: ${media.dominantColor}`) + console.log(`Dimensions: ${media.width}x${media.height}`) + + if (media.colors && Array.isArray(media.colors)) { + const colors = media.colors as Array<[string, number]> + + console.log('\n=== Color Distribution ===') + console.log('Top 15 colors:') + colors.slice(0, 15).forEach(([hex, percentage], index) => { + const isGrey = isGreyColor(hex) + console.log(`${index + 1}. ${hex} - ${percentage.toFixed(2)}%${isGrey ? ' (grey)' : ''}`) + }) + + console.log('\n=== Color Analysis Strategies ===') + + // Try different strategies + const strategies = { + 'Default (min 2%, prefer vibrant & bright)': selectBestDominantColor(colors, { + minPercentage: 2, + preferVibrant: true, + excludeGreys: false, + preferBrighter: true + }), + + 'Exclude greys, prefer bright': selectBestDominantColor(colors, { + minPercentage: 1, + preferVibrant: true, + excludeGreys: true, + preferBrighter: true + }), + + 'Very low threshold (0.5%), bright': selectBestDominantColor(colors, { + minPercentage: 0.5, + preferVibrant: true, + excludeGreys: false, + preferBrighter: true + }), + + 'Allow dark colors': selectBestDominantColor(colors, { + minPercentage: 1, + preferVibrant: true, + excludeGreys: false, + preferBrighter: false + }), + + 'Focus on prominence (5%)': selectBestDominantColor(colors, { + minPercentage: 5, + preferVibrant: false, + excludeGreys: false, + preferBrighter: true + }) + } + + Object.entries(strategies).forEach(([strategy, color]) => { + const analysis = analyzeColor(color) + console.log(`${strategy}: ${color} | V:${analysis.vibrance.toFixed(2)} B:${analysis.brightness.toFixed(2)}${analysis.isGrey ? ' (grey)' : ''}${analysis.isDark ? ' (dark)' : ''}`) + }) + + // Show non-grey colors + console.log('\n=== Non-Grey Colors ===') + const nonGreyColors = colors.filter(([hex]) => !isGreyColor(hex)) + console.log(`Found ${nonGreyColors.length} non-grey colors out of ${colors.length} total`) + + if (nonGreyColors.length > 0) { + console.log('\nTop 10 non-grey colors:') + nonGreyColors.slice(0, 10).forEach(([hex, percentage], index) => { + const analysis = analyzeColor(hex) + console.log(`${index + 1}. ${hex} - ${percentage.toFixed(2)}% | B:${analysis.brightness.toFixed(2)}`) + }) + + // Look for more vibrant colors deeper in the list + console.log('\n=== All Colors with >0.5% ===') + const significantColors = colors.filter(([_, pct]) => pct > 0.5) + significantColors.forEach(([hex, percentage]) => { + const isGrey = isGreyColor(hex) + // Convert hex to RGB to analyze + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const saturation = max === 0 ? 0 : (max - min) / max * 100 + + console.log(`${hex} - ${percentage.toFixed(2)}% | Sat: ${saturation.toFixed(0)}%${isGrey ? ' (grey)' : ''}`) + }) + } + } else { + console.log('\nNo color data available for this image') + } + + } catch (error) { + console.error('Error:', error) + } finally { + await prisma.$disconnect() + } +} + +// Get filename from command line argument +const filename = process.argv[2] || 'B0000295.jpg' +analyzeImage(filename) \ No newline at end of file diff --git a/scripts/check-photo-colors.ts b/scripts/check-photo-colors.ts new file mode 100644 index 0000000..646a33d --- /dev/null +++ b/scripts/check-photo-colors.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env tsx + +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +async function checkPhotoColors() { + try { + // Count total photography media + const totalPhotos = await prisma.media.count({ + where: { isPhotography: true } + }) + + // Count photos with dominant color + const photosWithColor = await prisma.media.count({ + where: { + isPhotography: true, + dominantColor: { not: null } + } + }) + + // Count photos without dominant color + const photosWithoutColor = await prisma.media.count({ + where: { + isPhotography: true, + dominantColor: null + } + }) + + // Get some examples + const examples = await prisma.media.findMany({ + where: { + isPhotography: true, + dominantColor: { not: null } + }, + select: { + filename: true, + dominantColor: true, + thumbnailUrl: true + }, + take: 5 + }) + + console.log('=== Photography Color Analysis ===') + console.log(`Total photography items: ${totalPhotos}`) + console.log(`With dominant color: ${photosWithColor} (${((photosWithColor/totalPhotos)*100).toFixed(1)}%)`) + console.log(`Without dominant color: ${photosWithoutColor} (${((photosWithoutColor/totalPhotos)*100).toFixed(1)}%)`) + + if (examples.length > 0) { + console.log('\n=== Examples with dominant colors ===') + examples.forEach(media => { + console.log(`${media.filename}: ${media.dominantColor}`) + }) + } + + } catch (error) { + console.error('Error:', error) + } finally { + await prisma.$disconnect() + } +} + +checkPhotoColors() \ No newline at end of file diff --git a/scripts/find-image-colors.ts b/scripts/find-image-colors.ts new file mode 100644 index 0000000..7135414 --- /dev/null +++ b/scripts/find-image-colors.ts @@ -0,0 +1,89 @@ +import { prisma } from '../src/lib/server/database' + +async function findImageColors() { + try { + console.log('Searching for image with filename: B0000295.jpg\n') + + // Search in Photo table + console.log('Checking Photo table...') + const photo = await prisma.photo.findFirst({ + where: { + filename: 'B0000295.jpg' + }, + select: { + id: true, + filename: true, + dominantColor: true, + colors: true, + url: true, + thumbnailUrl: true, + width: true, + height: true, + aspectRatio: true + } + }) + + if (photo) { + console.log('Found in Photo table:') + console.log('ID:', photo.id) + console.log('Filename:', photo.filename) + console.log('URL:', photo.url) + console.log('Dominant Color:', photo.dominantColor || 'Not set') + console.log('Colors:', photo.colors ? JSON.stringify(photo.colors, null, 2) : 'Not set') + console.log('Dimensions:', photo.width ? `${photo.width}x${photo.height}` : 'Not set') + console.log('Aspect Ratio:', photo.aspectRatio || 'Not set') + } else { + console.log('Not found in Photo table.') + } + + // Search in Media table + console.log('\nChecking Media table...') + const media = await prisma.media.findFirst({ + where: { + filename: 'B0000295.jpg' + }, + select: { + id: true, + filename: true, + originalName: true, + dominantColor: true, + colors: true, + url: true, + thumbnailUrl: true, + width: true, + height: true, + aspectRatio: true, + mimeType: true, + size: true + } + }) + + if (media) { + console.log('Found in Media table:') + console.log('ID:', media.id) + console.log('Filename:', media.filename) + console.log('Original Name:', media.originalName || 'Not set') + console.log('URL:', media.url) + console.log('Dominant Color:', media.dominantColor || 'Not set') + console.log('Colors:', media.colors ? JSON.stringify(media.colors, null, 2) : 'Not set') + console.log('Dimensions:', media.width ? `${media.width}x${media.height}` : 'Not set') + console.log('Aspect Ratio:', media.aspectRatio || 'Not set') + console.log('MIME Type:', media.mimeType) + console.log('Size:', media.size, 'bytes') + } else { + console.log('Not found in Media table.') + } + + if (!photo && !media) { + console.log('\nImage B0000295.jpg not found in either Photo or Media tables.') + } + + } catch (error) { + console.error('Error searching for image:', error) + } finally { + await prisma.$disconnect() + } +} + +// Run the script +findImageColors() \ No newline at end of file diff --git a/scripts/reanalyze-colors.ts b/scripts/reanalyze-colors.ts new file mode 100755 index 0000000..dc4ce05 --- /dev/null +++ b/scripts/reanalyze-colors.ts @@ -0,0 +1,167 @@ +#!/usr/bin/env tsx + +/** + * Script to reanalyze colors for specific images or all images + * Usage: tsx scripts/reanalyze-colors.ts [options] + * + * Options: + * --id Reanalyze specific media ID + * --grey-only Only reanalyze images with grey dominant colors + * --all Reanalyze all images with color data + * --dry-run Show what would be changed without updating + */ + +import { PrismaClient } from '@prisma/client' +import { selectBestDominantColor, isGreyColor } from '../src/lib/server/color-utils' + +const prisma = new PrismaClient() + +interface Options { + id?: number + greyOnly: boolean + all: boolean + dryRun: boolean +} + +function parseArgs(): Options { + const args = process.argv.slice(2) + const options: Options = { + greyOnly: false, + all: false, + dryRun: false + } + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--id': + options.id = parseInt(args[++i]) + break + case '--grey-only': + options.greyOnly = true + break + case '--all': + options.all = true + break + case '--dry-run': + options.dryRun = true + break + } + } + + return options +} + +async function reanalyzeColors(options: Options) { + try { + // Build query + const where: any = { + colors: { not: null } + } + + if (options.id) { + where.id = options.id + } else if (options.greyOnly) { + // We'll filter in code since Prisma doesn't support function calls in where + } + + // Get media items + const mediaItems = await prisma.media.findMany({ + where, + select: { + id: true, + filename: true, + dominantColor: true, + colors: true + } + }) + + console.log(`Found ${mediaItems.length} media items with color data`) + + let updated = 0 + let skipped = 0 + + for (const media of mediaItems) { + if (!media.colors || !Array.isArray(media.colors)) { + skipped++ + continue + } + + const currentColor = media.dominantColor + const colors = media.colors as Array<[string, number]> + + // Skip if grey-only filter and current color isn't grey + if (options.greyOnly && currentColor && !isGreyColor(currentColor)) { + skipped++ + continue + } + + // Calculate new dominant color + const newColor = selectBestDominantColor(colors, { + minPercentage: 2, + preferVibrant: true, + excludeGreys: false + }) + + if (newColor !== currentColor) { + console.log(`\n${media.filename}:`) + console.log(` Current: ${currentColor || 'none'}`) + console.log(` New: ${newColor}`) + + // Show color breakdown + const topColors = colors.slice(0, 5) + console.log(' Top colors:') + topColors.forEach(([hex, percentage]) => { + const isGrey = isGreyColor(hex) + console.log(` ${hex} - ${percentage.toFixed(1)}%${isGrey ? ' (grey)' : ''}`) + }) + + if (!options.dryRun) { + // Update media + await prisma.media.update({ + where: { id: media.id }, + data: { dominantColor: newColor } + }) + + // Update related photos + await prisma.photo.updateMany({ + where: { mediaId: media.id }, + data: { dominantColor: newColor } + }) + + updated++ + } + } else { + skipped++ + } + } + + console.log(`\nāœ“ Complete!`) + console.log(` Updated: ${updated}`) + console.log(` Skipped: ${skipped}`) + if (options.dryRun) { + console.log(` (Dry run - no changes made)`) + } + + } catch (error) { + console.error('Error:', error) + process.exit(1) + } finally { + await prisma.$disconnect() + } +} + +// Run the script +const options = parseArgs() + +if (!options.id && !options.all && !options.greyOnly) { + console.log('Usage: tsx scripts/reanalyze-colors.ts [options]') + console.log('') + console.log('Options:') + console.log(' --id Reanalyze specific media ID') + console.log(' --grey-only Only reanalyze images with grey dominant colors') + console.log(' --all Reanalyze all images with color data') + console.log(' --dry-run Show what would be changed without updating') + process.exit(1) +} + +reanalyzeColors(options) \ No newline at end of file