commit fef20670ac330227ef29e6ec0599417bc6ab49bc
parent d32e6b2e726ab50d56b8c3b6620f16460fc9ea3d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 29 Jun 2026 01:47:52 -0400
Add transcript coverage to public stats + charts
Carry a per-video `coverage` fraction (last cue end ÷ duration) in the
VideoStat record and expose it as a selectable chart Y-axis metric
("Transcript coverage") — e.g. average coverage by channel surfaces
channels with truncated/partial transcripts. Computed for free in
buildStats from the cue list already in memory; null when there's no
transcript or no usable duration.
Bumps STATS_SCHEMA_VERSION 4 -> 5 (clears the statsByPath cache; requires a
one-time build:index && build:stats to back-fill existing videos).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
8 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/common/components/charts/ChartConfigEditor.tsx b/common/components/charts/ChartConfigEditor.tsx
@@ -36,6 +36,7 @@ const METRIC_FIELDS: { value: MetricField; label: string }[] = [
{ value: "commentCount", label: "Comments" },
{ value: "channelFollowerCount", label: "Followers" },
{ value: "cueCount", label: "Cue count" },
+ { value: "coverage", label: "Transcript coverage" },
];
export function ChartConfigEditor({
diff --git a/common/components/charts/ChartShapeControls.tsx b/common/components/charts/ChartShapeControls.tsx
@@ -26,6 +26,7 @@ const METRIC_FIELDS: { value: MetricField; label: string }[] = [
{ value: "commentCount", label: "Comments" },
{ value: "channelFollowerCount", label: "Followers" },
{ value: "cueCount", label: "Cue count" },
+ { value: "coverage", label: "Transcript coverage" },
];
// Map a gallery template's ChartConfig onto the search-page chart shape. The
diff --git a/common/controller/buildStats.ts b/common/controller/buildStats.ts
Binary files differ.
diff --git a/common/lib/chartConfig.ts b/common/lib/chartConfig.ts
@@ -39,7 +39,8 @@ export type MetricField =
| "likeCount"
| "commentCount"
| "channelFollowerCount"
- | "cueCount";
+ | "cueCount"
+ | "coverage";
export type YMetric =
| { agg: "count" }
@@ -169,6 +170,7 @@ const METRIC_FIELD_LABEL: Record<MetricField, string> = {
commentCount: "comments",
channelFollowerCount: "followers",
cueCount: "cues",
+ coverage: "transcript coverage",
};
const AGG_LABEL: Record<Exclude<MetricAgg, "count">, string> = {
diff --git a/common/lib/chartShare.ts b/common/lib/chartShare.ts
@@ -138,7 +138,7 @@ function decY(raw: unknown): YMetric {
const agg = oneOf(r.a, ["sum", "avg", "median", "max", "min"], "sum");
const field = oneOf(
r.f,
- ["duration", "viewCount", "likeCount", "commentCount", "channelFollowerCount", "cueCount"],
+ ["duration", "viewCount", "likeCount", "commentCount", "channelFollowerCount", "cueCount", "coverage"],
"viewCount",
);
return { agg, field };
diff --git a/common/lib/stats.ts b/common/lib/stats.ts
@@ -2,7 +2,7 @@ import type { Platform } from "./platform";
// Bumping this invalidates the LMDB `statsByPath` incremental cache and forces
// a full re-extraction (e.g. when a new field is added below).
-export const STATS_SCHEMA_VERSION = 4;
+export const STATS_SCHEMA_VERSION = 5;
export const STATS_MANIFEST_VERSION = 1;
// Visibility of a video on its source platform, derived from the availability
@@ -52,6 +52,10 @@ export type VideoStat = {
status: VideoStatus;
hasTranscript: boolean;
cueCount: number | null;
+ // Fraction of the video's duration covered by the transcript (last cue end ÷
+ // duration). null when there's no transcript or no usable duration. Values
+ // well below 1 flag a truncated download (see lib/transcriptCoverage).
+ coverage: number | null;
};
export type StatsChannelEntry = {
diff --git a/common/lib/transcripts-server.ts b/common/lib/transcripts-server.ts
@@ -139,6 +139,8 @@ export function summarizeStats(
// source is resolvable.
downloadedDate: string | null;
transcribedDate: string | null;
+ // Last-cue-end ÷ duration; null when not computable. See transcriptCoverage.
+ coverage: number | null;
},
): VideoStat {
const base = summarize(channelSlug, videoDir, meta, configName);
@@ -168,6 +170,7 @@ export function summarizeStats(
status: transcript.status,
hasTranscript: transcript.hasTranscript,
cueCount: transcript.cueCount,
+ coverage: transcript.coverage,
};
}
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog
+## [Unreleased]
+- **Transcript coverage is now a chart metric.** Each video's stats record carries a `coverage` fraction (transcript's last cue end ÷ video duration), selectable as a Y-axis metric ("Transcript coverage") in the chart options — e.g. average coverage by channel surfaces channels with truncated/partial transcripts. Coverage is `null` for videos with no transcript or no usable duration. (Requires the one-time `build:index` + `build:stats` rebuild on the bumped stats schema; the field is back-filled for every transcribed video from its `transcript.cues.json`.)
+
## [0.4.1] - 2026-06-22
- **New "Content added" charts: track when videos joined the library, not just when they were uploaded.** The time axis of any chart can now bin on **when we downloaded** or **transcribed** a video, in addition to its upload date — pick the **Date field** in the chart options. The dashboard ships a cumulative **Library growth (added)** chart by default (grouped by channel), and the chart gallery gains a **Content added** group with cumulative download/transcription curves and a per-month stacked-by-channel breakdown. Like every chart, these filter by channel and export to CSV/PNG. (Populating acquisition dates for existing videos requires a one-time data rebuild — `build:index` + `build:stats` re-extract on the bumped stats schema; videos added before this release fall back to file timestamps.)