Docs/Skills and code/src/

src/transcript-sampling.js

Code104 lines4.1 KBsrc/

import path from "node:path";

export const TRANSCRIPT_ROOT = path.resolve(process.cwd(), "transcripts");
export const SAMPLE_FILE_PATTERN = /^(best|median|worst)-\d+_.+\.txt$/;

// A video this young hasn't had time to accumulate views, so it can't fairly be called "worst".
export const MIN_AGE_DAYS = 14;
// Views/day divides by at least this many days so a 3-week-old hit isn't ranked as if it were a 1-day-old rocket.
export const AGE_FLOOR_DAYS = 30;
const MIN_ELIGIBLE = 10;

/** Windows-safe folder name for a channel, keeping non-Latin letters (Hindi, etc.) intact. */
export function channelFolderName(name) {
  const cleaned = String(name ?? "")
    .replace(/[<>:"/\\|?*\u0000-\u001f]/g, "")
    .replace(/\s+/g, " ")
    .trim()
    .replace(/[. ]+$/, "")
    .slice(0, 80);
  return cleaned || "channel";
}

export function seededRandom(seed) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function shuffle(items, rng) {
  const out = [...items];
  for (let i = out.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [out[i], out[j]] = [out[j], out[i]];
  }
  return out;
}

function median(sorted) {
  if (!sorted.length) return null;
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

/**
 * Ranks a channel's videos by views per day (age floored, see AGE_FLOOR_DAYS), which is fairer than raw
 * lifetime views: raw views would make the newest uploads look like the channel's worst performers.
 *
 * Returns ordered candidate lists per bucket so a caller can fall through to the next candidate when a
 * video has no transcript:
 *   best   - highest views/day first
 *   worst  - lowest views/day first
 *   median - random picks from the middle 30% of the ranking, then the rest by distance from the middle
 */
export function rankAndBucket(videos, { now = Date.now(), rng = Math.random } = {}) {
  const dated = videos
    .filter(v => Number.isFinite(Number(v.view_count)) && v.published_at_normalized && Number.isFinite(Date.parse(v.published_at_normalized)))
    .map(v => ({ ...v, view_count: Number(v.view_count), age_days: (now - Date.parse(v.published_at_normalized)) / 86400000 }));

  let pool = dated.filter(v => v.age_days >= MIN_AGE_DAYS);
  let usedFallbackPool = false;
  if (pool.length < MIN_ELIGIBLE) { pool = dated; usedFallbackPool = true; }

  const ranked = pool
    .map(v => ({ ...v, views_per_day: v.view_count / Math.max(v.age_days, AGE_FLOOR_DAYS) }))
    .sort((a, b) => b.views_per_day - a.views_per_day || b.view_count - a.view_count);

  const medianViews = median(ranked.map(v => v.view_count).sort((a, b) => a - b));
  const medianVpd = median(ranked.map(v => v.views_per_day).sort((a, b) => a - b));
  const total = ranked.length;
  ranked.forEach((v, i) => {
    v.rank = i + 1;
    v.rank_total = total;
    v.percentile_from_top = total > 1 ? Math.round((i / (total - 1)) * 1000) / 10 : 0;
    v.vs_median_views = medianViews ? v.view_count / medianViews : null;
    v.vs_median_views_per_day = medianVpd ? v.views_per_day / medianVpd : null;
  });

  const mid = (total - 1) / 2;
  const bandHalf = Math.max(1, Math.round(total * 0.15));
  const inBand = ranked.filter((_, i) => Math.abs(i - mid) <= bandHalf);
  const outOfBand = ranked.filter((_, i) => Math.abs(i - mid) > bandHalf).sort((a, b) => Math.abs(a.rank - 1 - mid) - Math.abs(b.rank - 1 - mid));

  return {
    stats: {
      eligible: total,
      excluded: videos.length - total,
      median_views: medianViews,
      median_views_per_day: medianVpd,
      mean_views: total ? ranked.reduce((s, v) => s + v.view_count, 0) / total : null,
      used_fallback_pool: usedFallbackPool,
      min_age_days: MIN_AGE_DAYS,
      age_floor_days: AGE_FLOOR_DAYS
    },
    best: ranked,
    worst: [...ranked].reverse(),
    median: [...shuffle(inBand, rng), ...outOfBand]
  };
}

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.