Docs/Skills and code/src/
src/series-analysis/cohorts.js
What the file says about itself
Cohort selection and transcript-quality checks for the script study. Two cohorts are drawn from the same ranked pool with the same rule, differing only in video age: "new" (recent, still proving itself) and "established" (old and still…
/**
* Cohort selection and transcript-quality checks for the script study.
* Two cohorts are drawn from the same ranked pool with the same rule, differing only in video age:
* "new" (recent, still proving itself) and "established" (old and still earning).
*/
export function selectCohortCandidates(scored, spec, which) {
const c = spec.cohorts;
const age = c[which];
const [minDur, maxDur] = c.durationMinutes.map(m => m * 60);
const pool = scored.filter(r =>
r.learn_score != null && r.age_days != null &&
r.age_days >= (age.minAgeDays ?? 0) && r.age_days <= (age.maxAgeDays ?? Infinity) &&
r.views >= age.minViews && r.fit >= c.minFit &&
r.duration_seconds >= minDur && r.duration_seconds <= maxDur &&
r.format_flags.length === 0 && !c.exclude?.[r.video_id]
).sort((a, b) => b.performance - a.performance);
// Variety caps so one channel's house style or one company's story can't fill the cohort.
const byChannel = new Map(), bySubject = new Map(), out = [];
for (const r of pool) {
const ch = byChannel.get(r.channel_key) ?? 0, sb = r.subject ? bySubject.get(r.subject) ?? 0 : 0;
if (ch >= c.maxPerChannel || sb >= c.maxPerSubject) continue;
byChannel.set(r.channel_key, ch + 1);
if (r.subject) bySubject.set(r.subject, sb + 1);
out.push(r);
}
return out;
}
/**
* Is this transcript a faithful, complete capture of the spoken script?
* Auto-captions can be truncated, sparse (music-heavy or silent stretches) or in the wrong track, and any of those
* would make every downstream script measurement meaningless, so a failing transcript is replaced, not analysed.
*/
export function checkTranscript(segments, durationSeconds) {
const words = segments.reduce((n, s) => n + String(s.text).split(/\s+/).filter(Boolean).length, 0);
const last = segments.length ? segments[segments.length - 1] : null;
const covered = last ? (last.start + (last.duration || 0)) : 0;
const coverage = durationSeconds ? covered / durationSeconds : null;
const wpm = durationSeconds ? words / (durationSeconds / 60) : null;
const firstStart = segments[0]?.start ?? null;
const problems = [];
if (coverage != null && coverage < 0.85) problems.push(`captions end at ${(coverage * 100).toFixed(0)}% of the video`);
// Narrated explainers run ~110-200 words/min in Hindi and English; much lower means music-heavy, garbled or partial captions.
if (wpm != null && wpm < 95) problems.push(`only ${wpm.toFixed(0)} words/min (sparse captions)`);
if (wpm != null && wpm > 280) problems.push(`${wpm.toFixed(0)} words/min (duplicated or rolling captions?)`);
if (firstStart != null && firstStart > 45) problems.push(`first caption at ${firstStart.toFixed(0)}s (hook missing)`);
const text = segments.map(s => s.text).join(" ");
const unreadable = (text.match(/[�]/g) ?? []).length;
if (unreadable > 5) problems.push(`${unreadable} undecodable characters`);
// Subscribe-begging loops mean the captions are mostly filler, not the script.
const begging = (text.match(/subscribe|सब्सक्राइब|सबस्क्राइब/gi) ?? []).length;
if (begging > 6) problems.push(`${begging} "subscribe" mentions (filler or caption loop)`);
const tokens = text.toLowerCase().split(/\s+/).filter(Boolean);
const topShare = tokens.length ? Math.max(...Object.values(tokens.reduce((m, t) => ((m[t] = (m[t] ?? 0) + 1), m), {}))) / tokens.length : 0;
if (topShare > 0.08) problems.push(`one token is ${(topShare * 100).toFixed(0)}% of all words (caption loop)`);
return { ok: problems.length === 0, problems, words, wpm, coverage, segments: segments.length };
}
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.