File size: 5,462 Bytes
33f66e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | // 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);
};
})();
|