feat: add real-time collaborative shopping list at /cospend/list
All checks were successful
CI / update (push) Successful in 1m18s
All checks were successful
CI / update (push) Successful in 1m18s
Real-time shopping list with SSE sync between multiple clients, automatic item categorization using embedding-based classification + Bring icon matching, and card-based UI with category grouping. - SSE broadcast for live sync (add/check/remove items across tabs) - Hybrid categorizer: direct catalog lookup → category-scoped embedding search → per-category default icons, with DB caching - 388 Bring catalog icons matched via multilingual-e5-base embeddings - 170+ English→German icon aliases for reliable cross-language matching - Move cospend dashboard to /cospend/dash, /cospend redirects to list - Shopping icon on homepage links to /cospend/list
This commit is contained in:
55
scripts/embed-shopping-icons.ts
Normal file
55
scripts/embed-shopping-icons.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Pre-compute embeddings for Bring! catalog items to enable icon matching.
|
||||
* Maps item names to their icon filenames via semantic similarity.
|
||||
*
|
||||
* Run: pnpm exec vite-node scripts/embed-shopping-icons.ts
|
||||
*/
|
||||
import { pipeline } from '@huggingface/transformers';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const MODEL_NAME = 'Xenova/multilingual-e5-base';
|
||||
const CATALOG_PATH = resolve('static/shopping-icons/catalog.json');
|
||||
const OUTPUT_FILE = resolve('src/lib/data/shoppingIconEmbeddings.json');
|
||||
|
||||
async function main() {
|
||||
const catalog: Record<string, string> = JSON.parse(readFileSync(CATALOG_PATH, 'utf-8'));
|
||||
|
||||
// Deduplicate: multiple display names can map to the same icon
|
||||
// We want one embedding per unique display name
|
||||
const uniqueItems = new Map<string, string>();
|
||||
for (const [name, iconFile] of Object.entries(catalog)) {
|
||||
uniqueItems.set(name, iconFile);
|
||||
}
|
||||
|
||||
const items = [...uniqueItems.entries()];
|
||||
console.log(`Loading model ${MODEL_NAME}...`);
|
||||
const embedder = await pipeline('feature-extraction', MODEL_NAME, { dtype: 'q8' });
|
||||
|
||||
console.log(`Embedding ${items.length} catalog items...`);
|
||||
const entries: { name: string; icon: string; vector: number[] }[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const [name, icon] = items[i];
|
||||
const result = await embedder(`passage: ${name}`, { pooling: 'mean', normalize: true });
|
||||
const vector = Array.from(result.data as Float32Array).map(v => Math.round(v * 10000) / 10000);
|
||||
entries.push({ name, icon, vector });
|
||||
|
||||
if ((i + 1) % 50 === 0) {
|
||||
console.log(` ${i + 1}/${items.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
const output = {
|
||||
model: MODEL_NAME,
|
||||
dimensions: entries[0]?.vector.length || 768,
|
||||
count: entries.length,
|
||||
entries,
|
||||
};
|
||||
|
||||
const json = JSON.stringify(output);
|
||||
writeFileSync(OUTPUT_FILE, json, 'utf-8');
|
||||
console.log(`Written ${OUTPUT_FILE} (${(json.length / 1024).toFixed(1)}KB, ${entries.length} entries)`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user