flickpick / model.js
usmar's picture
Upload folder using huggingface_hub
33f66e0 verified
Raw
History Blame Contribute Delete
5.46 kB
// Client-side model runtime: intercepts the app's fetch() calls and answers
// them from item factors shipped with the page. Generated by export_static.py.
(() => {
const ALPHA = 20.0, REG = 0.1, F = 96;
const PERSONAS = [{"name": "Sci-fi night", "titles": ["Matrix, The", "Star Wars: Episode IV - A New Hope", "Terminator 2: Judgment Day", "Alien", "Blade Runner"]}, {"name": "Rom-com classics", "titles": ["Sleepless in Seattle", "You've Got Mail", "Pretty Woman", "Four Weddings and a Funeral", "Notting Hill"]}, {"name": "Animated favorites", "titles": ["Toy Story", "Bug's Life, A", "Aladdin", "Lion King, The", "Beauty and the Beast"]}, {"name": "Horror night", "titles": ["Shining, The", "Halloween", "Exorcist, The", "Scream", "Psycho"]}];
let catalog = null, V = null, VtV = null, unit = null, byId = null;
async function load() {
if (catalog) return;
catalog = await (await realFetch("catalog.json")).json();
const buf = await (await realFetch("factors.bin")).arrayBuffer();
V = new Float32Array(buf);
byId = new Map(catalog.map((m, i) => [m.movie_id, i]));
VtV = new Float64Array(F * F);
const n = catalog.length;
for (let i = 0; i < n; i++) {
const o = i * F;
for (let a = 0; a < F; a++) {
const va = V[o + a];
for (let b = a; b < F; b++) VtV[a * F + b] += va * V[o + b];
}
}
for (let a = 0; a < F; a++) for (let b = 0; b < a; b++) VtV[a * F + b] = VtV[b * F + a];
unit = new Float32Array(V.length);
for (let i = 0; i < n; i++) {
const o = i * F;
let s = 0;
for (let a = 0; a < F; a++) s += V[o + a] * V[o + a];
const inv = s > 0 ? 1 / Math.sqrt(s) : 0;
for (let a = 0; a < F; a++) unit[o + a] = V[o + a] * inv;
}
}
function solve(A, b) { // Gaussian elimination with partial pivoting
const n = b.length, x = Float64Array.from(b), M = Float64Array.from(A);
for (let c = 0; c < n; c++) {
let p = c;
for (let r = c + 1; r < n; r++) if (Math.abs(M[r * n + c]) > Math.abs(M[p * n + c])) p = r;
if (p !== c) {
for (let k = c; k < n; k++) { const t = M[c * n + k]; M[c * n + k] = M[p * n + k]; M[p * n + k] = t; }
const t = x[c]; x[c] = x[p]; x[p] = t;
}
const piv = M[c * n + c];
for (let r = c + 1; r < n; r++) {
const f = M[r * n + c] / piv;
if (f === 0) continue;
for (let k = c; k < n; k++) M[r * n + k] -= f * M[c * n + k];
x[r] -= f * x[c];
}
}
for (let r = n - 1; r >= 0; r--) {
let s = x[r];
for (let k = r + 1; k < n; k++) s -= M[r * n + k] * x[k];
x[r] = s / M[r * n + r];
}
return x;
}
const movie = i => {
const m = catalog[i];
return { movie_id: m.movie_id, title: m.title, year: m.year, genres: m.genres };
};
function recommend(movieIds, topN) {
const idx = [...new Set(movieIds.map(id => byId.get(id)))].filter(i => i !== undefined);
const A = Float64Array.from(VtV), b = new Float64Array(F);
for (const i of idx) {
const o = i * F;
for (let a = 0; a < F; a++) {
b[a] += (1 + ALPHA) * V[o + a];
for (let c = 0; c < F; c++) A[a * F + c] += ALPHA * V[o + a] * V[o + c];
}
}
for (let a = 0; a < F; a++) A[a * F + a] += REG;
const u = solve(A, b);
const n = catalog.length, scores = new Float64Array(n);
for (let i = 0; i < n; i++) {
const o = i * F;
let s = 0;
for (let a = 0; a < F; a++) s += V[o + a] * u[a];
scores[i] = s;
}
const excluded = new Set(idx);
const order = [...scores.keys()].filter(i => !excluded.has(i))
.sort((x, y) => scores[y] - scores[x]).slice(0, topN);
const maxScore = Math.max(scores[order[0]], 1e-9);
const recs = order.map((i, r) => {
let best = idx[0], bestSim = -2;
for (const p of idx) {
let s = 0;
for (let a = 0; a < F; a++) s += unit[i * F + a] * unit[p * F + a];
if (s > bestSim) { bestSim = s; best = p; }
}
return { ...movie(i), rank: r + 1, score: Math.round(scores[i] * 1e4) / 1e4,
match: Math.round(scores[i] / maxScore * 1e4) / 1e4, because: catalog[best].title };
});
return { picks: idx.map(movie), recommendations: recs };
}
const respond = data => Promise.resolve({ ok: true, status: 200, json: async () => data });
const realFetch = window.fetch.bind(window);
window.fetch = async (url, opts) => {
const u = String(url);
if (u.startsWith("search?")) {
await load();
const q = new URLSearchParams(u.split("?")[1]).get("q").trim().toLowerCase();
if (q.length < 2) return respond([]);
const hits = catalog.filter(m => m.title.toLowerCase().includes(q))
.sort((a, b) => b.n_likes - a.n_likes).slice(0, 20)
.map(m => movie(byId.get(m.movie_id)));
return respond(hits);
}
if (u.startsWith("sample?")) {
await load();
const i = parseInt(new URLSearchParams(u.split("?")[1]).get("index") || "0", 10);
const p = PERSONAS[i % PERSONAS.length];
return respond({ name: p.name,
movies: p.titles.filter(t => catalog.some(m => m.title === t))
.map(t => movie(byId.get(catalog.find(m => m.title === t).movie_id))) });
}
if (u === "recommend") {
await load();
const body = JSON.parse(opts.body);
return respond(recommend(body.movie_ids, body.n || 10));
}
return realFetch(url, opts);
};
})();