Docs/Skills and code/src/

src/topic-research/export-cohort.js

Code56 lines2.7 KBsrc/

What the file says about itself

Usage: node src/topic-research/export-cohort.js <research dir> <out cohort dir> <videoId> [<videoId> ...] Writes one transcript .txt per video in the layout the winning-video-teardown skill reads (=== VIDEO === / === PERFORMANCE === / ===…

Mentioned on: page 12

#!/usr/bin/env node
// Usage: node src/topic-research/export-cohort.js <research dir> <out cohort dir> <videoId> [<videoId> ...]
// Writes one transcript .txt per video in the layout the winning-video-teardown skill reads
// (=== VIDEO === / === PERFORMANCE === / === CHANNEL === / === TRANSCRIPT ===), fetching any transcript not cached.
import fs from "node:fs";
import path from "node:path";
import { fetchOriginalTranscript, formatTranscriptText } from "../transcript-fetch.js";
import { readJson, writeJson } from "./util.js";

const [dir, outDir, ...ids] = process.argv.slice(2);
const search = readJson(path.join(dir, "search.json"));
const csv = fs.readFileSync(path.join(dir, "all-videos.csv"), "utf8").replace(/^/, "").trim().split("\n");
const header = csv[0].match(/"(?:[^"]|"")*"/g).map(x => x.slice(1, -1));
const rows = new Map(csv.slice(1).map(line => {
  const f = line.match(/"(?:[^"]|"")*"/g).map(x => x.slice(1, -1).replace(/""/g, '"'));
  return [f[0], Object.fromEntries(header.map((h, i) => [h, f[i]]))];
}));

const duration = s => `${Math.floor(s / 60)}m ${s % 60}s`;
const ageDays = text => {
  const m = String(text).match(/(\d+)\s*(day|week|mo|yr|year|month)/i);
  if (!m) return null;
  const unit = { day: 1, week: 7, mo: 30, month: 30, yr: 365, year: 365 }[m[2].toLowerCase()];
  return Math.max(1, +m[1] * unit);
};
const subsNumber = t => {
  const m = String(t).match(/([\d.]+)\s*([KMB])?/i);
  return m ? Math.round(+m[1] * ({ K: 1e3, M: 1e6, B: 1e9 }[(m[2] ?? "").toUpperCase()] ?? 1)) : "";
};

fs.mkdirSync(outDir, { recursive: true });
for (const id of ids) {
  const cache = path.join(dir, "transcripts", `${id}.json`);
  let t = readJson(cache);
  if (!t) { t = await fetchOriginalTranscript(id); writeJson(cache, t); }
  const c = search.candidates.find(x => x.video_id === id);
  const r = rows.get(id);
  const days = ageDays(c.published);
  const text = [
    "=== VIDEO ===",
    `Title: ${c.title}`, `URL: ${c.url}`, `Video ID: ${id}`, `Channel: ${c.channel}`,
    `Published: ${c.published} (YouTube's rounded age text)`, `Duration: ${duration(c.duration_seconds)}`,
    "Likes: unknown", "Description: (not captured)", "",
    "=== PERFORMANCE ===",
    `Views: ${c.views}`, `Views per day: ${days ? Math.round(c.views / days) : "unknown"}`,
    "Performance score: n/a", "Outlier vs channel: n/a", "",
    "=== CHANNEL ===",
    `Subscribers: ${subsNumber(r.channel_subscribers)}`, "Channel median views (recent uploads): unknown", "",
    "=== TRANSCRIPT ===",
    `Language: ${t.lang} (${t.languageName ?? t.lang}; auto-captions, ${t.detection})`, "",
    formatTranscriptText(t.segments), ""
  ].join("\n");
  fs.writeFileSync(path.join(outDir, `${id}.txt`), text, "utf8");
  console.log(`${id} ${c.channel} ${t.segments.length} segments`);
}

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.