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