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 };
|