| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | export async function loadEmbeddings() { |
| | console.log('🔄 Chargement des embeddings CLIP...'); |
| |
|
| | try { |
| | const response = await fetch('/data/embeddings.json'); |
| | if (!response.ok) { |
| | throw new Error(`HTTP Error: ${response.status}`); |
| | } |
| |
|
| | const data = await response.json(); |
| |
|
| | console.log(`✅ ${data.fonts.length} polices chargées`); |
| |
|
| | return data; |
| | } catch (error) { |
| | console.error('❌ Erreur lors du chargement des embeddings:', error); |
| | throw error; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | export function extractFusionPrefix(fontId, fontData) { |
| | const parts = fontId.split('-'); |
| | if (parts.length <= 1) { |
| | return fontId; |
| | } |
| |
|
| | |
| | if (fontData && fontData.subsets && Array.isArray(fontData.subsets)) { |
| | const commonSubsets = ['latin', 'latin-ext', 'cyrillic', 'cyrillic-ext', 'greek', 'greek-ext']; |
| | for (const subset of fontData.subsets) { |
| | if (!commonSubsets.includes(subset) && fontId.includes(subset)) { |
| | const baseName = fontId.replace(`-${subset}`, '').replace(subset, ''); |
| | if (baseName && baseName !== fontId) { |
| | 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]; |
| | } |
| |
|
| | |
| | |
| | |
| | export function mergeFontFamilies(fontDataList, embeddingMatrices, enableFusion = true) { |
| | if (!enableFusion) { |
| | return { fontDataList, embeddingMatrices }; |
| | } |
| |
|
| | const prefixGroups = {}; |
| | const prefixEmbeddingGroups = {}; |
| |
|
| | |
| | for (let i = 0; i < fontDataList.length; i++) { |
| | const font = fontDataList[i]; |
| | const prefix = extractFusionPrefix(font.id, font); |
| |
|
| | if (!prefixGroups[prefix]) { |
| | prefixGroups[prefix] = []; |
| | prefixEmbeddingGroups[prefix] = []; |
| | } |
| |
|
| | prefixGroups[prefix].push(font); |
| | prefixEmbeddingGroups[prefix].push(embeddingMatrices[i]); |
| | } |
| |
|
| | const mergedFonts = []; |
| | const mergedEmbeddings = []; |
| |
|
| | |
| | for (const [prefix, fonts] of Object.entries(prefixGroups)) { |
| | if (fonts.length > 1) { |
| | let representativeFont = fonts[0]; |
| |
|
| | |
| | const representatives = { |
| | 'noto': 'noto-sans-arabic', |
| | 'noto-serif': 'noto-serif-latin', |
| | 'ibm-plex': 'ibm-plex-sans', |
| | 'baloo': 'baloo-2' |
| | }; |
| |
|
| | if (representatives[prefix]) { |
| | const found = fonts.find(f => f.id === representatives[prefix]); |
| | if (found) representativeFont = found; |
| | } |
| |
|
| | const representativeIndex = fonts.findIndex(f => f.id === representativeFont.id); |
| | const representativeEmbedding = prefixEmbeddingGroups[prefix][representativeIndex]; |
| |
|
| | const mergedFont = { |
| | ...representativeFont, |
| | id: prefix, |
| | name: prefix.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), |
| | imageName: representativeFont.id |
| | }; |
| |
|
| | mergedFonts.push(mergedFont); |
| | mergedEmbeddings.push(representativeEmbedding); |
| | } else { |
| | mergedFonts.push({ ...fonts[0], imageName: fonts[0].id }); |
| | mergedEmbeddings.push(prefixEmbeddingGroups[prefix][0]); |
| | } |
| | } |
| |
|
| | return { |
| | fontDataList: mergedFonts, |
| | embeddingMatrices: mergedEmbeddings |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | export function normalizeData(data) { |
| | const rows = data.length; |
| | const cols = data[0].length; |
| |
|
| | const means = new Array(cols).fill(0); |
| | const stds = new Array(cols).fill(0); |
| |
|
| | for (let i = 0; i < rows; i++) { |
| | for (let j = 0; j < cols; j++) { |
| | means[j] += data[i][j]; |
| | } |
| | } |
| | for (let j = 0; j < cols; j++) { |
| | means[j] /= rows; |
| | } |
| |
|
| | for (let i = 0; i < rows; i++) { |
| | for (let j = 0; j < cols; j++) { |
| | const diff = data[i][j] - means[j]; |
| | stds[j] += diff * diff; |
| | } |
| | } |
| | for (let j = 0; j < cols; j++) { |
| | stds[j] = Math.sqrt(stds[j] / rows); |
| | if (stds[j] === 0) stds[j] = 1; |
| | } |
| |
|
| | const normalized = data.map(row => |
| | row.map((val, j) => (val - means[j]) / stds[j]) |
| | ); |
| |
|
| | return normalized; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function applyPCA(data, nComponents = 50) { |
| | const rows = data.length; |
| | const cols = data[0].length; |
| | const target = Math.min(nComponents, cols, rows); |
| |
|
| | |
| | const means = new Array(cols).fill(0); |
| | for (let i = 0; i < rows; i++) { |
| | for (let j = 0; j < cols; j++) means[j] += data[i][j]; |
| | } |
| | for (let j = 0; j < cols; j++) means[j] /= rows; |
| |
|
| | const centered = data.map(row => row.map((v, j) => v - means[j])); |
| |
|
| | |
| | |
| | if (rows < cols) { |
| | |
| | const gram = Array.from({ length: rows }, () => new Float64Array(rows)); |
| | for (let i = 0; i < rows; i++) { |
| | for (let j = i; j < rows; j++) { |
| | let dot = 0; |
| | for (let k = 0; k < cols; k++) dot += centered[i][k] * centered[j][k]; |
| | gram[i][j] = dot / (rows - 1); |
| | gram[j][i] = gram[i][j]; |
| | } |
| | } |
| |
|
| | |
| | const eigenvectors = []; |
| | const eigenvalues = []; |
| | const gramCopy = gram.map(row => Float64Array.from(row)); |
| |
|
| | for (let comp = 0; comp < target; comp++) { |
| | let vec = new Float64Array(rows); |
| | for (let i = 0; i < rows; i++) vec[i] = Math.random() - 0.5; |
| |
|
| | for (let iter = 0; iter < 100; iter++) { |
| | const newVec = new Float64Array(rows); |
| | for (let i = 0; i < rows; i++) { |
| | let sum = 0; |
| | for (let j = 0; j < rows; j++) sum += gramCopy[i][j] * vec[j]; |
| | newVec[i] = sum; |
| | } |
| |
|
| | let norm = 0; |
| | for (let i = 0; i < rows; i++) norm += newVec[i] * newVec[i]; |
| | norm = Math.sqrt(norm); |
| | if (norm === 0) break; |
| | for (let i = 0; i < rows; i++) newVec[i] /= norm; |
| |
|
| | let diff = 0; |
| | for (let i = 0; i < rows; i++) diff += (newVec[i] - vec[i]) ** 2; |
| | vec = newVec; |
| | if (diff < 1e-10) break; |
| | } |
| |
|
| | let eigenvalue = 0; |
| | const Av = new Float64Array(rows); |
| | for (let i = 0; i < rows; i++) { |
| | let sum = 0; |
| | for (let j = 0; j < rows; j++) sum += gramCopy[i][j] * vec[j]; |
| | Av[i] = sum; |
| | } |
| | for (let i = 0; i < rows; i++) eigenvalue += vec[i] * Av[i]; |
| |
|
| | eigenvalues.push(eigenvalue); |
| | eigenvectors.push(vec); |
| |
|
| | |
| | for (let i = 0; i < rows; i++) { |
| | for (let j = 0; j < rows; j++) { |
| | gramCopy[i][j] -= eigenvalue * vec[i] * vec[j]; |
| | } |
| | } |
| | } |
| |
|
| | |
| | const result = Array.from({ length: rows }, () => new Array(target)); |
| | for (let comp = 0; comp < target; comp++) { |
| | for (let i = 0; i < rows; i++) { |
| | result[i][comp] = eigenvectors[comp][i] * Math.sqrt(Math.max(0, eigenvalues[comp]) * (rows - 1)); |
| | } |
| | } |
| |
|
| | const totalVar = eigenvalues.reduce((s, v) => s + Math.max(0, v), 0) || 1; |
| | const explainedVar = eigenvalues.slice(0, target).reduce((s, v) => s + Math.max(0, v), 0); |
| | console.log(`📐 PCA: ${cols}D → ${target}D (${(explainedVar / totalVar * 100).toFixed(1)}% variance)`); |
| |
|
| | return result; |
| | } |
| |
|
| | |
| | |
| | console.log(`📐 PCA: using standard covariance path (${cols}D → ${target}D)`); |
| | return centered.map(row => row.slice(0, target)); |
| | } |
| |
|