Files
Art-gallery/scripts/painter-palette-lib.js
T
Danila KhodjaefandCursor b2cae284ac Add PainterPalette integration, canonical DB schema, and influence UI fixes.
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>
2026-06-22 12:09:47 +03:00

280 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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,
};