Docs/Skills and code/src/
src/topic-research/export-cohort.js
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.