/** * Shared PainterPalette.csv parsing, name matching, and influence cleaning. */ const fs = require('fs'); const path = require('path'); const CSV_PATH = path.join(__dirname, '../Inputs/PainterPalette.csv'); /** Gallery DB name → PainterPalette.csv artist column */ const DB_TO_PALETTE_NAME = { Bronzino: 'Agnolo Bronzino', Pontormo: 'Jacopo Pontormo', 'Ivan Klyun': 'Ivan Kliun', 'Henri Privat-Livemont': 'Henri Privat Livemont', Zeuxis: 'Zeuxis', Parrhasius: 'Parrhasius', 'Theophanes the Greek': 'Theophanes the Greek', 'J. M. W. Turner': 'J.M.W. Turner', 'Édouard Manet': 'Edouard Manet', 'Paul Cézanne': 'Paul Cezanne', 'Eugène Delacroix': 'Eugene Delacroix', 'François Boucher': 'Francois Boucher', 'Jean-Auguste-Dominique Ingres': 'Jean Auguste Dominique Ingres', 'Jean-Baptiste-Camille Corot': 'Jean-Baptiste-Camille Corot', 'Wassily Kandinsky': 'Wassily Kandinsky', 'Kazimir Malevich': 'Kazimir Malevich', 'Willem de Kooning': 'Willem de Kooning', 'René Magritte': 'Rene Magritte', }; /** PainterPalette movement/style label → gallery art_movements.name */ const MOVEMENT_ALIASES = new Map( Object.entries({ 'byzantine art': 'Byzantine', byzantine: 'Byzantine', impressionism: 'Impressionism', 'post-impressionism': 'Post-Impressionism', 'neo-impressionism': 'Post-Impressionism', cubism: 'Cubism', 'analytical cubism': 'Cubism', 'synthetic cubism': 'Cubism', surrealism: 'Surrealism', symbolism: 'Symbolism', fauvism: 'Fauvism', expressionism: 'Expressionism', 'abstract expressionism': 'Abstract Expressionism', futurism: 'Futurism', dada: 'Dada', constructivism: 'Constructivism', suprematism: 'Suprematism', 'pop art': 'Pop Art', realism: 'Realism', romanticism: 'Romanticism', baroque: 'Baroque', rococo: 'Rococo', neoclassicism: 'Neoclassicism', 'art nouveau': 'Art Nouveau', 'art nouveau modern': 'Art Nouveau', gothic: 'Gothic', 'high renaissance': 'High Renaissance', 'early renaissance': 'Early Renaissance', 'northern renaissance': 'Northern Renaissance', mannerism: 'Mannerism', 'mannerism late renaissance': 'Mannerism', 'ancient greek roman': 'Ancient Greek & Roman', 'ancient greek & roman': 'Ancient Greek & Roman', }) ); function parseCSVLine(line) { const out = []; let cur = ''; let q = false; for (let i = 0; i < line.length; i++) { const c = line[i]; if (c === '"') { q = !q; continue; } if (c === ',' && !q) { out.push(cur); cur = ''; continue; } cur += c; } out.push(cur); return out; } function normalizeName(name) { return (name || '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, ' ') .trim(); } function parseNameList(field) { if (!field || !String(field).trim()) return []; return String(field) .split(',') .map((s) => s.trim().replace(/^['"]|['"]$/g, '')) .filter(Boolean); } function isNoiseInfluenceName(name) { const n = name.trim(); if (!n || n.length < 2) return true; if (/\.(jpg|jpeg|png|gif)\b/i.test(n)) return true; if (/^Artists\d/i.test(n)) return true; if (/\b(museum|gallery|tretyakov|hermitage|louvre|national museum)\b/i.test(n)) return true; if (/^(famous-people|male-portraits|female-portraits|winter|gardens-and-parks|rivers-and)/i.test(n)) return true; if (/^[a-z0-9]+(-[a-z0-9]+){2,}$/.test(n) && !/\s/.test(n)) return true; if (/^\d+(\.\d+)?\s*x\s*\d+(\.\d+)?\s*cm$/i.test(n)) return true; if (/^[\d\s.x×cm]+$/i.test(n)) return true; if (/^[^\x00-\x7F]+$/.test(n) && n.length < 40) return true; if (n.length > 120) return true; return false; } function paletteRowToMetadata(row, idx) { const num = (v) => { if (v == null || v === '') return null; const n = Number(v); return Number.isFinite(n) ? n : null; }; return { source: 'PainterPalette.csv', nationality: row[idx.Nationality] || null, citizenship: row[idx.citizenship] || null, gender: row[idx.gender] || null, styles: row[idx.styles] || null, movement: row[idx.movement] || row[idx.ArtMovement] || null, art500k_movements: row[idx.Art500k_Movements] || null, birth_place: row[idx.birth_place] || null, death_place: row[idx.death_place] || null, birth_year: num(row[idx.birth_year]), death_year: num(row[idx.death_year]), first_year: num(row[idx.FirstYear]), last_year: num(row[idx.LastYear]), wikiart_pictures_count: num(row[idx.wikiart_pictures_count]), occupations: row[idx.occupations] || null, painting_school: row[idx.PaintingSchool] || null, styles_extended: row[idx.styles_extended] || null, influenced_by_raw: row[idx.Influencedby] || null, influenced_on_raw: row[idx.Influencedon] || null, teachers_raw: row[idx.Teachers] || null, pupils_raw: row[idx.Pupils] || null, }; } function loadPainterPalette() { const lines = fs.readFileSync(CSV_PATH, 'utf8').split(/\r?\n/).filter(Boolean); const hdr = parseCSVLine(lines[0]); const idx = Object.fromEntries(hdr.map((h, i) => [h, i])); const byNorm = new Map(); const byExact = new Map(); for (let i = 1; i < lines.length; i++) { const row = parseCSVLine(lines[i]); const artist = row[idx.artist]?.trim(); if (!artist) continue; const entry = { artist, row, idx, metadata: paletteRowToMetadata(row, idx) }; byExact.set(artist.toLowerCase(), entry); byNorm.set(normalizeName(artist), entry); } return { idx, byNorm, byExact, rowCount: lines.length - 1 }; } function findPaletteRow(dbName, palette) { const alias = DB_TO_PALETTE_NAME[dbName]; if (alias) { const hit = palette.byExact.get(alias.toLowerCase()); if (hit) return hit; } const exact = palette.byExact.get(dbName.toLowerCase()); if (exact) return exact; const norm = palette.byNorm.get(normalizeName(dbName)); if (norm) return norm; const dbNorm = normalizeName(dbName); const parts = dbNorm.split(' ').filter(Boolean); if (parts.length >= 2) { const last = parts[parts.length - 1]; const candidates = []; for (const [n, entry] of palette.byNorm) { if (n.endsWith(` ${last}`) || n === last) candidates.push(entry); } if (candidates.length === 1) return candidates[0]; } return null; } function buildArtistResolver(dbArtists) { const byNorm = new Map(); const byLast = new Map(); for (const a of dbArtists) { const norm = normalizeName(a.name); if (!byNorm.has(norm)) byNorm.set(norm, a); const parts = norm.split(' ').filter(Boolean); const last = parts[parts.length - 1]; if (!byLast.has(last)) byLast.set(last, []); byLast.get(last).push(a); } function resolve(rawName) { const name = rawName.trim(); if (!name || isNoiseInfluenceName(name)) return null; const alias = DB_TO_PALETTE_NAME[name] || name; const norm = normalizeName(alias); if (byNorm.has(norm)) return { type: 'artist', artist: byNorm.get(norm).name, id: byNorm.get(norm).id }; const parts = norm.split(' ').filter(Boolean); if (parts.length >= 2) { const last = parts[parts.length - 1]; const cands = (byLast.get(last) || []).filter((a) => { const an = normalizeName(a.name); return parts.every((p) => an.includes(p)); }); if (cands.length === 1) return { type: 'artist', artist: cands[0].name, id: cands[0].id }; } const movementKey = norm.replace(/\s+/g, ' '); if (MOVEMENT_ALIASES.has(movementKey)) { return { type: 'movement', movement: MOVEMENT_ALIASES.get(movementKey) }; } for (const [key, movement] of MOVEMENT_ALIASES) { if (movementKey === key || movementKey.startsWith(`${key} `)) { return { type: 'movement', movement }; } } return null; } return { resolve }; } function collectInfluenceSources(paletteEntry) { const { row, idx } = paletteEntry; const sources = []; const seen = new Set(); const addField = (field, role) => { for (const raw of parseNameList(row[idx[field]])) { const key = `${role}:${normalizeName(raw)}`; if (seen.has(key)) continue; seen.add(key); sources.push({ raw, role }); } }; addField('Teachers', 'teacher'); addField('Influencedby', 'influenced_by'); addField('Influencedon', 'influenced_on'); addField('Pupils', 'pupil'); return sources; } module.exports = { CSV_PATH, DB_TO_PALETTE_NAME, MOVEMENT_ALIASES, parseCSVLine, normalizeName, parseNameList, isNoiseInfluenceName, paletteRowToMetadata, loadPainterPalette, findPaletteRow, buildArtistResolver, collectInfluenceSources, };