Integrate Inputs/PainterPalette.csv for artist metadata and influence links, add db/schema.sql with server/migrate.js, letterbox influence panel thumbnails, and refresh Titian/Pontormo images. Co-authored-by: Cursor <cursoragent@cursor.com>
280 lines
8.5 KiB
JavaScript
280 lines
8.5 KiB
JavaScript
/**
|
||
* 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,
|
||
};
|