| |
| |
| (() => { |
| 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) { |
| 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); |
| }; |
| })(); |
|
|