| | |
| | import { UMAP } from 'umap-js'; |
| | import { loadEmbeddings, mergeFontFamilies, normalizeData, applyPCA } from '../utils/umapCalculator'; |
| |
|
| | |
| | let cachedData = null; |
| |
|
| | |
| | let cachedKNN = { |
| | nNeighbors: null, |
| | enableFontFusion: null, |
| | indices: null, |
| | distances: null |
| | }; |
| |
|
| | self.onmessage = async (e) => { |
| | const { type, payload } = e.data; |
| |
|
| | if (type === 'CALCULATE') { |
| | await calculateUMAP(payload); |
| | } |
| | }; |
| |
|
| | async function calculateUMAP(config) { |
| | const { |
| | nNeighbors = 15, |
| | minDist = 1.0, |
| | enableFontFusion = true, |
| | randomSeed = 42 |
| | } = config; |
| |
|
| | try { |
| | |
| | if (!cachedData) { |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'loading', progress: 0 } }); |
| | cachedData = await loadEmbeddings(); |
| | } |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'preparing', progress: 20 } }); |
| |
|
| | |
| | const fontDataList = cachedData.fonts.map(font => ({ |
| | id: font.id, |
| | name: font.name, |
| | imageName: font.imageName || font.id, |
| | family: font.family, |
| | google_fonts_url: font.google_fonts_url, |
| | weights: font.weights, |
| | styles: font.styles, |
| | subsets: font.subsets, |
| | unicodeRange: font.unicodeRange |
| | })); |
| |
|
| | const embeddingMatrices = cachedData.fonts.map(font => font.embedding); |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'merging', progress: 40 } }); |
| | const { fontDataList: mergedFonts, embeddingMatrices: mergedEmbeddings } = |
| | mergeFontFamilies(fontDataList, embeddingMatrices, enableFontFusion); |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'normalizing', progress: 45 } }); |
| | const normalizedData = normalizeData(mergedEmbeddings); |
| |
|
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'pca', progress: 52 } }); |
| | const pcaData = applyPCA(normalizedData, 50); |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'umap', progress: 60 } }); |
| |
|
| | |
| | let seed = randomSeed; |
| | const randomFn = () => { |
| | seed = (seed * 9301 + 49297) % 233280; |
| | return seed / 233280; |
| | }; |
| |
|
| | const umapParams = { |
| | nComponents: 2, |
| | nNeighbors, |
| | minDist, |
| | metric: 'cosine', |
| | random: randomFn |
| | }; |
| |
|
| | const umap = new UMAP(umapParams); |
| |
|
| | console.log('🚀 Starting UMAP calculation...'); |
| | const t1 = performance.now(); |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'umap_knn', progress: 65 } }); |
| |
|
| | |
| | |
| | |
| | const canReuseKNN = cachedKNN.indices && |
| | cachedKNN.nNeighbors === nNeighbors && |
| | cachedKNN.enableFontFusion === enableFontFusion; |
| |
|
| | if (canReuseKNN) { |
| | console.log('♻️ Reusing cached KNN results'); |
| | umap.setPrecomputedKNN(cachedKNN.indices, cachedKNN.distances); |
| | umap.initializeFit(pcaData); |
| | } else { |
| | console.log('🆕 Calculating new KNN'); |
| | umap.initializeFit(pcaData); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | if (umap.knnIndices && umap.knnDistances) { |
| | cachedKNN = { |
| | nNeighbors, |
| | enableFontFusion, |
| | indices: umap.knnIndices, |
| | distances: umap.knnDistances |
| | }; |
| | } |
| | } |
| |
|
| | const t2 = performance.now(); |
| | console.log(`⏱️ KNN & Init took: ${(t2 - t1).toFixed(2)}ms ${canReuseKNN ? '(Cached)' : ''}`); |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'umap_optimize', progress: 70 } }); |
| | const t3 = performance.now(); |
| | umap.optimizeLayout(); |
| | const t4 = performance.now(); |
| | console.log(`⏱️ Optimization took: ${(t4 - t3).toFixed(2)}ms`); |
| | console.log(`⏱️ Total UMAP took: ${(t4 - t1).toFixed(2)}ms`); |
| |
|
| | const embedding = umap.getEmbedding(); |
| |
|
| | |
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'finalizing', progress: 90 } }); |
| |
|
| | const finalData = mergedFonts.map((font, i) => ({ |
| | ...font, |
| | x: embedding[i][0], |
| | y: embedding[i][1] |
| | })); |
| |
|
| | const result = { |
| | config: { |
| | nNeighbors, |
| | minDist, |
| | metric: 'cosine', |
| | enableFontFusion, |
| | testName: `live-n${nNeighbors}-d${minDist}`, |
| | randomSeed |
| | }, |
| | metadata: { |
| | generated_at: new Date().toISOString(), |
| | total_fonts: finalData.length, |
| | method: "umap_from_clip_embeddings_pure_visual_frontend", |
| | note: "100% visual embeddings, calculated in browser worker" |
| | }, |
| | fonts: finalData |
| | }; |
| |
|
| | self.postMessage({ type: 'PROGRESS', payload: { stage: 'complete', progress: 100 } }); |
| | self.postMessage({ type: 'RESULT', payload: result }); |
| |
|
| | } catch (error) { |
| | self.postMessage({ |
| | type: 'ERROR', |
| | payload: error.message || 'Erreur inconnue dans le worker' |
| | }); |
| | } |
| | } |
| |
|