|
|
#!/usr/bin/env node |
|
|
|
|
|
import fs from 'fs/promises'; |
|
|
import path from 'path'; |
|
|
import { fileURLToPath } from 'url'; |
|
|
import sharp from 'sharp'; |
|
|
import { UMAP } from 'umap-js'; |
|
|
import { Matrix } from 'ml-matrix'; |
|
|
import cliProgress from 'cli-progress'; |
|
|
import chalk from 'chalk'; |
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = path.dirname(__filename); |
|
|
|
|
|
|
|
|
const PNGS_DIR = path.join(__dirname, 'output', 'pngs'); |
|
|
const DATA_DIR = path.join(__dirname, 'output', 'data'); |
|
|
const FONT_INDEX_PATH = path.join(__dirname, 'input', 'font-index.json'); |
|
|
const OUTPUT_FILENAME = 'typography_data.json'; |
|
|
const FULL_OUTPUT_PATH = path.join(DATA_DIR, OUTPUT_FILENAME); |
|
|
|
|
|
|
|
|
const UMAP_PARAMS = { |
|
|
nComponents: 2, |
|
|
nNeighbors: 15, |
|
|
minDist: 1.0, |
|
|
metric: 'euclidean', |
|
|
random: Math.random |
|
|
}; |
|
|
|
|
|
|
|
|
const CATEGORY_WEIGHT = 0.3; |
|
|
const PIXEL_WEIGHT = 0.7; |
|
|
|
|
|
|
|
|
const ENABLE_FONT_FUSION = true; |
|
|
const FUSION_PREFIX_LENGTH = 2; |
|
|
|
|
|
|
|
|
const progressBar = new cliProgress.SingleBar({ |
|
|
format: chalk.cyan('{bar}') + ' | {percentage}% | {value}/{total} | {fontName}', |
|
|
barCompleteChar: '\u2588', |
|
|
barIncompleteChar: '\u2591', |
|
|
hideCursor: true |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function analyzePNGQuality(pngPath) { |
|
|
try { |
|
|
|
|
|
const image = sharp(pngPath); |
|
|
const { data, info } = await image.grayscale().raw().toBuffer({ resolveWithObject: true }); |
|
|
|
|
|
const { width, height } = info; |
|
|
const totalPixels = width * height; |
|
|
|
|
|
let blackPixels = 0; |
|
|
let whitePixels = 0; |
|
|
let grayPixels = 0; |
|
|
|
|
|
|
|
|
for (let i = 0; i < data.length; i++) { |
|
|
const pixel = data[i]; |
|
|
|
|
|
if (pixel < 50) { |
|
|
blackPixels++; |
|
|
} else if (pixel > 200) { |
|
|
whitePixels++; |
|
|
} else { |
|
|
grayPixels++; |
|
|
} |
|
|
} |
|
|
|
|
|
const blackPercentage = (blackPixels / totalPixels) * 100; |
|
|
const whitePercentage = (whitePixels / totalPixels) * 100; |
|
|
const grayPercentage = (grayPixels / totalPixels) * 100; |
|
|
|
|
|
|
|
|
const hasContent = blackPixels > 0; |
|
|
const isMostlyWhite = whitePercentage > 90; |
|
|
const hasReasonableContent = 0.1 <= blackPercentage && blackPercentage <= 50; |
|
|
|
|
|
return { |
|
|
totalPixels, |
|
|
blackPixels, |
|
|
whitePixels, |
|
|
grayPixels, |
|
|
blackPercentage, |
|
|
whitePercentage, |
|
|
grayPercentage, |
|
|
hasContent, |
|
|
isMostlyWhite, |
|
|
hasReasonableContent, |
|
|
qualityScore: hasReasonableContent ? blackPercentage : 0 |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`❌ Error during l'analyse de ${pngPath}:`, error.message); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadPNGAsMatrix(pngPath) { |
|
|
try { |
|
|
|
|
|
const image = sharp(pngPath); |
|
|
const { data, info } = await image.grayscale().raw().toBuffer({ resolveWithObject: true }); |
|
|
|
|
|
|
|
|
if (info.width !== 40 || info.height !== 40) { |
|
|
console.warn(`⚠️ Taille inattendue pour ${pngPath}: ${info.width}x${info.height}`); |
|
|
|
|
|
const resized = await sharp(pngPath) |
|
|
.resize(40, 40) |
|
|
.grayscale() |
|
|
.raw() |
|
|
.toBuffer({ resolveWithObject: true }); |
|
|
data = resized.data; |
|
|
} |
|
|
|
|
|
|
|
|
const quality = await analyzePNGQuality(pngPath); |
|
|
if (!quality) { |
|
|
return { pixelVector: null, quality: null }; |
|
|
} |
|
|
|
|
|
|
|
|
if (!quality.hasReasonableContent) { |
|
|
console.warn(`⚠️ PNG de mauvaise qualité: ${pngPath} (${quality.blackPercentage.toFixed(1)}% noir)`); |
|
|
return { pixelVector: null, quality: null }; |
|
|
} |
|
|
|
|
|
|
|
|
const pixelMatrix = Array.from(data).map(pixel => pixel / 255.0); |
|
|
|
|
|
return { pixelVector: pixelMatrix, quality }; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`❌ Erreur lors du chargement de ${pngPath}:`, error.message); |
|
|
return { pixelVector: null, quality: null }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function extractFusionPrefix(fontId, fontData, maxDashes = FUSION_PREFIX_LENGTH) { |
|
|
const parts = fontId.split('-'); |
|
|
if (parts.length <= 1) { |
|
|
return fontId; |
|
|
} |
|
|
|
|
|
|
|
|
if (fontData && fontData.subsets && Array.isArray(fontData.subsets)) { |
|
|
|
|
|
for (const subset of fontData.subsets) { |
|
|
|
|
|
if (['latin', 'latin-ext', 'cyrillic', 'cyrillic-ext', 'greek', 'greek-ext'].includes(subset)) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
if (fontId.includes(subset)) { |
|
|
|
|
|
const baseName = fontId.replace(`-${subset}`, '').replace(subset, ''); |
|
|
if (baseName && baseName !== fontId) { |
|
|
console.log(chalk.yellow(` 🔍 ${fontId} → ${baseName} (subset: ${subset})`)); |
|
|
return baseName; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const specialCases = { |
|
|
'baloo': ['baloo-2', 'baloo-bhai-2', 'baloo-bhaijaan-2', 'baloo-bhaina-2', 'baloo-chettan-2', 'baloo-da-2', 'baloo-paaji-2', 'baloo-tamma-2', 'baloo-tammudu-2', 'baloo-thambi-2'], |
|
|
'ibm-plex': ['ibm-plex'], |
|
|
'playwrite': ['playwrite'] |
|
|
}; |
|
|
|
|
|
|
|
|
for (const [familyPrefix, patterns] of Object.entries(specialCases)) { |
|
|
for (const pattern of patterns) { |
|
|
if (fontId.startsWith(pattern)) { |
|
|
return familyPrefix; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (fontId.startsWith('noto-serif-')) { |
|
|
return 'noto-serif'; |
|
|
} |
|
|
if (fontId.startsWith('noto-')) { |
|
|
return 'noto'; |
|
|
} |
|
|
|
|
|
|
|
|
const secondWord = parts[1]; |
|
|
if (secondWord === 'sans' || secondWord === 'serif' || secondWord === 'plex') { |
|
|
return parts.slice(0, 2).join('-'); |
|
|
} |
|
|
|
|
|
|
|
|
return parts[0]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function mergeFontFamilies(fontDataList, pixelMatrices) { |
|
|
if (!ENABLE_FONT_FUSION) { |
|
|
return { fontDataList, pixelMatrices }; |
|
|
} |
|
|
|
|
|
console.log(chalk.blue('🔄 Fusion des familles de polices...')); |
|
|
|
|
|
const prefixGroups = {}; |
|
|
const prefixPixelGroups = {}; |
|
|
|
|
|
|
|
|
for (let i = 0; i < fontDataList.length; i++) { |
|
|
const font = fontDataList[i]; |
|
|
const prefix = extractFusionPrefix(font.id, font); |
|
|
|
|
|
if (!prefixGroups[prefix]) { |
|
|
prefixGroups[prefix] = []; |
|
|
prefixPixelGroups[prefix] = []; |
|
|
} |
|
|
|
|
|
prefixGroups[prefix].push(font); |
|
|
prefixPixelGroups[prefix].push(pixelMatrices[i]); |
|
|
} |
|
|
|
|
|
|
|
|
const mergedFonts = []; |
|
|
const mergedPixels = []; |
|
|
let fusionCount = 0; |
|
|
let totalReduction = 0; |
|
|
|
|
|
for (const [prefix, fonts] of Object.entries(prefixGroups)) { |
|
|
if (fonts.length > 1) { |
|
|
|
|
|
const allPixels = prefixPixelGroups[prefix]; |
|
|
|
|
|
|
|
|
let representativePixels; |
|
|
let representativeFont; |
|
|
|
|
|
|
|
|
if (prefix === 'noto') { |
|
|
|
|
|
representativeFont = fonts.find(f => f.id === 'noto-sans-arabic') || |
|
|
fonts.find(f => f.id === 'noto-sans-latin') || |
|
|
fonts.find(f => f.id === 'noto-sans') || |
|
|
fonts[0]; |
|
|
} else if (prefix === 'noto-serif') { |
|
|
|
|
|
representativeFont = fonts.find(f => f.id === 'noto-serif-latin') || |
|
|
fonts.find(f => f.id === 'noto-serif') || |
|
|
fonts[0]; |
|
|
} else if (prefix === 'ibm-plex') { |
|
|
|
|
|
representativeFont = fonts.find(f => f.id === 'ibm-plex-sans') || |
|
|
fonts.find(f => f.id === 'ibm-plex') || |
|
|
fonts[0]; |
|
|
} else if (prefix === 'baloo') { |
|
|
|
|
|
representativeFont = fonts.find(f => f.id === 'baloo-2') || fonts[0]; |
|
|
} else { |
|
|
|
|
|
representativeFont = fonts.find(f => f.id.includes('-regular') || f.id.includes('-normal')) || |
|
|
fonts.find(f => !f.id.includes('-italic') && !f.id.includes('-bold')) || |
|
|
fonts[0]; |
|
|
} |
|
|
|
|
|
|
|
|
const representativeIndex = fonts.findIndex(f => f.id === representativeFont.id); |
|
|
representativePixels = allPixels[representativeIndex]; |
|
|
|
|
|
|
|
|
const allWeights = [...new Set(fonts.flatMap(f => f.weights || []))].sort((a, b) => a - b); |
|
|
const allStyles = [...new Set(fonts.flatMap(f => f.styles || []))].sort(); |
|
|
const allSubsets = [...new Set(fonts.flatMap(f => f.subsets || []))].sort(); |
|
|
|
|
|
|
|
|
const mergedFont = { |
|
|
...representativeFont, |
|
|
id: prefix, |
|
|
name: prefix.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), |
|
|
|
|
|
imageName: representativeFont.id, |
|
|
weights: allWeights, |
|
|
styles: allStyles, |
|
|
subsets: allSubsets, |
|
|
originalVariants: fonts.map(f => ({ |
|
|
id: f.id, |
|
|
name: f.name, |
|
|
google_fonts_url: f.google_fonts_url, |
|
|
weights: f.weights || [], |
|
|
styles: f.styles || [] |
|
|
})), |
|
|
variantCount: fonts.length, |
|
|
fusionInfo: { |
|
|
merged: true, |
|
|
originalCount: fonts.length, |
|
|
representative: representativeFont.id, |
|
|
representativeName: representativeFont.name, |
|
|
variants: fonts.map(f => f.id), |
|
|
selectionMethod: prefix === 'noto' ? 'noto-sans-latin-priority' : |
|
|
prefix === 'noto-serif' ? 'noto-serif-latin-priority' : |
|
|
prefix === 'ibm-plex' ? 'ibm-plex-sans-priority' : |
|
|
prefix === 'baloo' ? 'baloo-2-priority' : 'standard-variant-priority' |
|
|
} |
|
|
}; |
|
|
|
|
|
mergedFonts.push(mergedFont); |
|
|
mergedPixels.push(representativePixels); |
|
|
|
|
|
fusionCount++; |
|
|
totalReduction += fonts.length - 1; |
|
|
|
|
|
console.log(chalk.green(` ✓ ${prefix}: ${fonts.length} variantes → 1 famille`)); |
|
|
} else { |
|
|
|
|
|
const singleFont = { |
|
|
...fonts[0], |
|
|
imageName: fonts[0].id |
|
|
}; |
|
|
mergedFonts.push(singleFont); |
|
|
mergedPixels.push(prefixPixelGroups[prefix][0]); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(chalk.green(`✅ Fusion terminée: ${fusionCount} familles fusionnées, ${totalReduction} polices supprimées`)); |
|
|
console.log(chalk.cyan(`📊 Résultat: ${fontDataList.length} → ${mergedFonts.length} polices (${((totalReduction / fontDataList.length) * 100).toFixed(1)}% de réduction)`)); |
|
|
|
|
|
return { |
|
|
fontDataList: mergedFonts, |
|
|
pixelMatrices: mergedPixels |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function extractFontInfoFromFilename(filename, fontIndexData) { |
|
|
|
|
|
const fontId = filename.replace('.png', '').replace('_a', ''); |
|
|
|
|
|
|
|
|
const fontData = fontIndexData[fontId]; |
|
|
|
|
|
if (!fontData) { |
|
|
console.warn(`⚠️ Police non trouvée dans l'index: ${fontId}`); |
|
|
|
|
|
const fontName = fontId.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); |
|
|
const googleFontsUrl = `https://fonts.google.com/specimen/${fontName.replace(/\s+/g, '+')}`; |
|
|
|
|
|
return { |
|
|
name: fontName, |
|
|
id: fontId, |
|
|
imageName: fontId, |
|
|
family: "sans-serif", |
|
|
google_fonts_url: googleFontsUrl |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
const fontName = fontId; |
|
|
const category = fontData.category; |
|
|
const googleFontsUrl = `https://fonts.google.com/specimen/${fontData.family.replace(/\s+/g, '+')}`; |
|
|
|
|
|
return { |
|
|
name: fontName, |
|
|
id: fontId, |
|
|
imageName: fontId, |
|
|
family: category, |
|
|
google_fonts_url: googleFontsUrl, |
|
|
|
|
|
weights: fontData.weights || [], |
|
|
styles: fontData.styles || [], |
|
|
subsets: fontData.subsets || [], |
|
|
unicodeRange: fontData.unicodeRange || {} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeData(data) { |
|
|
const matrix = new Matrix(data); |
|
|
const means = matrix.mean('column'); |
|
|
const stds = matrix.standardDeviation('column'); |
|
|
|
|
|
|
|
|
for (let i = 0; i < stds.length; i++) { |
|
|
if (stds[i] === 0) { |
|
|
stds[i] = 1; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const normalized = matrix.clone(); |
|
|
for (let i = 0; i < normalized.rows; i++) { |
|
|
for (let j = 0; j < normalized.columns; j++) { |
|
|
normalized.set(i, j, (normalized.get(i, j) - means[j]) / stds[j]); |
|
|
} |
|
|
} |
|
|
|
|
|
return normalized.to2DArray(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function encodeCategories(fontDataList) { |
|
|
console.log(chalk.blue('🔢 Encoding categories to numerical vectors...')); |
|
|
|
|
|
|
|
|
const categories = [...new Set(fontDataList.map(font => font.family))]; |
|
|
console.log(chalk.cyan(`📊 Found ${categories.length} unique categories: ${categories.join(', ')}`)); |
|
|
|
|
|
|
|
|
const categoryToIndex = {}; |
|
|
categories.forEach((category, index) => { |
|
|
categoryToIndex[category] = index; |
|
|
}); |
|
|
|
|
|
|
|
|
const encodedCategories = fontDataList.map(font => { |
|
|
const vector = new Array(categories.length).fill(0); |
|
|
const categoryIndex = categoryToIndex[font.family]; |
|
|
vector[categoryIndex] = 1; |
|
|
return vector; |
|
|
}); |
|
|
|
|
|
console.log(chalk.green(`✅ Encoded ${encodedCategories.length} fonts with ${categories.length} category dimensions`)); |
|
|
|
|
|
return { |
|
|
encodedCategories, |
|
|
categories, |
|
|
categoryToIndex |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function combinePixelAndCategoryData(pixelMatrices, encodedCategories) { |
|
|
console.log(chalk.blue('🔗 Combining pixel data with category encodings...')); |
|
|
|
|
|
const combinedData = []; |
|
|
|
|
|
for (let i = 0; i < pixelMatrices.length; i++) { |
|
|
const pixelVector = pixelMatrices[i]; |
|
|
const categoryVector = encodedCategories[i]; |
|
|
|
|
|
|
|
|
const normalizedPixels = pixelVector.map(pixel => pixel / 255); |
|
|
|
|
|
|
|
|
const combinedVector = [ |
|
|
...normalizedPixels.map(p => p * PIXEL_WEIGHT), |
|
|
...categoryVector.map(c => c * CATEGORY_WEIGHT) |
|
|
]; |
|
|
|
|
|
combinedData.push(combinedVector); |
|
|
} |
|
|
|
|
|
console.log(chalk.green(`✅ Combined data: ${pixelMatrices[0].length} pixels + ${encodedCategories[0].length} categories = ${combinedData[0].length} total dimensions`)); |
|
|
console.log(chalk.cyan(`📊 Weights: ${(PIXEL_WEIGHT * 100).toFixed(0)}% pixels, ${(CATEGORY_WEIGHT * 100).toFixed(0)}% categories`)); |
|
|
|
|
|
return combinedData; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAllFontData() { |
|
|
console.log(chalk.blue('🔄 Chargement des données de polices depuis les PNGs...')); |
|
|
|
|
|
|
|
|
await fs.mkdir(DATA_DIR, { recursive: true }); |
|
|
|
|
|
|
|
|
console.log(chalk.yellow('📖 Chargement de l\'index des polices...')); |
|
|
const fontIndexData = JSON.parse(await fs.readFile(FONT_INDEX_PATH, 'utf8')); |
|
|
console.log(chalk.green(`✅ Index chargé: ${Object.keys(fontIndexData).length} polices`)); |
|
|
|
|
|
|
|
|
const files = await fs.readdir(PNGS_DIR); |
|
|
const pngFiles = files.filter(file => file.endsWith('_a.png')); |
|
|
|
|
|
if (pngFiles.length === 0) { |
|
|
throw new Error(`Aucun fichier PNG trouvé dans ${PNGS_DIR}`); |
|
|
} |
|
|
|
|
|
console.log(chalk.cyan(`📁 ${pngFiles.length} fichiers PNG trouvés`)); |
|
|
|
|
|
const fontDataList = []; |
|
|
const pixelMatrices = []; |
|
|
let rejectedCount = 0; |
|
|
|
|
|
|
|
|
for (let i = 0; i < pngFiles.length; i++) { |
|
|
const filename = pngFiles[i]; |
|
|
const pngPath = path.join(PNGS_DIR, filename); |
|
|
|
|
|
|
|
|
const fontInfo = extractFontInfoFromFilename(filename, fontIndexData); |
|
|
|
|
|
|
|
|
const { pixelVector, quality } = await loadPNGAsMatrix(pngPath); |
|
|
|
|
|
if (pixelVector && quality) { |
|
|
fontDataList.push(fontInfo); |
|
|
pixelMatrices.push(pixelVector); |
|
|
|
|
|
|
|
|
fontInfo.quality = quality; |
|
|
|
|
|
if ((i + 1) % 50 === 0) { |
|
|
console.log(chalk.yellow(`⚡ ${i + 1}/${pngFiles.length} polices traitées...`)); |
|
|
} |
|
|
} else { |
|
|
rejectedCount++; |
|
|
console.log(chalk.red(`❌ Rejeté: ${filename} (qualité insuffisante)`)); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(chalk.green(`✅ ${fontDataList.length} polices chargées avec succès`)); |
|
|
console.log(chalk.red(`❌ ${rejectedCount} polices rejetées pour problèmes de qualité`)); |
|
|
|
|
|
console.log(chalk.blue(`📊 Matrice finale: ${pixelMatrices.length} polices × ${pixelMatrices[0]?.length || 0} pixels`)); |
|
|
|
|
|
return { fontDataList, pixelMatrices }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateUMAPEmbedding(pixelMatrices) { |
|
|
console.log(chalk.blue('🔄 Génération des embeddings UMAP...')); |
|
|
|
|
|
|
|
|
console.log(chalk.yellow('📊 Normalisation des données...')); |
|
|
const normalizedData = normalizeData(pixelMatrices); |
|
|
|
|
|
|
|
|
console.log(chalk.cyan(`🗺️ Application d'UMAP avec paramètres:`, UMAP_PARAMS)); |
|
|
const umap = new UMAP(UMAP_PARAMS); |
|
|
const embedding = umap.fit(normalizedData); |
|
|
|
|
|
console.log(chalk.green(`✅ UMAP terminé - Forme de l'embedding: ${embedding.length} × ${embedding[0]?.length || 0}`)); |
|
|
|
|
|
if (embedding.length > 0) { |
|
|
const xValues = embedding.map(row => row[0]); |
|
|
const yValues = embedding.map(row => row[1]); |
|
|
console.log(chalk.blue(`📊 Plage X: [${Math.min(...xValues).toFixed(2)}, ${Math.max(...xValues).toFixed(2)}]`)); |
|
|
console.log(chalk.blue(`📊 Plage Y: [${Math.min(...yValues).toFixed(2)}, ${Math.max(...yValues).toFixed(2)}]`)); |
|
|
} |
|
|
|
|
|
return embedding; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function saveTypographyData(fontDataList, embedding, categories) { |
|
|
console.log(chalk.blue('💾 Sauvegarde des données...')); |
|
|
|
|
|
|
|
|
const finalData = []; |
|
|
for (let i = 0; i < fontDataList.length; i++) { |
|
|
const fontInfo = fontDataList[i]; |
|
|
|
|
|
const fontData = { |
|
|
...fontInfo, |
|
|
x: embedding[i][0], |
|
|
y: embedding[i][1] |
|
|
}; |
|
|
finalData.push(fontData); |
|
|
} |
|
|
|
|
|
|
|
|
const metadata = { |
|
|
generated_at: new Date().toISOString(), |
|
|
method: "umap_from_png_pixels_with_category_influence_and_font_fusion", |
|
|
total_fonts: finalData.length, |
|
|
font_fusion: { |
|
|
enabled: ENABLE_FONT_FUSION, |
|
|
prefix_length: FUSION_PREFIX_LENGTH, |
|
|
fusion_method: "average_pixels_with_metadata_aggregation" |
|
|
}, |
|
|
umap_params: UMAP_PARAMS, |
|
|
category_weights: { |
|
|
pixel_weight: PIXEL_WEIGHT, |
|
|
category_weight: CATEGORY_WEIGHT |
|
|
}, |
|
|
categories: categories, |
|
|
category_count: categories.length, |
|
|
data_source: "PNG pixel matrices (40x40) + category encoding + font family fusion" |
|
|
}; |
|
|
|
|
|
|
|
|
const outputData = { |
|
|
metadata, |
|
|
fonts: finalData |
|
|
}; |
|
|
|
|
|
|
|
|
await fs.writeFile(FULL_OUTPUT_PATH, JSON.stringify(outputData, null, 2), 'utf8'); |
|
|
|
|
|
console.log(chalk.green(`✅ Données sauvegardées dans ${FULL_OUTPUT_PATH}`)); |
|
|
|
|
|
|
|
|
const categoryStats = {}; |
|
|
for (const font of finalData) { |
|
|
const cat = font.family; |
|
|
categoryStats[cat] = (categoryStats[cat] || 0) + 1; |
|
|
} |
|
|
|
|
|
console.log(chalk.cyan('\n📊 Distribution par catégorie:')); |
|
|
for (const [cat, count] of Object.entries(categoryStats).sort(([,a], [,b]) => b - a)) { |
|
|
const percentage = ((count / finalData.length) * 100).toFixed(1); |
|
|
console.log(chalk.white(` ${cat}: ${count} polices (${percentage}%)`)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function main() { |
|
|
try { |
|
|
console.log(chalk.blue.bold('🎨 Génération UMAP pour la typographie à partir des matrices de pixels\n')); |
|
|
|
|
|
|
|
|
const { fontDataList, pixelMatrices } = await loadAllFontData(); |
|
|
|
|
|
if (fontDataList.length === 0) { |
|
|
throw new Error('Aucune donnée de police valide chargée'); |
|
|
} |
|
|
|
|
|
|
|
|
const { fontDataList: mergedFontDataList, pixelMatrices: mergedPixelMatrices } = mergeFontFamilies(fontDataList, pixelMatrices); |
|
|
|
|
|
|
|
|
const { encodedCategories, categories, categoryToIndex } = encodeCategories(mergedFontDataList); |
|
|
|
|
|
|
|
|
const combinedData = combinePixelAndCategoryData(mergedPixelMatrices, encodedCategories); |
|
|
|
|
|
|
|
|
const normalizedData = normalizeData(combinedData); |
|
|
|
|
|
|
|
|
const embedding = generateUMAPEmbedding(normalizedData); |
|
|
|
|
|
|
|
|
await saveTypographyData(mergedFontDataList, embedding, categories); |
|
|
|
|
|
console.log(chalk.green.bold('\n🎉 Génération UMAP terminée avec succès !')); |
|
|
|
|
|
} catch (error) { |
|
|
console.error(chalk.red('💥 Erreur fatale:'), error.message); |
|
|
process.exit(1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
main(); |
|
|
|