Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 189bb8a3ed05cbfb446d29a0ac3a6d9e146d4757
parent 7f1b762f28b77c2a6cdb2738d1c06e417a80c559
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 16:18:11 -0400

homepage: / rethought — claim, chart, family numbers, official instances, one paragraph of what it does

Drops the recent-acquisitions rail (component kept), How it works, What this
isn't and the eyebrows. The stats strip and the cards read the summary's v5
official / per-site numbers and hide without them; cards stripe with
seriesColor(i) to match the chart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mhomepage/app/components/ArchiveCards.tsx | 115+++++++++++++++++++++++++++++++++++++++----------------------------------------
Ahomepage/app/components/FamilyStats.tsx | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhomepage/app/page.tsx | 279++++++++++++++++++++++++-------------------------------------------------------
3 files changed, 209 insertions(+), 254 deletions(-)

diff --git a/homepage/app/components/ArchiveCards.tsx b/homepage/app/components/ArchiveCards.tsx @@ -1,70 +1,69 @@ import type { HomepageSummarySite } from "yt-dlp-transcript-common/lib/homepageSummary"; import { seriesColor } from "yt-dlp-transcript-common/lib/homepageChart"; -// THE ONLY POLYCHROME MOMENT ON THE PAGE. +// The official instances: one card per public archive, its title linking out, +// its own numbers underneath. The stripe is seriesColor(i), the same index the +// growth chart colours that instance's layer with, so a card is its own legend. // -// The chrome of this family spends no colour at all. Here it does, and the -// colour carries the argument: the tool is achromatic, the archives are not. A -// visitor should be able to see at a glance that these are distinct -// publications, not one product's sections. +// A site's `accent` is deliberately NOT used here any more: the chart cannot +// wear a per-site brand hex (it would break the validated order and dark mode), +// and a card whose stripe disagrees with its layer is worse than no stripe. // -// WHY NOT SiteGrid. SiteGrid is a `"use client"` component whose props ARE chart -// state — metric, breakdown, a hidden-set and a toggle callback — because on -// /stats/ its cards double as the chart legend. There is no chart here, so -// reusing it would mean inventing chart state and shipping a client bundle to -// render a static list. This is the same data, rendered flat and server-side. -// -// COLOUR SOURCE, in order: the site's own `accent` from its config, when it has -// one. Otherwise the colour that site already wears in the dashboard's charts — -// which is a legend assignment, deterministic per site, and not a brand claim -// being invented on the site's behalf. +// Numbers are v5 fields; a pre-v5 summary lacks them and the card shows only +// the transcript count it always had. + +function Figure({ value, unit }: { value: number | undefined; unit: string }) { + if (value === undefined) return null; + return ( + <div className="flex min-w-0 flex-col-reverse justify-end gap-1"> + <dt className="text-xs text-[var(--faint)]">{unit}</dt> + <dd className="tabular text-[0.9375rem] text-[var(--foreground)]"> + {value.toLocaleString()} + </dd> + </div> + ); +} + export function ArchiveCards({ sites }: { sites: HomepageSummarySite[] }) { if (sites.length === 0) return null; return ( - // Hairline geometry without phantom cells: each card draws its own right - // and bottom rule and the list draws the top and left, so a row that - // doesn't divide evenly leaves empty SPACE rather than an empty filled - // cell. (A `gap-px` grid over a coloured background paints the gaps of the - // missing cells too, which reads as a broken card.) <ul className="grid grid-cols-1 border-t border-l border-[var(--border)] sm:grid-cols-2 lg:grid-cols-3 list-none"> - {sites.map((site, i) => { - const color = site.accent ?? seriesColor(i); - return ( - <li - key={site.siteId} - className="border-r border-b border-[var(--border)] bg-[var(--surface)]" - > - <a - href={site.siteUrl} - target="_blank" - rel="noopener noreferrer" - className="group flex h-full flex-col gap-3 p-5 transition-colors hover:bg-[var(--panel-2)]" - > - <div className="flex items-center gap-3"> - <span - aria-hidden="true" - className="h-5 w-[3px] shrink-0" - style={{ backgroundColor: color }} - /> - <h3 className="font-display text-base font-semibold text-[var(--foreground)]"> - {site.siteTitle} - </h3> - </div> - {site.siteDescription && ( - <p className="line-clamp-2 text-sm leading-relaxed text-[var(--muted-foreground)]"> - {site.siteDescription} - </p> - )} - <div className="mt-auto flex items-baseline gap-2 pt-2"> - <span className="tabular text-lg text-[var(--foreground)]"> - {site.transcribed.total.toLocaleString()} - </span> - <span className="label-machine">transcripts</span> - </div> - </a> - </li> - ); - })} + {sites.map((site, i) => ( + <li + key={site.siteId} + className="relative flex flex-col gap-4 border-r border-b border-[var(--border)] bg-[var(--surface)] p-5 pl-6" + > + <span + aria-hidden="true" + className="absolute left-0 top-0 h-full w-[3px]" + style={{ backgroundColor: seriesColor(i) }} + /> + <div className="flex flex-col gap-1.5"> + <h3 className="font-display text-lg font-semibold text-[var(--foreground)]"> + <a + href={site.siteUrl} + target="_blank" + rel="noopener noreferrer" + className="underline decoration-transparent underline-offset-4 transition-colors hover:decoration-[var(--border-strong)]" + > + {site.siteTitle} + <span aria-hidden="true" className="ml-1.5 text-sm text-[var(--faint)]">↗</span> + </a> + </h3> + {site.siteDescription && ( + <p className="text-sm leading-relaxed text-[var(--muted-foreground)]"> + {site.siteDescription} + </p> + )} + </div> + <dl className="mt-auto grid grid-cols-4 gap-3"> + <Figure value={site.channels} unit="channels" /> + <Figure value={site.recordings} unit="recordings" /> + <Figure value={site.transcribed.total} unit="transcripts" /> + <Figure value={site.hoursArchived} unit="hours" /> + </dl> + </li> + ))} </ul> ); } diff --git a/homepage/app/components/FamilyStats.tsx b/homepage/app/components/FamilyStats.tsx @@ -0,0 +1,69 @@ +import type { HomepageOfficialTotals } from "yt-dlp-transcript-common/lib/homepageSummary"; + +// The family's numbers as one readout strip: hairline cells, a figure and a +// plain label each. Everything is the sum over the official instances (the +// summary's `official`), which is what the caption claims. +// +// The "gone" cell is the one that carries the project's argument, so it is the +// one that gets the width: a full row on a phone, two cells on a wide screen. + +export function FamilyStats({ + official, + generatedAt, +}: { + official: HomepageOfficialTotals; + generatedAt: string | null; +}) { + const cells = [ + { value: official.hoursArchived, label: "Hours of speech" }, + { value: official.transcripts, label: "Transcripts" }, + { value: official.recordings, label: "Recordings" }, + { value: official.channels, label: "Channels" }, + { value: official.sites, label: "Official instances" }, + ]; + const built = generatedAt ? new Date(generatedAt) : null; + const builtOk = built && !Number.isNaN(built.getTime()); + return ( + <div className="flex flex-col gap-3"> + <dl className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-7 border-t border-l border-[var(--border)]"> + {cells.map((c, i) => ( + <div + key={c.label} + // Five cells in a two-column phone grid: the fifth takes the row. + className={`flex flex-col-reverse justify-end gap-2 border-r border-b border-[var(--border)] px-4 py-5 ${ + i === cells.length - 1 ? "col-span-2 sm:col-span-1" : "" + }`} + > + <dt className="text-sm text-[var(--muted-foreground)]">{c.label}</dt> + <dd className="tabular text-[1.5rem] sm:text-[1.75rem] font-medium leading-none tracking-[-0.02em] text-[var(--foreground)]"> + {c.value.toLocaleString()} + </dd> + </div> + ))} + {official.gone > 0 && ( + <div className="col-span-2 flex flex-col-reverse justify-end gap-2 border-r border-b border-[var(--border)] px-4 py-5 sm:col-span-1 lg:col-span-2"> + <dt className="text-sm text-[var(--muted-foreground)]"> + Recordings gone at the source, still here + </dt> + <dd className="tabular text-[1.5rem] sm:text-[1.75rem] font-medium leading-none tracking-[-0.02em] text-[var(--state-gone)]"> + {official.gone.toLocaleString()} + </dd> + </div> + )} + </dl> + {builtOk && ( + <p className="text-sm text-[var(--faint)]"> + Across the official instances · index built{" "} + <time dateTime={built.toISOString().slice(0, 10)}> + {built.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + timeZone: "UTC", + })} + </time> + </p> + )} + </div> + ); +} diff --git a/homepage/app/page.tsx b/homepage/app/page.tsx @@ -1,48 +1,29 @@ import Link from "next/link"; -import { RecentAdditions } from "./components/RecentAdditions"; -import { KpiHeader } from "./components/KpiHeader"; +import { ArchiveGrowthChart } from "./components/ArchiveGrowthChart"; +import { FamilyStats } from "./components/FamilyStats"; import { ArchiveCards } from "./components/ArchiveCards"; import { loadSummary } from "./lib/summary"; import { currentHomepage } from "./lib/homepage"; -// The project's front page. Its ONE job: make a visitor understand what -// Archilyzer is and download it. +// The project's front page. It says what Archilyzer is, shows what it has +// built, and offers the download. // -// Every number on this page is computed at build time from the operator's own -// corpus and gated on that corpus existing — see loadSummary(). There are no -// hardcoded figures, because they move: the public-archive count went from three -// to four during this page's own construction. A build without data drops the -// numeric bands entirely rather than printing zeroes. +// Every number is computed at build time from the operator's corpus (the +// summary's `official` block and `monthly` series) and hidden when absent — see +// loadSummary(). A build with no data renders the words and the links only. +// +// Copy rules, kept deliberately: short declarative sentences, no eyebrow over +// every band, no subtitles under the headings. The page has two headings after +// the H1 and both name what is under them. const CONTAINER = "w-full max-w-6xl mx-auto px-5 sm:px-6"; -function Band({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( - <section className={`border-t border-[var(--border)] ${className ?? ""}`}> - <div className={`${CONTAINER} py-14 sm:py-20`}>{children}</div> - </section> - ); -} - -function Eyebrow({ children }: { children: React.ReactNode }) { - return <h2 className="label-machine mb-8">{children}</h2>; -} - export default function Home() { const summary = loadSummary(); - const hours = summary?.totals.hoursArchived ?? null; - const gone = summary?.availability?.byState.deleted ?? 0; - const counted = summary?.availability?.counted ?? 0; - // The operator's hub — one search over every published archive — is a - // separate app on its own Pages project; homepage.json's siteUrl is its - // address (the same field the hub's own build reads). No URL, no link: a - // source-only build has no hub to point at. + const official = summary?.official ?? null; + const hours = official?.hoursArchived ?? summary?.totals.hoursArchived ?? null; + const sites = summary?.sites ?? []; + const months = summary?.monthly ?? []; // Operator decision 2026-09-25: do not link the hub from the homepage until it // is more polished. The URL stays in homepage.json (the hub's own build reads // it); only the hero button is withheld. Flip this to true to show it again. @@ -51,14 +32,11 @@ export default function Home() { return ( <> - {/* ── Hero ─────────────────────────────────────────────────────────── */} - <section className={`${CONTAINER} pt-14 pb-16 sm:pt-20 sm:pb-24`}> - <div className="flex flex-col gap-8"> - {/* Sentence case, and nowhere near poster scale. The earlier version - was set in bold expanded caps at 4.75rem, which read as shouting — - the wrong register for a tool whose argument is composure. */} + {/* ── Hero: the claim, the two ways in, then the evidence ───────────── */} + <section className={`${CONTAINER} pt-12 pb-14 sm:pt-16 sm:pb-20`}> + <div className="flex flex-col gap-6"> <h1 - className="reveal font-display text-[1.9rem] leading-[1.1] font-semibold tracking-[-0.01em] text-[var(--foreground)] sm:text-[2.6rem] lg:text-[3rem] max-w-3xl" + className="reveal font-display text-[1.75rem] leading-[1.12] font-semibold tracking-[-0.01em] text-[var(--foreground)] sm:text-[2.25rem] lg:text-[2.625rem]" style={{ "--d": "60ms" } as React.CSSProperties} > {hours ? ( @@ -74,16 +52,16 @@ export default function Home() { </h1> <p - className="reveal max-w-xl text-[1.0625rem] leading-relaxed text-[var(--muted-foreground)]" - style={{ "--d": "160ms" } as React.CSSProperties} + className="reveal max-w-2xl text-[1.0625rem] leading-relaxed text-[var(--muted-foreground)]" + style={{ "--d": "140ms" } as React.CSSProperties} > Download a channel&rsquo;s back catalogue, transcribe it on your own - machine, publish a static site you host yourself. + hardware, publish a static site you host yourself. </p> <div className="reveal flex flex-wrap items-center gap-x-8 gap-y-4" - style={{ "--d": "240ms" } as React.CSSProperties} + style={{ "--d": "200ms" } as React.CSSProperties} > <Link href="/downloads/" @@ -107,167 +85,76 @@ export default function Home() { )} </div> - {/* The rail: real acquisitions, real states. The signature object. */} - <div - className="reveal mt-6" - style={{ "--d": "320ms" } as React.CSSProperties} - > - <RecentAdditions items={summary?.recent ?? []} /> - </div> + {months.length > 0 && sites.length > 0 && ( + <div + className="reveal mt-6" + style={{ "--d": "280ms" } as React.CSSProperties} + > + <ArchiveGrowthChart months={months} sites={sites} /> + </div> + )} </div> </section> - {/* ── The numbers, only when there are numbers ─────────────────────── */} - {summary && ( - <Band> - <Eyebrow>This deployment, at last build</Eyebrow> - <KpiHeader summary={summary} /> - </Band> + {/* ── The family's numbers ─────────────────────────────────────────── */} + {official && official.sites > 0 && ( + <section className={`${CONTAINER} pb-14 sm:pb-20`}> + <FamilyStats + official={official} + generatedAt={summary?.generatedAt ?? null} + /> + </section> )} - {/* ── What it does ─────────────────────────────────────────────────── */} - <Band> - <Eyebrow>What it does</Eyebrow> - <div className="grid grid-cols-1 gap-px bg-[var(--border)] border border-[var(--border)] md:grid-cols-3"> - {[ - { - title: "Archive", - body: "The whole back catalogue, then new uploads on a schedule. Captions where the platform has them, local transcription where it doesn't.", - }, - { - title: "Publish", - body: "Plain HTML and JSON — no database, nothing to keep running. One corpus can publish several sites without duplicating data.", - }, - { - title: "Search & ask", - body: "Search every transcript at once, land on the exact second. A machine-readable index ships with it, so an LLM can navigate the archive too.", - }, - ].map((panel) => ( - <div - key={panel.title} - className="bg-[var(--surface)] p-6 flex flex-col gap-2" - > - <h3 className="font-display text-base font-semibold text-[var(--foreground)]"> - {panel.title} - </h3> - <p className="text-sm leading-[1.65] text-[var(--muted-foreground)]"> - {panel.body} - </p> - </div> - ))} - </div> - </Band> + {/* ── Official instances ───────────────────────────────────────────── */} + {sites.length > 0 && ( + <section className="border-t border-[var(--border)]"> + <div className={`${CONTAINER} py-14 sm:py-20`}> + <h2 className="font-display text-2xl font-semibold text-[var(--foreground)]"> + Official instances + </h2> + <p className="mt-2 mb-8 max-w-xl text-[var(--muted-foreground)] leading-relaxed"> + The archives I run. Anyone can run their own. + </p> + <ArchiveCards sites={sites} /> + </div> + </section> + )} - {/* ── The proof point ──────────────────────────────────────────────── */} - {gone > 0 && ( - <Band> - <div className="flex flex-col gap-4 max-w-2xl"> - <p className="font-display text-xl sm:text-2xl font-semibold leading-[1.25] text-[var(--foreground)]"> - <span className="tabular font-medium text-[var(--state-gone)]"> - {gone.toLocaleString()} - </span>{" "} - recordings here no longer exist where they came from. + {/* ── What it does ─────────────────────────────────────────────────── */} + {/* One paragraph, not three titled panels: without titles, three + one-sentence items are just a paragraph with bullets. */} + <section className="border-t border-[var(--border)]"> + <div className={`${CONTAINER} py-14 sm:py-20`}> + <h2 className="font-display text-2xl font-semibold text-[var(--foreground)]"> + What it does + </h2> + <div className="mt-6 flex max-w-2xl flex-col gap-4 text-[1.0625rem] leading-relaxed text-[var(--muted-foreground)]"> + <p> + Archilyzer downloads a channel&rsquo;s videos and keeps up with + new uploads. It uses the platform&rsquo;s captions where they + exist and transcribes the audio on your machine where they + don&rsquo;t. </p> - <p className="text-[var(--muted-foreground)] leading-relaxed"> - Re-checked and found gone. The transcripts are still here, still - searchable, still citable to the second. + <p> + The result is a static site of plain HTML and JSON that any file + host can serve. Search covers every transcript and opens the video + at the second the words were said. </p> - <p className="text-sm text-[var(--faint)] leading-relaxed"> - The other {(counted - gone).toLocaleString()} are simply not known - to be gone — nothing re-checks a corpus this size continuously. A - floor, not a guarantee. + <p> + It is a program you run, not a service. There is no account and + nothing phones home.{" "} + <Link + href="/docs/" + className="text-[var(--foreground)] underline decoration-[var(--border-strong)] underline-offset-4 hover:decoration-[var(--foreground)]" + > + The docs + </Link>{" "} + cover setup and deployment. </p> </div> - </Band> - )} - - {/* ── How it works ─────────────────────────────────────────────────── */} - <Band> - <Eyebrow>How it works</Eyebrow> - <ol className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4 list-none"> - {[ - { - n: "01", - t: "Pick channels", - d: "Add a URL. Captions as published, or audio transcribed here.", - }, - { - n: "02", - t: "Transcribe", - d: "whisper.cpp or another local backend. Nothing is uploaded.", - }, - { - n: "03", - t: "Compose", - d: "A paginated index and a static site, rebuilt incrementally.", - }, - { - n: "04", - t: "Ship", - d: "Deploy anywhere that serves files. A free tier is enough.", - }, - ].map((step) => ( - <li key={step.n} className="flex flex-col gap-1.5"> - <span className="tabular text-xs text-[var(--faint)]"> - {step.n} - </span> - <h3 className="font-display text-base font-semibold text-[var(--foreground)]"> - {step.t} - </h3> - <p className="text-sm leading-[1.65] text-[var(--muted-foreground)]"> - {step.d} - </p> - </li> - ))} - </ol> - </Band> - - {/* ── What this isn't. The highest-trust block on the page, and the - cheapest: it is all simply true. ──────────────────────────────── */} - <Band> - <Eyebrow>What this isn&rsquo;t</Eyebrow> - <ul className="grid grid-cols-1 gap-x-10 gap-y-6 sm:grid-cols-2 list-none max-w-4xl"> - {[ - [ - "Not a hosted service", - "Nothing to sign up for. Your machine, your hosting.", - ], - [ - "No public repository", - "A dated snapshot tarball. Nothing to pull.", - ], - [ - "You supply the tools", - "yt-dlp, ffmpeg and a transcription backend are yours to install.", - ], - [ - "Transcription is slow", - "Bound by your GPU. A large catalogue is days of machine time.", - ], - ].map(([title, body]) => ( - <li key={title} className="flex flex-col gap-1"> - <h3 className="text-sm font-semibold text-[var(--foreground)]"> - {title} - </h3> - <p className="text-sm leading-[1.65] text-[var(--muted-foreground)]"> - {body} - </p> - </li> - ))} - </ul> - </Band> - - {/* ── The reference deployment ─────────────────────────────────────── */} - {summary && summary.sites.length > 0 && ( - <Band> - <Eyebrow>The reference deployment</Eyebrow> - <p className="mb-8 max-w-xl text-[var(--muted-foreground)] leading-relaxed"> - Public archives built from one corpus on one machine — the output in - use. - </p> - <ArchiveCards sites={summary.sites} /> - </Band> - )} + </div> + </section> </> ); }