Humuhumu33 commited on
Commit
5952d10
Β·
verified Β·
1 Parent(s): 38518f0

holo-evicted-publish: player +4 object(s)

Browse files
b/00b8cf7da4a06ec9ee95edda7ea2277e08218c7998cd4dd834cd88031be37d25 ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-library.mjs β€” THE PLANE: the ΞΊ-addressed universe catalog at runtime (HOLO-TV-LIBRARY-PROMPT.md L2).
2
+ //
3
+ // A lean resolver over the forge's shards: the tiny root (feed/library-index.json) names every shard by
4
+ // TWO hashes β€” blake3 (the ΞΊ-mirror path) and sha256 (the verify key). Reads are verify-or-refuse (Law
5
+ // L5): bytes are cached raw in CacheStorage and RE-HASHED on every read; a tampered shard is refused by
6
+ // name, never parsed. Once cached, the whole universe browses OFFLINE and every facet pivot is a plain
7
+ // in-memory scan over compact rows β€” ~19k titles filter in well under a frame, no bitset machinery needed.
8
+ //
9
+ // Dependency-injected (fetch + cache + sha256) so Node witnesses it with fake shards β€” no network.
10
+ // Browser binding: window.HoloLibrary.live() β†’ { load, query, search, person, personsIndex, rows, root }.
11
+
12
+ // createLibrary({ root, fetch, cacheGet, cachePut, sha256hex })
13
+ // root : the parsed library-index.json (or null β†’ loadRoot() fetches it)
14
+ // cacheGet : async (sha) β†’ Uint8Array | null cachePut: async (sha, bytes) β†’ void
15
+ // sha256hex : async (Uint8Array) β†’ hex
16
+ export function createLibrary({ root = null, fetch: f, cacheGet, cachePut, sha256hex, rootUrl = "feed/library-index.json" } = {}) {
17
+ const doFetch = f || (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
18
+ if (!doFetch) throw new Error("holo-library: fetch required");
19
+ let R = root, rows = [], byKey = new Map(), loaded = false, personsCache = null, searchIdx = null;
20
+ const stats = { shardsFromCache: 0, shardsFromNet: 0, refused: 0 };
21
+
22
+ async function loadRoot() {
23
+ if (R) return R;
24
+ // network-first (fresh universe), then HTTP cache, then the LAST GOOD root parked in the object cache β€”
25
+ // a returning user boots the whole library with the network gone (P4 offline-first).
26
+ try { R = await (await doFetch(rootUrl + "?v=" + Date.now(), { cache: "no-store" })).json(); }
27
+ catch { try { R = await (await doFetch(rootUrl)).json(); } catch { R = null; } }
28
+ if (R && cachePut) { try { await cachePut("root", new TextEncoder().encode(JSON.stringify(R))); } catch {} }
29
+ if (!R && cacheGet) { const c = await cacheGet("root"); if (c) R = JSON.parse(new TextDecoder().decode(c)); }
30
+ if (!R) throw new Error("holo-library: no root reachable (network + cache empty)");
31
+ return R;
32
+ }
33
+ // verify-or-refuse read of ONE named object { sha256, blake3 }
34
+ async function readObject(ref, name) {
35
+ if (cacheGet) {
36
+ const c = await cacheGet(ref.sha256);
37
+ if (c) {
38
+ if ((await sha256hex(c)) === ref.sha256) { stats.shardsFromCache++; return c; }
39
+ stats.refused++; console.warn("holo-library: REFUSED tampered cache object", name, ref.sha256.slice(0, 12));
40
+ }
41
+ }
42
+ const res = await doFetch(R.mirror + ref.blake3);
43
+ if (!res.ok) throw new Error("holo-library: mirror " + res.status + " for " + name);
44
+ const bytes = new Uint8Array(await res.arrayBuffer());
45
+ if ((await sha256hex(bytes)) !== ref.sha256) { stats.refused++; throw new Error("holo-library: REFUSED " + name + " β€” bytes do not re-derive"); }
46
+ stats.shardsFromNet++;
47
+ if (cachePut) await cachePut(ref.sha256, bytes);
48
+ return bytes;
49
+ }
50
+ const dec = (u8) => JSON.parse(new TextDecoder().decode(u8));
51
+
52
+ // load(onShard) β€” pull all shards (parallel), verify each, build the in-memory plane.
53
+ async function load(onShard = null) {
54
+ if (loaded) return rows;
55
+ await loadRoot();
56
+ const parts = await Promise.all(R.shards.map(async (ref, n) => {
57
+ const s = dec(await readObject(ref, "shard" + n));
58
+ if (onShard) try { onShard(n, s.rows.length); } catch {}
59
+ return s.rows;
60
+ }));
61
+ rows = parts.flat();
62
+ byKey = new Map(rows.map((r) => [r.k + r.i, r]));
63
+ loaded = true;
64
+ return rows;
65
+ }
66
+
67
+ // ── facet algebra β€” one scan, every dimension composable ────────────────────────────────────────────────
68
+ // q: { kind:"m"|"t", genres:[ids], moods:[keys], decades:[1980,…], runtimeMax, runtimeMin, ratingMin,
69
+ // votesMax (hidden gems), lang, person (id), exclude:Set(keys), limit, sort:"top"|"new"|"old"|"az" }
70
+ function query(q = {}) {
71
+ let list = rows;
72
+ if (q.person != null) { const p = personsCache && personsCache[q.person]; list = p ? p.t.map((k) => byKey.get(k)).filter(Boolean) : []; }
73
+ const out = [];
74
+ for (const r of list) {
75
+ if (q.kind && r.k !== q.kind) continue;
76
+ if (q.genres && q.genres.length && !q.genres.some((g) => r.g.includes(g))) continue;
77
+ if (q.moods && q.moods.length && !q.moods.some((m) => r.m.includes(m))) continue;
78
+ if (q.decades && q.decades.length && !q.decades.includes(r.dec)) continue;
79
+ if (q.runtimeMax && !(r.rt && r.rt <= q.runtimeMax)) continue;
80
+ if (q.runtimeMin && !(r.rt && r.rt >= q.runtimeMin)) continue;
81
+ if (q.ratingMin && !(r.r >= q.ratingMin)) continue;
82
+ if (q.votesMax && !(r.v <= q.votesMax)) continue;
83
+ if (q.lang && r.l !== q.lang) continue;
84
+ if (q.exclude && q.exclude.has(r.k + r.i)) continue;
85
+ out.push(r);
86
+ }
87
+ const score = (r) => r.r * Math.log10(1 + r.v); // quality Γ— confidence
88
+ if (q.sort === "az") out.sort((a, b) => a.t.localeCompare(b.t));
89
+ else if (q.sort === "new") out.sort((a, b) => b.y - a.y || score(b) - score(a));
90
+ else if (q.sort === "old") out.sort((a, b) => a.y - b.y || score(b) - score(a));
91
+ else out.sort((a, b) => score(b) - score(a));
92
+ return q.limit ? out.slice(0, q.limit) : out;
93
+ }
94
+
95
+ // search-as-you-type over the full index (lazy lowercase map; prefix beats substring)
96
+ function search(text, limit = 40) {
97
+ const t = String(text || "").toLowerCase().trim();
98
+ if (!t) return [];
99
+ if (!searchIdx) searchIdx = rows.map((r) => [r.t.toLowerCase(), r]);
100
+ const pre = [], sub = [];
101
+ for (const [lt, r] of searchIdx) {
102
+ if (lt.startsWith(t)) pre.push(r);
103
+ else if (lt.includes(t)) sub.push(r);
104
+ if (pre.length >= limit) break;
105
+ }
106
+ const sc = (r) => r.r * Math.log10(1 + r.v);
107
+ pre.sort((a, b) => sc(b) - sc(a)); sub.sort((a, b) => sc(b) - sc(a));
108
+ return [...pre, ...sub].slice(0, limit);
109
+ }
110
+
111
+ // persons (lazy) β€” person id β†’ { n: name, t: [rowKeys] }; enables the "tap a face β†’ filmography" pivot
112
+ async function personsIndex() {
113
+ if (personsCache) return personsCache;
114
+ await loadRoot();
115
+ personsCache = dec(await readObject(R.persons, "persons")).persons;
116
+ return personsCache;
117
+ }
118
+ async function person(id) { return (await personsIndex())[id] || null; }
119
+
120
+ return {
121
+ load, query, search, person, personsIndex, loadRoot,
122
+ get rows() { return rows; }, get root() { return R; }, get loaded() { return loaded; },
123
+ rowByKey: (k) => byKey.get(k) || null, stats: () => ({ ...stats }),
124
+ };
125
+ }
126
+
127
+ // ── browser binding β€” CacheStorage raw-bytes tier + SubtleCrypto verify ─────────────────────────────────
128
+ if (typeof window !== "undefined") {
129
+ const CACHE = "holo-library-objects"; // content-addressed: entries never change, only appear
130
+ const sha256hex = async (u8) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", u8))].map((b) => b.toString(16).padStart(2, "0")).join("");
131
+ const cacheGet = async (sha) => { try { const c = await caches.open(CACHE); const r = await c.match("https://holo.local/lib/" + sha); return r ? new Uint8Array(await r.arrayBuffer()) : null; } catch { return null; } };
132
+ const cachePut = async (sha, bytes) => { try { const c = await caches.open(CACHE); await c.put("https://holo.local/lib/" + sha, new Response(bytes)); } catch {} };
133
+ let one = null;
134
+ window.HoloLibrary = { createLibrary, live() { return one || (one = createLibrary({ sha256hex, cacheGet, cachePut })); } };
135
+ }
136
+
137
+ export default { createLibrary };
b/0546f3b90f9f5b7318967dabc5afed0247424f3b59fedb8746042fb0599c6f90 ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$comment": "Holo TV Library root β€” the ΞΊ-addressed universe catalog. Each shard/persons object lives on the ΞΊ-mirror at b/<blake3>; sha256 is the verify key (SubtleCrypto at runtime, verify-or-refuse). Rows are compact: i/k/t/y/dec/g[]/m[](moods)/r/v/p/b/rt/imdb/col/c3/dir. Mood taxonomy is a fixed rule table in holo-library-forge.mjs.",
3
+ "version": 2,
4
+ "thumbhash": true,
5
+ "moodTaxonomyVersion": 1,
6
+ "generatedAt": "2026-07-11",
7
+ "counts": {
8
+ "titles": 18712,
9
+ "movies": 14474,
10
+ "series": 4238,
11
+ "persons": 1856
12
+ },
13
+ "mirror": "https://huggingface.co/HOLOGRAMTECH/holo-messenger-shell/resolve/main/b/",
14
+ "moods": [
15
+ "feelgood",
16
+ "dark",
17
+ "mindbending",
18
+ "cozy",
19
+ "adrenaline",
20
+ "tearjerker",
21
+ "scary",
22
+ "romantic",
23
+ "epic",
24
+ "quirky",
25
+ "truestory",
26
+ "family"
27
+ ],
28
+ "shards": [
29
+ {
30
+ "sha256": "d2e261229323a13aeb96964794e53b8666dbe0d7ff74767fec3001dfa7527852",
31
+ "blake3": "a92629dbd613fa4d9f5beffdc129b988e59c1de4134512709a05660391e7fea4",
32
+ "bytes": 318860
33
+ },
34
+ {
35
+ "sha256": "a38f1234acbeec00c823ee9c31eaa5b51d4d840fddae11bb0644b58d858dd881",
36
+ "blake3": "16ecc4e9c94e1c19661e2618b5ed11bbaa4b5b250366f9643fe388d6693a0078",
37
+ "bytes": 325376
38
+ },
39
+ {
40
+ "sha256": "b9a56739fdb6c7945d7dca6f6c942b56ba0d91f4339dbee6628d1e632dbdfdb3",
41
+ "blake3": "577d031f65706f04ca4e53501bba6c33ab18c8c4f15fb6033b03e6335ed3e33d",
42
+ "bytes": 311092
43
+ },
44
+ {
45
+ "sha256": "032e2dbfe0cb95cd5111a676c9e732255796b374ab1551332c386f1f01cd52a2",
46
+ "blake3": "20e7fc59b9c3e09f06db096b748d908c2ad2704ef5ea9a874c20e5e97f244ca7",
47
+ "bytes": 320466
48
+ },
49
+ {
50
+ "sha256": "f76bfbc163a1067c6d95cc350723a316b8d12859dbb92f35eee1ab6b380caa49",
51
+ "blake3": "07d695bf2d9d9cd2306f7b35cf398a02bded8824bb7a242ee47f1f045a81b998",
52
+ "bytes": 307474
53
+ },
54
+ {
55
+ "sha256": "2feb5fa70aee133edc0bfac0c778f022e92c5f234bd2cda299ba9586447a395b",
56
+ "blake3": "70897235fb9455bc9fb526cb96fb67734395c626be4654ad9e4d57a73d918e99",
57
+ "bytes": 323573
58
+ },
59
+ {
60
+ "sha256": "9e09e6580ce873f8f3eba3d0876b0cda13111d6fa132f70a63f23317c9c9d279",
61
+ "blake3": "afa09ba7128ecee94541eefe4bd45c4f3c8e509ddb4d65be4996682124806105",
62
+ "bytes": 316769
63
+ },
64
+ {
65
+ "sha256": "56b7382403180510cdce4324b4cd43fc9b55ffda7253e09e8c42c2ea4e819554",
66
+ "blake3": "70febd6a953f13218655baaad9ac5b79e6fec3650481555204b9d3c1ef787659",
67
+ "bytes": 297402
68
+ },
69
+ {
70
+ "sha256": "1d76736fc7d920d4bfcfb546c2cdf159369033ce1145400787caef7aff6d3b72",
71
+ "blake3": "eecd2cfa43d503a1fa0f8360abf1dee47c46289d68782920a122b7422410bd58",
72
+ "bytes": 303760
73
+ },
74
+ {
75
+ "sha256": "007be2d4e0b8cb3d1f24252f3ad26252574f9d369cab2c400091768dbe384edb",
76
+ "blake3": "c7b58078fd43b8b87cc21b553a91481d99eb396b4e8e753ef626bbc1fd0667ca",
77
+ "bytes": 309154
78
+ },
79
+ {
80
+ "sha256": "162976700a78c3579bbb2ba8f779457ee62682b01f981c87fd2160fcb00b6cb8",
81
+ "blake3": "620a8d3cdf0fbccc7107512e02543390fafe47ae8a95c3a2a97e6196da5271c3",
82
+ "bytes": 310759
83
+ },
84
+ {
85
+ "sha256": "f78c0b2b516d01a556e24339e6cf9f476ad8e787fa62f27136645b18937257ed",
86
+ "blake3": "27953cfa655cccd43477b9929932772d80af8f0ad7898ffac87a9bf162b60f09",
87
+ "bytes": 328843
88
+ },
89
+ {
90
+ "sha256": "a228cf9548613e6e1c0d474cee5ba89762fe3b9ce920b889e234ff2c748cd319",
91
+ "blake3": "83b144dcc6fcfbcd91f4970b7dcbce2b9d29ff3cee266b7b3007958dbb922e40",
92
+ "bytes": 332231
93
+ },
94
+ {
95
+ "sha256": "ebe97488b2244dff262fc776483b98bee10e9cdce0ee4132c1992e4880e53344",
96
+ "blake3": "46c77e4286f2c148681cefe4d57d1e3c9ba4d602a44c82b0800798c559cdf133",
97
+ "bytes": 304097
98
+ },
99
+ {
100
+ "sha256": "e095069d38d1a8985b4d6743a1550b5e18b23c1400642d2ef371d2855bc2f0cd",
101
+ "blake3": "007e6e4e9dd1ae84eb84a628fca3aba0a63628b3b278018062b5cdca9d2a10a9",
102
+ "bytes": 304596
103
+ },
104
+ {
105
+ "sha256": "8f05fbb5f221014512abff69dc51876255cc4950108f6fe77612a0fb8dec0dbe",
106
+ "blake3": "861b40c8f8f61aee3036deee1594745f810a5a55e23777ddc36131d0378c7508",
107
+ "bytes": 311049
108
+ }
109
+ ],
110
+ "persons": {
111
+ "sha256": "5d86ef6c1b5ec78c7e0ddd11a8d3ada0b24d6a879f2e7e5567b8a6dcb28a7531",
112
+ "blake3": "3c6d39d693bc0cb5e46d572037269e20b6379985350e4d72bb6fbfdf2831670e",
113
+ "bytes": 179005
114
+ }
115
+ }
b/1d14232cc5710fada7b165e6dae8f481b5bac754d82963b8c9f93d59a2a99354 ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ // holo-library-forge.mjs β€” THE FORGE: the movie/TV universe β†’ ΞΊ (HOLO-TV-LIBRARY-PROMPT.md L1).
3
+ //
4
+ // Sweeps TMDb /discover by year Γ— kind (vote floors keep it real titles, not noise), enriches the head
5
+ // of the popularity curve with one append_to_response call each (runtime Β· imdb Β· collection Β· top cast Β·
6
+ // director · keywords→moods), and mints the whole catalog as CONTENT-ADDRESSED shards:
7
+ //
8
+ // rows β†’ 16 shards (hash(id)%16), each a raw-JSON ΞΊ-object (sha256 of bytes = verify key,
9
+ // blake3 of bytes = the ΞΊ-mirror path b/<hex>)
10
+ // persons β†’ ONE inverse posting object (person β†’ their title ids; persons with β‰₯3 titles)
11
+ // root β†’ feed/library-index.json (tiny, published in the Q repo): shard ΞΊs + counts + taxonomy
12
+ //
13
+ // Deterministic: same input β†’ same rows β†’ same ΞΊs (rows sorted by id; JSON.stringify of plain objects
14
+ // with fixed key order; undefined fields OMITTED β€” the RFC-8785 lesson). Moods are a FIXED rule table
15
+ // (versioned below), never a model call. Resumable: every network page lands in a checkpoint file first.
16
+ //
17
+ // node holo-library-forge.mjs sweep + enrich + mint + UPLOAD + write root
18
+ // node holo-library-forge.mjs --dry sweep + mint locally, print ΞΊs + sizes, no upload/root
19
+ // node holo-library-forge.mjs --no-net re-mint from the checkpoint only (deterministic re-derive)
20
+
21
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
22
+ import { createHash } from "node:crypto";
23
+ import os from "node:os";
24
+ import path from "node:path";
25
+ import { fileURLToPath, pathToFileURL } from "node:url";
26
+
27
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
28
+ const HOLOGRAM = path.resolve(HERE, "../../..");
29
+ const { blake3hex } = await import(pathToFileURL(path.join(HOLOGRAM, "holo-os/system/os/usr/lib/holo/holo-blake3.mjs")).href);
30
+
31
+ const DRY = process.argv.includes("--dry");
32
+ const NO_NET = process.argv.includes("--no-net");
33
+ // --thumbhash <map.json> — P1 (INSTANT PLANE): merge key→holo-thumbhash (~26B) into each row as `h`,
34
+ // so the whole universe paints its own colours from the shards alone (see holo-thumbhash.mjs).
35
+ const THUMBS = (() => { const i = process.argv.indexOf("--thumbhash"); if (i < 0) return null; try { return JSON.parse(readFileSync(process.argv[i + 1], "utf8")); } catch { throw new Error("--thumbhash map unreadable"); } })();
36
+ const KEY = "45f791c5f6a6940a50403bb3890c7b86"; // the built-in community key (holo-tmdb.mjs)
37
+ const API = "https://api.themoviedb.org/3";
38
+ const CKPT = path.join(HERE, ".library-forge-checkpoint.json");
39
+ const SHARDS = 16;
40
+ const YEARS = { movie: [1940, new Date().getFullYear()], tv: [1950, new Date().getFullYear()] };
41
+ const FLOOR = { movie: 200, tv: 100 }; // vote_count floors (L0-probed: β‰ˆ19k titles)
42
+ const ENRICH_TOP = 5000; // head of the curve gets runtime/cast/keywords
43
+ const MIRROR = "https://huggingface.co/HOLOGRAMTECH/holo-messenger-shell/resolve/main/b/";
44
+ const HF_REPO = "HOLOGRAMTECH/holo-messenger-shell";
45
+
46
+ // ── mood taxonomy v1 β€” FIXED deterministic rules (genres βˆͺ keyword clusters); versioned, never a model ──
47
+ export const MOOD_TAXONOMY_VERSION = 1;
48
+ const KW = {
49
+ mindbending: ["time travel", "time loop", "parallel", "memory", "dream", "simulation", "plot twist", "identity", "surreal", "nonlinear"],
50
+ tearjerker: ["grief", "terminal illness", "cancer", "tragedy", "loss of", "death of", "holocaust", "euthanasia"],
51
+ quirky: ["surreal", "absurd", "black comedy", "dark comedy", "satire", "offbeat", "deadpan", "mockumentary"],
52
+ truestory: ["based on true story", "based on a true story", "biography", "based on real events", "true events", "biopic"],
53
+ };
54
+ const kwHit = (kws, list) => list.some((n) => kws.some((k) => k.includes(n)));
55
+ // g = tmdb genre ids (movie+tv unioned); rt minutes|0; r rating; v votes; kws lowercase keyword names
56
+ export function moodsOf({ g = [], rt = 0, r = 0, v = 0, kws = [] }) {
57
+ const has = (...ids) => ids.some((x) => g.includes(x));
58
+ const m = [];
59
+ if (has(35, 10402, 10751) && r >= 6.5) m.push("feelgood");
60
+ if (has(80, 53, 9648) && !has(10751, 16) ) m.push("dark");
61
+ if ((has(878, 10765) && has(9648)) || kwHit(kws, KW.mindbending)) m.push("mindbending");
62
+ if (has(10751, 16, 10749) && (!rt || rt <= 115) && r >= 6) m.push("cozy");
63
+ if (has(28, 53, 10759)) m.push("adrenaline");
64
+ if (has(18) && (kwHit(kws, KW.tearjerker) || (has(10749) && r >= 7.2))) m.push("tearjerker");
65
+ if (has(27)) m.push("scary");
66
+ if (has(10749)) m.push("romantic");
67
+ if (has(12, 10752, 36, 14, 10765, 10768) && ((rt >= 140) || v >= 5000)) m.push("epic");
68
+ if (has(35) && kwHit(kws, KW.quirky)) m.push("quirky");
69
+ if (kwHit(kws, KW.truestory) || has(36, 99)) m.push("truestory");
70
+ if (has(10751, 16, 10762)) m.push("family");
71
+ return m;
72
+ }
73
+ export const MOODS = ["feelgood", "dark", "mindbending", "cozy", "adrenaline", "tearjerker", "scary", "romantic", "epic", "quirky", "truestory", "family"];
74
+
75
+ // ── polite, resumable TMDb ───────────────────────────────────────────────────────────────────────────────
76
+ let inflight = 0, lastTick = 0;
77
+ async function tmdb(p, params = {}) {
78
+ const u = new URL(API + p); u.searchParams.set("api_key", KEY);
79
+ for (const [k, v] of Object.entries(params)) u.searchParams.set(k, String(v));
80
+ for (let attempt = 0; ; attempt++) {
81
+ const wait = Math.max(0, lastTick + 34 - Date.now()); // β‰ˆ29 req/s ceiling
82
+ lastTick = Math.max(Date.now(), lastTick + 34);
83
+ if (wait) await new Promise((r) => setTimeout(r, wait));
84
+ const res = await fetch(u).catch(() => null);
85
+ if (res && res.status === 429) { await new Promise((r) => setTimeout(r, 1500 * (attempt + 1))); continue; }
86
+ if (!res || !res.ok) { if (attempt >= 3) throw new Error("tmdb " + (res ? res.status : "net") + " " + p); await new Promise((r) => setTimeout(r, 800 * (attempt + 1))); continue; }
87
+ return res.json();
88
+ }
89
+ }
90
+
91
+ // ── the sweep (checkpointed) ─────────────────────────────────────────────────────────────────────────────
92
+ const ckpt = existsSync(CKPT) ? JSON.parse(readFileSync(CKPT, "utf8")) : { discover: {}, enrich: {} };
93
+ const saveCkpt = () => writeFileSync(CKPT, JSON.stringify(ckpt));
94
+
95
+ async function sweep() {
96
+ for (const kind of ["movie", "tv"]) {
97
+ const [y0, y1] = YEARS[kind];
98
+ for (let y = y0; y <= y1; y++) {
99
+ const ck = kind + ":" + y;
100
+ if (ckpt.discover[ck]) continue;
101
+ const yearParam = kind === "movie" ? { primary_release_year: y } : { first_air_date_year: y };
102
+ const base = { ...yearParam, "vote_count.gte": FLOOR[kind], sort_by: "vote_count.desc", include_adult: false };
103
+ const first = await tmdb(`/discover/${kind}`, { ...base, page: 1 });
104
+ const pages = Math.min(first.total_pages || 1, 500);
105
+ const rows = [...(first.results || [])];
106
+ for (let pg = 2; pg <= pages; pg++) rows.push(...((await tmdb(`/discover/${kind}`, { ...base, page: pg })).results || []));
107
+ ckpt.discover[ck] = rows.map((t) => ({
108
+ id: t.id, kind, title: kind === "movie" ? t.title : t.name,
109
+ year: parseInt((kind === "movie" ? t.release_date : t.first_air_date) || "0") || 0,
110
+ g: t.genre_ids || [], r: Math.round((t.vote_average || 0) * 10) / 10, v: t.vote_count || 0,
111
+ p: t.poster_path || null, b: t.backdrop_path || null, l: t.original_language || "",
112
+ }));
113
+ saveCkpt();
114
+ if (y % 10 === 0) console.log(`sweep ${ck}: ${rows.length} titles`);
115
+ }
116
+ }
117
+ }
118
+
119
+ async function enrich(all) {
120
+ const head = [...all].sort((a, b) => b.v - a.v).slice(0, ENRICH_TOP);
121
+ let done = 0;
122
+ for (const t of head) {
123
+ const ck = t.kind + ":" + t.id;
124
+ if (!(ck in ckpt.enrich)) {
125
+ try {
126
+ const d = await tmdb(`/${t.kind}/${t.id}`, { append_to_response: "keywords,credits,external_ids" });
127
+ const kws = ((d.keywords && (d.keywords.keywords || d.keywords.results)) || []).map((k) => (k.name || "").toLowerCase());
128
+ const cast = ((d.credits && d.credits.cast) || []).slice(0, 3).map((c) => [c.id, c.name]);
129
+ const dir = ((d.credits && d.credits.crew) || []).find((c) => c.job === "Director" || c.job === "Creator");
130
+ ckpt.enrich[ck] = {
131
+ rt: d.runtime || (Array.isArray(d.episode_run_time) && d.episode_run_time[0]) || 0,
132
+ imdb: (d.external_ids && d.external_ids.imdb_id) || d.imdb_id || null,
133
+ col: (d.belongs_to_collection && d.belongs_to_collection.id) || null,
134
+ colName: (d.belongs_to_collection && d.belongs_to_collection.name) || null,
135
+ c3: cast.length ? cast : null, dir: dir ? [dir.id, dir.name] : null, kws,
136
+ seasons: d.number_of_seasons || null,
137
+ };
138
+ } catch { ckpt.enrich[ck] = {}; }
139
+ if (++done % 250 === 0) { saveCkpt(); console.log(`enrich ${done}/${head.length}`); }
140
+ }
141
+ }
142
+ saveCkpt();
143
+ }
144
+
145
+ // ── mint: rows β†’ shards β†’ ΞΊ ──────────────────────────────────────────────────────────────────────────────
146
+ const sha256hex = (buf) => createHash("sha256").update(buf).digest("hex");
147
+ function mint() {
148
+ const seen = new Set(); const all = [];
149
+ for (const rows of Object.values(ckpt.discover)) for (const t of rows) {
150
+ const k = t.kind + ":" + t.id;
151
+ if (seen.has(k) || !t.year || !t.p) continue; // no art or no year = not library-grade
152
+ seen.add(k); all.push(t);
153
+ }
154
+ const persons = new Map(); // id β†’ { n: name, t: [rowKey…] }
155
+ const rows = all.map((t) => {
156
+ const e = ckpt.enrich[t.kind + ":" + t.id] || {};
157
+ const kws = e.kws || [];
158
+ const row = {
159
+ i: t.id, k: t.kind === "movie" ? "m" : "t", t: t.title, y: t.year, dec: Math.floor(t.year / 10) * 10,
160
+ g: t.g, m: moodsOf({ g: t.g, rt: e.rt || 0, r: t.r, v: t.v, kws }),
161
+ r: t.r, v: t.v, p: t.p, b: t.b || undefined, l: t.l || undefined,
162
+ rt: e.rt || undefined, imdb: e.imdb || undefined, col: e.col || undefined, cn: e.colName || undefined,
163
+ sn: e.seasons || undefined,
164
+ h: (THUMBS && THUMBS[(t.kind === "movie" ? "m" : "t") + t.id]) || undefined, // ~26B poster soul (P1)
165
+ };
166
+ const key = row.k + row.i;
167
+ for (const pers of [...(e.c3 || []), ...(e.dir ? [e.dir] : [])]) {
168
+ const rec = persons.get(pers[0]) || { n: pers[1], t: [] };
169
+ rec.t.push(key); persons.set(pers[0], rec);
170
+ }
171
+ if (e.c3) row.c3 = e.c3.map((c) => c[0]);
172
+ if (e.dir) row.dir = e.dir[0];
173
+ return JSON.parse(JSON.stringify(row)); // strips undefined β€” ΞΊ-stable bytes
174
+ }).sort((a, b) => (a.k + a.i < b.k + b.i ? -1 : 1));
175
+
176
+ const shards = Array.from({ length: SHARDS }, () => []);
177
+ for (const r of rows) shards[(r.i + (r.k === "t" ? 7 : 0)) % SHARDS].push(r);
178
+ const personsObj = Object.fromEntries([...persons].filter(([, v]) => v.t.length >= 3).sort((a, b) => a[0] - b[0]));
179
+
180
+ const objects = shards.map((s, n) => ({ name: "shard" + n, bytes: Buffer.from(JSON.stringify({ v: 1, n, rows: s })) }));
181
+ objects.push({ name: "persons", bytes: Buffer.from(JSON.stringify({ v: 1, persons: personsObj })) });
182
+ for (const o of objects) { o.sha256 = sha256hex(o.bytes); o.blake3 = blake3hex(new Uint8Array(o.bytes)); }
183
+ return { rows, objects, personsCount: Object.keys(personsObj).length };
184
+ }
185
+
186
+ // ── upload + root ────────────────────────────────────────────────────────────────────────────────────────
187
+ async function upload(objects) {
188
+ const token = readFileSync(path.join(os.homedir(), ".cache/huggingface/token"), "utf8").trim();
189
+ const missing = [];
190
+ for (const o of objects) if (!(await fetch(MIRROR + o.blake3, { method: "HEAD" })).ok) missing.push(o);
191
+ console.log(`mirror: ${objects.length - missing.length} present, ${missing.length} to upload`);
192
+ if (missing.length) {
193
+ const ndjson = [JSON.stringify({ key: "header", value: { summary: `holo-library-forge: ${missing.length} catalog object(s)` } })]
194
+ .concat(missing.map((o) => JSON.stringify({ key: "file", value: { path: "b/" + o.blake3, content: o.bytes.toString("base64"), encoding: "base64" } })))
195
+ .join("\n");
196
+ const r = await fetch(`https://huggingface.co/api/models/${HF_REPO}/commit/main`, {
197
+ method: "POST", headers: { Authorization: "Bearer " + token, "Content-Type": "application/x-ndjson" }, body: ndjson,
198
+ });
199
+ if (!r.ok) throw new Error("HF commit failed: " + r.status + " " + (await r.text()).slice(0, 200));
200
+ console.log("HF commit βœ“");
201
+ }
202
+ for (const o of objects) { // refuse-at-boundary: every object must re-derive from the mirror
203
+ const back = new Uint8Array(await (await fetch(MIRROR + o.blake3)).arrayBuffer());
204
+ if (blake3hex(back) !== o.blake3 || sha256hex(Buffer.from(back)) !== o.sha256) throw new Error("mirror does not re-derive: " + o.name);
205
+ }
206
+ console.log("mirror re-derives βœ“ all", objects.length);
207
+ }
208
+
209
+ // ── run ──────────────────────────────────────────────────────────────────────────────────────────────────
210
+ if (!NO_NET) { await sweep(); }
211
+ const preAll = Object.values(ckpt.discover).flat();
212
+ if (!NO_NET) { await enrich(preAll.filter((t) => t.p && t.year)); }
213
+ const { rows, objects, personsCount } = mint();
214
+ const total = objects.reduce((s, o) => s + o.bytes.length, 0);
215
+ console.log(`minted ${rows.length} titles (${rows.filter((r) => r.k === "m").length} movies Β· ${rows.filter((r) => r.k === "t").length} series) Β· ${personsCount} persons Β· ${objects.length} objects Β· ${(total / 1048576).toFixed(1)} MB`);
216
+ for (const o of objects) console.log(` ${o.name}: ${(o.bytes.length / 1024).toFixed(0)} KB sha256:${o.sha256.slice(0, 12)}… blake3:${o.blake3.slice(0, 12)}…`);
217
+
218
+ if (!DRY) {
219
+ await upload(objects);
220
+ const root = {
221
+ $comment: "Holo TV Library root β€” the ΞΊ-addressed universe catalog. Each shard/persons object lives on the ΞΊ-mirror at b/<blake3>; sha256 is the verify key (SubtleCrypto at runtime, verify-or-refuse). Rows are compact: i/k/t/y/dec/g[]/m[](moods)/r/v/p/b/rt/imdb/col/c3/dir. Mood taxonomy is a fixed rule table in holo-library-forge.mjs.",
222
+ version: THUMBS ? 2 : 1, thumbhash: !!THUMBS, moodTaxonomyVersion: MOOD_TAXONOMY_VERSION, generatedAt: new Date().toISOString().slice(0, 10),
223
+ counts: { titles: rows.length, movies: rows.filter((r) => r.k === "m").length, series: rows.filter((r) => r.k === "t").length, persons: personsCount },
224
+ mirror: MIRROR, moods: MOODS,
225
+ shards: objects.filter((o) => o.name.startsWith("shard")).map((o) => ({ sha256: o.sha256, blake3: o.blake3, bytes: o.bytes.length })),
226
+ persons: (({ sha256, blake3, bytes }) => ({ sha256, blake3, bytes: bytes.length }))(objects.find((o) => o.name === "persons")),
227
+ };
228
+ writeFileSync(path.join(HERE, "feed/library-index.json"), JSON.stringify(root, null, 1) + "\n");
229
+ console.log("root written: feed/library-index.json βœ“");
230
+ }
b/c4b2125423bba7997b58b761894a40a671b6da3a0f754d214cfac8da2f039025 ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // holo-thumbhash.mjs β€” the poster's SOUL in ~26 bytes, shipped inside its ΞΊ-row (INSTANT-PLANE P1).
2
+ //
3
+ // A 4Γ—6 RGB332 micro-thumbnail: enough to paint every tile in the library with its own colours in ONE
4
+ // frame, from the shards alone β€” offline, before a single image request exists. The real artwork then
5
+ // crossfades over it (same colours by construction, so it never pops). Self-consistent codec: WE encode
6
+ // (forge, Node) and WE decode (player, browser) β€” no external format to match, nothing to drift.
7
+ //
8
+ // encode: rgbaToHash(rgba, w, h) β€” rgba = Uint8Array RGBA at EXACTLY wΓ—h (forge downsamples first)
9
+ // decode: hashToDataURL(b64) β€” tiny canvas β†’ smoothed PNG data URL (browser only)
10
+
11
+ const W = 4, H = 6; // grid β€” portrait 2:3, matches poster tiles
12
+ export const GRID = { W, H };
13
+
14
+ // RGB β†’ one byte: RRRGGGBB (3-3-2). Blurred to bits anyway; hue survives, weight stays tiny.
15
+ const pack = (r, g, b) => ((r >> 5) << 5) | ((g >> 5) << 2) | (b >> 6);
16
+ const unpack = (v) => [((v >> 5) & 7) * 36.5, ((v >> 2) & 7) * 36.5, (v & 3) * 85];
17
+
18
+ // rgba (wΓ—hΓ—4) β†’ base64 hash. Header byte = 0x1 (version) β€” future grids can coexist.
19
+ export function rgbaToHash(rgba, w = W, h = H) {
20
+ if (!rgba || rgba.length < w * h * 4) return null;
21
+ const out = new Uint8Array(1 + w * h);
22
+ out[0] = 0x1;
23
+ for (let i = 0; i < w * h; i++) out[1 + i] = pack(rgba[i * 4], rgba[i * 4 + 1], rgba[i * 4 + 2]);
24
+ return typeof Buffer !== "undefined" ? Buffer.from(out).toString("base64") : btoa(String.fromCharCode(...out));
25
+ }
26
+
27
+ export function hashToBytes(b64) {
28
+ try {
29
+ const bin = typeof Buffer !== "undefined" ? new Uint8Array(Buffer.from(b64, "base64")) : (() => { const s = atob(b64); const u = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) u[i] = s.charCodeAt(i); return u; })();
30
+ return bin[0] === 0x1 && bin.length === 1 + W * H ? bin : null;
31
+ } catch { return null; }
32
+ }
33
+
34
+ // browser: hash β†’ data URL (memoized β€” same hash decodes once per session)
35
+ const memo = typeof Map !== "undefined" ? new Map() : null;
36
+ export function hashToDataURL(b64) {
37
+ if (!b64) return null;
38
+ if (memo && memo.has(b64)) return memo.get(b64);
39
+ const bin = hashToBytes(b64);
40
+ if (!bin || typeof document === "undefined") return null;
41
+ const c = document.createElement("canvas"); c.width = W; c.height = H;
42
+ const ctx = c.getContext("2d");
43
+ const id = ctx.createImageData(W, H);
44
+ for (let i = 0; i < W * H; i++) { const [r, g, b] = unpack(bin[1 + i]); id.data[i * 4] = r; id.data[i * 4 + 1] = g; id.data[i * 4 + 2] = b; id.data[i * 4 + 3] = 255; }
45
+ ctx.putImageData(id, 0, 0);
46
+ // upscale with smoothing so the placeholder is a soft gradient, not visible blocks
47
+ const up = document.createElement("canvas"); up.width = W * 8; up.height = H * 8;
48
+ const uctx = up.getContext("2d"); uctx.imageSmoothingEnabled = true; uctx.imageSmoothingQuality = "high";
49
+ uctx.drawImage(c, 0, 0, up.width, up.height);
50
+ const url = up.toDataURL("image/png");
51
+ if (memo) memo.set(b64, url);
52
+ return url;
53
+ }
54
+
55
+ if (typeof window !== "undefined") window.HoloThumbHash = { rgbaToHash, hashToDataURL, hashToBytes, GRID };
56
+ export default { rgbaToHash, hashToDataURL, hashToBytes, GRID };