Docs/Skills and code/skill winning-video-teardown

.claude/skills/winning-video-teardown/scripts/prepare.js

Code125 lines7.6 KBSkill: winning-video-teardown

What the file says about itself

Builds the case files for a teardown run: one folder per video with metadata + transcript, the thumbnail downloaded so analysts can look at it, and one task file per analyst.

#!/usr/bin/env node
// Builds the case files for a teardown run: one folder per video with metadata + transcript, the thumbnail
// downloaded so analysts can look at it, and one task file per analyst.
//
//   node prepare.js --cohort <dir-of-transcript-txts> [--cohort <another-dir>] --out <runDir>
//
// Transcript files use the layout written by the series-analysis `cohorts` stage:
//   === VIDEO === (Title, URL, Video ID, Channel, Published, Duration, Likes, Description)
//   === PERFORMANCE === (Views, Views per day, Performance score, Outlier vs channel)
//   === CHANNEL === (Subscribers, Channel median views)
//   === TRANSCRIPT === (Language, then "[mm:ss] text" paragraphs)

const fs = require('fs');
const path = require('path');

const argv = process.argv.slice(2);
const cohorts = [];
let out = null;
let perfUnknown = false; // --perf-unknown: a topic cohort, not a set of proven winners (the videos were picked by topic, not by outperformance)
for (let i = 0; i < argv.length; i++) {
  if (argv[i] === '--cohort') cohorts.push(path.resolve(argv[++i]));
  else if (argv[i] === '--out') out = path.resolve(argv[++i]);
  else if (argv[i] === '--perf-unknown') perfUnknown = true;
}
if (!cohorts.length || !out) {
  console.error('usage: node prepare.js --cohort <dir> [--cohort <dir> ...] --out <runDir>');
  process.exit(1);
}

const SKILL = path.resolve(__dirname, '..');
const field = (text, label) => (text.match(new RegExp('^' + label + ':\\s*(.*)$', 'm')) || [])[1]?.trim() ?? null;
const num = (s) => { const m = String(s ?? '').replace(/,/g, '').match(/-?\d+(\.\d+)?/); return m ? Number(m[0]) : null; };

function parse(file) {
  const raw = fs.readFileSync(file, 'utf8');
  const [head, body] = raw.split('=== TRANSCRIPT ===');
  if (!body) throw new Error(`${file}: no "=== TRANSCRIPT ===" section`);
  const dur = (field(head, 'Duration') || '').match(/(?:(\d+)h\s*)?(?:(\d+)m\s*)?(\d+)s/);
  return {
    title: field(head, 'Title'), url: field(head, 'URL'), video_id: field(head, 'Video ID'), channel: field(head, 'Channel'),
    published: field(head, 'Published'), likes: num(field(head, 'Likes')), description: field(head, 'Description'),
    views: num(field(head, 'Views')), views_per_day: num(field(head, 'Views per day')),
    performance_score: num(field(head, 'Performance score')), outlier: field(head, 'Outlier vs channel'),
    subscribers: num(field(head, 'Subscribers')), channel_median_views: num(field(head, 'Channel median views \\(recent uploads\\)')),
    language: (body.match(/^Language:\s*(.*)$/m) || [])[1]?.split('[')[0].trim() ?? null,
    duration_sec: dur ? Number(dur[1] || 0) * 3600 + Number(dur[2] || 0) * 60 + Number(dur[3]) : null,
    transcript: raw.slice(raw.indexOf('=== TRANSCRIPT ===')),
  };
}

async function thumbnail(id, dest) {
  for (const name of ['maxresdefault', 'hqdefault']) {
    try {
      const res = await fetch(`https://i.ytimg.com/vi/${id}/${name}.jpg`);
      if (!res.ok) continue;
      const buf = Buffer.from(await res.arrayBuffer());
      if (buf.length < 4000) continue; // YouTube serves a tiny grey placeholder when a size is missing
      fs.writeFileSync(dest, buf);
      return name;
    } catch { /* try next size */ }
  }
  return null;
}

(async () => {
  fs.mkdirSync(path.join(out, 'cases'), { recursive: true });
  fs.mkdirSync(path.join(out, 'thumbs'), { recursive: true });
  fs.mkdirSync(path.join(out, 'analyses'), { recursive: true });
  fs.mkdirSync(path.join(out, 'tasks'), { recursive: true });

  const manifest = { created_at: new Date().toISOString(), cases: [] };
  let n = 0;
  for (const dir of cohorts) {
    const label = path.basename(dir).replace(/^cohort-/, '');
    const files = fs.readdirSync(dir).filter((f) => f.endsWith('.txt') && !f.startsWith('_')).sort();
    for (const f of files) {
      const m = parse(path.join(dir, f));
      const cid = `V${String(++n).padStart(2, '0')}`;
      const caseDir = path.join(out, 'cases', cid);
      fs.mkdirSync(caseDir, { recursive: true });
      const thumbPath = path.join(out, 'thumbs', `${m.video_id}.jpg`);
      const size = await thumbnail(m.video_id, thumbPath);

      const meta = [
        `# Case ${cid}`, '',
        `Title: ${m.title}`, `Channel: ${m.channel} (${m.subscribers != null ? m.subscribers.toLocaleString('en-US') + ' subscribers' : 'subscribers unknown'})`,
        `URL: ${m.url}`, `Published: ${m.published}`, `Duration: ${Math.floor(m.duration_sec / 60)}m ${m.duration_sec % 60}s`, `Spoken language: ${m.language}`,
        `Views: ${m.views?.toLocaleString('en-US')} | Views/day: ${m.views_per_day} | Likes: ${m.likes?.toLocaleString('en-US')}`,
        `Channel's typical recent upload: ${m.channel_median_views?.toLocaleString('en-US') ?? 'unknown'} views | This video vs that: ${m.outlier ?? 'n/a'}`,
        `Group: ${label} (${label === 'new' ? 'recent, 30-420 days old' : label === 'established' ? 'older, 900+ days, still earning' : label})`,
        `Description (as uploaded): ${m.description}`, '',
        perfUnknown
          ? 'This video was picked because it is about the topic, not because it outperformed: its performance relative to its channel is unknown, and some videos in this set have small reach. Explain how it works and where it is weak; do not assume it is a winner or that every choice in it is good.'
          : 'This video performed far above the norm for its channel and genre, which is why it is being studied. Explain how it works; do not assume every choice in it is good.', '',
      ].join('\n');
      fs.writeFileSync(path.join(caseDir, 'case.md'), meta, 'utf8');
      fs.writeFileSync(path.join(caseDir, 'transcript.txt'), m.transcript, 'utf8');

      const outJson = path.join(out, 'analyses', `${cid}.json`);
      const validator = `node "${path.join(__dirname, 'validate.js')}" --file "${outJson}" --case "${caseDir}" --strict 1`;
      const task = [
        `# Teardown task: ${cid}`, '',
        `1. Read the instructions: ${path.join(SKILL, 'references', 'analyst-prompt.md')}`,
        `2. Read the mechanism vocabulary: ${path.join(SKILL, 'references', 'taxonomy.md')}`,
        `3. Read the output schema: ${path.join(SKILL, 'references', 'schema.md')}`,
        `4. Read the case metadata: ${path.join(caseDir, 'case.md')}`,
        size ? `5. Look at the thumbnail (use the Read tool on this image): ${thumbPath}` : '5. No thumbnail could be downloaded for this video; set thumbnail.seen to false.',
        `6. Read the full transcript: ${path.join(caseDir, 'transcript.txt')}`,
        `7. Write your analysis JSON to: ${outJson}`,
        `8. Validate with Bash (this is the only command you may run): ${validator}`,
        '   Fix the JSON and rewrite it until the validator prints OK.', '',
        'Read only the files listed above. Do not list directories or open other cases.', '',
      ].join('\n');
      const taskPath = path.join(out, 'tasks', `analyze_${cid}.md`);
      fs.writeFileSync(taskPath, task, 'utf8');
      manifest.cases.push({ cid, group: label, video_id: m.video_id, title: m.title, channel: m.channel, url: m.url, views: m.views,
        performance_score: m.performance_score, outlier: m.outlier, subscribers: m.subscribers, duration_sec: m.duration_sec, language: m.language,
        thumbnail: size ? thumbPath : null, task: taskPath, output: outJson, case_dir: caseDir });
      console.log(`${cid} [${label}] ${m.title.slice(0, 60)} | thumbnail: ${size ?? 'none'}`);
    }
  }
  fs.writeFileSync(path.join(out, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
  console.log(`\n${manifest.cases.length} cases in ${out}\nTask files: ${path.join(out, 'tasks')}`);
})();

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.