Docs/Skills and code/src/

src/series-analysis/cohorts.js

Code60 lines3.6 KBsrc/

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.