Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 336 lines 12 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>jeffrey's instagram · platter</title> 7<meta name="description" content="An index of Jeffrey Alan Scudder's confirmed-self IG selfie corpus from @whistlegraph — 457 photos with face-match scores and per-image GPT-4o descriptions."> 8<link rel="icon" href="https://aesthetic.computer/icon/128x128/prompt.png" type="image/png"> 9<link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css"> 10<style> 11:root { 12 color-scheme: dark light; 13 --bg: #111; --bg2: #1a1a1a; --fg: #ddd; --fg2: #888; 14 --accent: #ff6b9d; --accent2: #4ecdc4; --gold: #ffd93d; 15 --border: #333; --hover: #222; 16 --img-bg: #000; --img-placeholder: #222; 17 --lightbox-bg: rgba(0,0,0,0.95); 18} 19@media (prefers-color-scheme: light) { 20 :root { 21 --bg: #fafafa; --bg2: #fff; --fg: #222; --fg2: #666; 22 --accent: #d63384; --accent2: #0a8a82; --gold: #b8860b; 23 --border: #ddd; --hover: #f0f0f0; 24 --img-bg: #eee; --img-placeholder: #ddd; 25 --lightbox-bg: rgba(250,250,250,0.96); 26 } 27} 28* { margin: 0; padding: 0; box-sizing: border-box; } 29html, body { background: var(--bg); color: var(--fg); } 30body { 31 font-family: 'Berkeley Mono Variable', ui-monospace, monospace; 32 font-size: 13px; line-height: 1.4; 33 min-height: 100vh; 34} 35a { color: var(--accent2); text-decoration: none; } 36a:hover { color: var(--accent); text-decoration: underline; } 37 38header { 39 padding: 16px 12px 12px; 40 border-bottom: 1px solid var(--border); 41 position: sticky; top: 0; background: var(--bg); z-index: 10; 42} 43h1 { 44 font-size: 18px; font-weight: normal; letter-spacing: 4px; 45 margin-bottom: 4px; 46} 47h1 .accent { color: var(--accent); } 48.sub { font-size: 11px; color: var(--fg2); margin-bottom: 10px; } 49.controls { 50 display: flex; gap: 8px; flex-wrap: wrap; align-items: center; 51} 52.controls input, .controls select { 53 font-family: inherit; font-size: 12px; padding: 4px 8px; 54 background: var(--bg2); color: var(--fg); 55 border: 1px solid var(--border); outline: none; 56 min-width: 180px; 57} 58.controls input:focus, .controls select:focus { border-color: var(--accent2); } 59.controls .stat { 60 font-size: 11px; color: var(--fg2); 61 margin-left: auto; 62 font-variant-numeric: tabular-nums; 63} 64.controls .stat b { color: var(--fg); font-weight: normal; } 65 66main { 67 padding: 12px; 68 display: grid; 69 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); 70 gap: 10px; 71} 72.card { 73 background: var(--bg2); border: 1px solid var(--border); 74 overflow: hidden; cursor: pointer; 75 display: flex; flex-direction: column; 76 transition: border-color 0.1s, transform 0.1s; 77} 78.card:hover { border-color: var(--accent2); } 79.card:active { transform: scale(0.99); } 80.thumb-wrap { 81 width: 100%; aspect-ratio: 1; background: var(--img-bg); 82 overflow: hidden; position: relative; 83} 84.thumb-wrap img { 85 width: 100%; height: 100%; object-fit: cover; display: block; 86 background: var(--img-placeholder); 87} 88.thumb-wrap .sim { 89 position: absolute; top: 4px; right: 4px; 90 font-size: 10px; padding: 2px 5px; 91 background: rgba(0,0,0,0.7); color: #fff; 92 font-variant-numeric: tabular-nums; 93} 94.meta { 95 padding: 6px 8px; font-size: 11px; color: var(--fg2); 96 display: flex; flex-direction: column; gap: 2px; 97} 98.meta .date { color: var(--fg); font-variant-numeric: tabular-nums; } 99.meta .caption { 100 color: var(--fg2); 101 display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; 102 overflow: hidden; 103} 104.meta .tags { color: var(--accent2); font-size: 10px; opacity: 0.8; } 105 106#empty { padding: 40px; text-align: center; color: var(--fg2); } 107#loading { padding: 40px; text-align: center; color: var(--fg2); } 108 109/* lightbox */ 110#lightbox { 111 display: none; position: fixed; inset: 0; 112 background: var(--lightbox-bg); z-index: 100; 113 padding: 20px; overflow-y: auto; 114} 115#lightbox.open { display: flex; align-items: flex-start; justify-content: center; } 116.lb-inner { 117 max-width: 1400px; width: 100%; 118 display: grid; grid-template-columns: 1fr; gap: 20px; 119} 120@media (min-width: 800px) { 121 .lb-inner { grid-template-columns: minmax(0, 1fr) 360px; } 122} 123.lb-img { 124 background: var(--img-bg); 125 width: 100%; 126 max-height: calc(100vh - 80px); 127 display: flex; align-items: center; justify-content: center; 128 overflow: hidden; 129} 130.lb-img img { 131 max-width: 100%; max-height: calc(100vh - 80px); 132 width: auto; height: auto; 133 object-fit: contain; 134 transition: filter 0.2s; 135} 136.lb-info { 137 font-size: 13px; line-height: 1.5; 138 display: flex; flex-direction: column; gap: 10px; 139} 140.lb-info h2 { 141 font-size: 14px; font-weight: normal; letter-spacing: 2px; 142 color: var(--accent); margin-bottom: 4px; 143} 144.lb-row { 145 display: grid; grid-template-columns: 110px 1fr; gap: 8px; 146 font-size: 12px; 147} 148.lb-row .k { color: var(--fg2); text-transform: uppercase; font-size: 10px; letter-spacing: 1px; padding-top: 1px; } 149.lb-row .v { color: var(--fg); } 150.lb-row .v.tag-list { color: var(--accent2); } 151.lb-close { 152 position: fixed; top: 12px; right: 16px; 153 font-family: inherit; background: none; border: 1px solid var(--border); 154 color: var(--fg); padding: 4px 10px; cursor: pointer; 155} 156.lb-close:hover { border-color: var(--accent); color: var(--accent); } 157.lb-ig-link { 158 font-size: 11px; color: var(--accent2); margin-top: 4px; 159} 160 161footer { 162 padding: 20px 12px; text-align: center; font-size: 11px; 163 color: var(--fg2); border-top: 1px solid var(--border); 164 margin-top: 20px; 165} 166footer a { color: var(--fg2); } 167footer a:hover { color: var(--accent2); } 168</style> 169</head> 170<body> 171 172<header> 173 <h1><a href="../" style="color:inherit;text-decoration:none;">platter</a><span class="accent">·</span>jeffrey's instagram</h1> 174 <div class="sub"> 175 confirmed-self selfie corpus from 176 <a href="https://www.instagram.com/whistlegraph/" target="_blank">@whistlegraph</a>, 177 matched against the AV studio shoot, described per-image by GPT-4o 178 </div> 179 <div class="controls"> 180 <input id="search" type="search" placeholder="search descriptions, tags, locations…"> 181 <select id="sort"> 182 <option value="date-desc">newest first</option> 183 <option value="date-asc">oldest first</option> 184 <option value="sim-desc">highest face-match</option> 185 </select> 186 <select id="domain"> 187 <option value="">all domains</option> 188 </select> 189 <span class="stat" id="stat"></span> 190 </div> 191</header> 192 193<main id="grid"> 194 <div id="loading">loading manifest…</div> 195</main> 196 197<div id="lightbox"> 198 <button class="lb-close" id="lbClose">close ✕</button> 199 <div class="lb-inner" id="lbBody"></div> 200</div> 201 202<footer> 203 <a href="../">all platters</a> 204 &nbsp;·&nbsp; 205 <a href="https://github.com/digitpain/aesthetic-computer/blob/main/papers/jeffrey-platter/README.md">jeffrey-platter README</a> 206 &nbsp;·&nbsp; 207 <a href="../../">papers.aesthetic.computer</a> 208</footer> 209 210<script type="module"> 211const $ = (id) => document.getElementById(id); 212const grid = $("grid"); 213const search = $("search"); 214const sortSel = $("sort"); 215const domainSel = $("domain"); 216const stat = $("stat"); 217const lightbox = $("lightbox"); 218const lbBody = $("lbBody"); 219 220let rows = []; 221 222(async () => { 223 try { 224 const res = await fetch("./manifest.json", { cache: "no-store" }); 225 if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); 226 const data = await res.json(); 227 rows = data.rows; 228 populateDomains(rows); 229 render(); 230 } catch (e) { 231 grid.innerHTML = `<div id="empty">failed to load manifest: ${e.message}</div>`; 232 } 233})(); 234 235function populateDomains(rs) { 236 const domains = [...new Set(rs.map((r) => r.domain).filter(Boolean))].sort(); 237 for (const d of domains) { 238 const opt = document.createElement("option"); 239 opt.value = d; opt.textContent = d; 240 domainSel.appendChild(opt); 241 } 242} 243 244function filterAndSort() { 245 const q = search.value.trim().toLowerCase(); 246 const dom = domainSel.value; 247 let out = rows; 248 if (dom) out = out.filter((r) => r.domain === dom); 249 if (q) { 250 out = out.filter((r) => { 251 const hay = [ 252 r.description, r.expression, r.pose, r.location, r.caption, 253 r.style, r.framing, r.domain, 254 ...(r.tags || []), 255 ].filter(Boolean).join(" ").toLowerCase(); 256 return hay.includes(q); 257 }); 258 } 259 const mode = sortSel.value; 260 if (mode === "date-asc") out = [...out].sort((a, b) => a.date.localeCompare(b.date)); 261 else if (mode === "sim-desc") out = [...out].sort((a, b) => b.similarity - a.similarity); 262 else out = [...out].sort((a, b) => b.date.localeCompare(a.date)); 263 return out; 264} 265 266function render() { 267 const list = filterAndSort(); 268 stat.innerHTML = `<b>${list.length}</b> / ${rows.length}`; 269 if (list.length === 0) { 270 grid.innerHTML = `<div id="empty">no matches</div>`; 271 return; 272 } 273 const html = list.map((r, i) => ` 274 <div class="card" data-i="${rows.indexOf(r)}"> 275 <div class="thumb-wrap"> 276 <img src="${r.full || r.thumb}" loading="lazy" decoding="async" alt=""> 277 <div class="sim">${(r.similarity).toFixed(2)}</div> 278 </div> 279 <div class="meta"> 280 <div class="date">${r.date}</div> 281 ${r.caption ? `<div class="caption">${escapeHtml(r.caption)}</div>` : ""} 282 ${r.tags?.length ? `<div class="tags">${escapeHtml(r.tags.slice(0, 4).join(" · "))}</div>` : ""} 283 </div> 284 </div>`).join(""); 285 grid.innerHTML = html; 286 for (const card of grid.querySelectorAll(".card")) { 287 card.addEventListener("click", () => openLightbox(rows[+card.dataset.i])); 288 } 289} 290 291function openLightbox(r) { 292 const igUrl = `https://www.instagram.com/p/${r.shortcode}/`; 293 // Same full-res image the grid uses — the browser already has it cached 294 // from the card click, so the lightbox renders instantly. 295 const hires = r.full || r.thumb; 296 lbBody.innerHTML = ` 297 <div class="lb-img"><img src="${hires}" alt=""></div> 298 <div class="lb-info"> 299 <h2>${r.date}</h2> 300 <div class="lb-row"><span class="k">subject</span><span class="v">${escapeHtml(r.description || "—")}</span></div> 301 <div class="lb-row"><span class="k">expression</span><span class="v">${escapeHtml(r.expression || "—")}</span></div> 302 <div class="lb-row"><span class="k">pose</span><span class="v">${escapeHtml(r.pose || "—")}</span></div> 303 <div class="lb-row"><span class="k">location</span><span class="v">${escapeHtml(r.location || "—")}</span></div> 304 <div class="lb-row"><span class="k">style</span><span class="v">${escapeHtml(r.style || "—")} · ${escapeHtml(r.framing || "—")}</span></div> 305 <div class="lb-row"><span class="k">domain</span><span class="v">${escapeHtml(r.domain || "—")}</span></div> 306 ${r.tags?.length ? `<div class="lb-row"><span class="k">tags</span><span class="v tag-list">${escapeHtml(r.tags.join(" · "))}</span></div>` : ""} 307 ${r.caption ? `<div class="lb-row"><span class="k">caption</span><span class="v">${escapeHtml(r.caption)}</span></div>` : ""} 308 <div class="lb-row"><span class="k">face-match</span><span class="v">${r.similarity.toFixed(4)} (conf ${r.confidence ?? "—"})</span></div> 309 <div class="lb-row"><span class="k">others</span><span class="v">${r.n_other_people} other people in frame</span></div> 310 <a class="lb-ig-link" href="${igUrl}" target="_blank">view on instagram ↗</a> 311 </div>`; 312 lightbox.classList.add("open"); 313 document.body.style.overflow = "hidden"; 314} 315 316function closeLightbox() { 317 lightbox.classList.remove("open"); 318 document.body.style.overflow = ""; 319} 320 321$("lbClose").addEventListener("click", closeLightbox); 322lightbox.addEventListener("click", (e) => { if (e.target === lightbox) closeLightbox(); }); 323document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeLightbox(); }); 324 325let t; 326search.addEventListener("input", () => { clearTimeout(t); t = setTimeout(render, 120); }); 327sortSel.addEventListener("change", render); 328domainSel.addEventListener("change", render); 329 330function escapeHtml(s) { 331 return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); 332} 333</script> 334 335</body> 336</html>