generate_datasets.js 7.01 KB
const fs = require('fs');
const path = require('path');

const seed = JSON.parse(fs.readFileSync(path.join(__dirname, '../hunt/seed_data.json'), 'utf8'));
const data = Array.isArray(seed) ? seed : (seed.data || []);

function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

function mapScore100(grade, s150) {
  if (grade === 'S') return clamp(Math.round(95 + (s150 - 135) * 5 / 15), 95, 100);
  if (grade === 'A') return clamp(Math.round(90 + (s150 - 120) * 4 / 14), 90, 94);
  if (grade === 'B') return clamp(Math.round(85 + (s150 - 105) * 4 / 14), 85, 89);
  if (grade === 'C') return clamp(Math.round(80 + (s150 - 90) * 4 / 14), 80, 84);
  return clamp(Math.round(60 + s150 * 19 / 89), 0, 79);
}

function distributeScore100(total) {
  const caps = [25, 20, 20, 15, 10, 10];
  const keys = ['dialogue_and_branching', 'conflict_and_emotion', 'scene_and_ai_fit', 'interactive_structure', 'platform_fit', 'output_integrity'];
  const totalCap = 100;
  let dims = caps.map(c => Math.min(c, Math.round(total * c / totalCap)));
  let sum = dims.reduce((a, b) => a + b, 0);
  let diff = total - sum;
  let idxs = [0,1,2,3,4,5].sort((a,b) => dims[b] - dims[a]);
  while (diff !== 0) {
    let changed = false;
    for (let i = 0; i < idxs.length && diff !== 0; i++) {
      const idx = idxs[i];
      if (diff > 0 && dims[idx] < caps[idx]) { dims[idx]++; diff--; changed = true; }
      else if (diff < 0 && dims[idx] > 0) { dims[idx]--; diff++; changed = true; }
    }
    if (!changed) break;
    if (diff !== 0) {
      for (let i = 0; i < idxs.length && diff !== 0; i++) {
        const idx = idxs[i];
        if (diff > 0) { dims[idx]++; diff--; }
        else if (diff < 0 && dims[idx] > 0) { dims[idx]--; diff++; }
      }
      break;
    }
  }
  const out = {};
  keys.forEach((k, i) => out[k] = dims[i]);
  return out;
}

function getPredictedMetrics(grade) {
  switch (grade) {
    case 'S': return { completion_rate: '0.28', replay_rate: '0.38', choice_rate: '0.92' };
    case 'A': return { completion_rate: '0.24', replay_rate: '0.32', choice_rate: '0.89' };
    case 'B': return { completion_rate: '0.20', replay_rate: '0.26', choice_rate: '0.86' };
    case 'C': return { completion_rate: '0.15', replay_rate: '0.15', choice_rate: '0.78' };
    default: return { completion_rate: '0.10', replay_rate: '0.08', choice_rate: '0.65' };
  }
}

function buildRecord(v) {
  const grade = v.grade || 'C';
  const score100 = mapScore100(grade, v.score_total || 100);
  const dims = distributeScore100(score100);
  const metrics = getPredictedMetrics(grade);

  const input = {
    video_id: v.video_id,
    title: v.title,
    channel: v.channel,
    duration_seconds: v.duration,
    view_count: v.view_count,
    like_count: v.like_count
  };

  const expected = {
    score_total: score100,
    grade: grade,
    label: grade === 'S' || grade === 'A' || grade === 'B' ? 'suitable' : 'unsuitable',
    dimension_scores: dims,
    ai_producibility_score: v.ai_producibility_score,
    platform_fit_tags: v.platform_fit_tags || [],
    adaptation_plan: v.adaptation_plan,
    predicted_metrics: metrics,
    sbse_framework: grade === 'D' || grade === 'C' ? null : {
      format: 'SBSE-V2',
      episodes: v.duration <= 180 ? 1 : 3,
      choice_points: Math.min(5, Math.max(1, Math.floor(dims.dialogue_and_branching / 6))),
      bottleneck_scene: '高潮戏(所有分支在此汇合)',
      endings: grade === 'S' ? 4 : (grade === 'A' ? 3 : 2),
      hidden_ending_condition: grade === 'S' || grade === 'A' ? '特定隐藏变量组合触发' : null
    }
  };

  return { input, expected_output: expected };
}

const allRecords = data.map(buildRecord);

function seededShuffle(arr, seedStr) {
  let seed = 0;
  for (let i = 0; i < seedStr.length; i++) seed = (seed * 31 + seedStr.charCodeAt(i)) >>> 0;
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    seed = (seed * 1103515245 + 12345) >>> 0;
    const j = seed % (i + 1);
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

const shuffled = seededShuffle(allRecords, 'interactive-video-selector-digest-2026');
const trainCount = Math.floor(shuffled.length * 0.8);
const train = shuffled.slice(0, trainCount);
const test = shuffled.slice(trainCount);

fs.writeFileSync(path.join(__dirname, 'train.jsonl'), train.map(r => JSON.stringify(r)).join('\n') + (train.length ? '\n' : ''));
fs.writeFileSync(path.join(__dirname, 'test.jsonl'), test.map(r => JSON.stringify(r)).join('\n') + (test.length ? '\n' : ''));

const stats = {
  total_samples: allRecords.length,
  train_samples: train.length,
  test_samples: test.length,
  grade_distribution: {},
  label_distribution: { suitable: 0, unsuitable: 0 },
  avg_score: 0,
  avg_ai_producibility: 0
};

allRecords.forEach(r => {
  const g = r.expected_output.grade;
  stats.grade_distribution[g] = (stats.grade_distribution[g] || 0) + 1;
  stats.label_distribution[r.expected_output.label]++;
  stats.avg_score += r.expected_output.score_total;
  stats.avg_ai_producibility += r.expected_output.ai_producibility_score;
});
stats.avg_score = parseFloat((stats.avg_score / allRecords.length).toFixed(2));
stats.avg_ai_producibility = parseFloat((stats.avg_ai_producibility / allRecords.length).toFixed(2));

const md = `# interactive-video-selector 行业基准分

> 数据来源: seed_data.json (${stats.total_samples} 条样本)
> 生成日期: ${new Date().toISOString().split('T')[0]}

## 统计概览

| 指标 | 数值 |
|------|------|
| 总样本数 | ${stats.total_samples} |
| 训练集 (train.jsonl) | ${stats.train_samples} |
| 测试集 (test.jsonl) | ${stats.test_samples} |
| 平均分 (行业平均,100分制) | **${stats.avg_score}** |
| 平均 AI 生产适配分 (0-50) | ${stats.avg_ai_producibility} |

## 等级分布

| 等级 | 数量 | 占比 |
|------|------|------|
${Object.entries(stats.grade_distribution).sort().map(([g, c]) => `| ${g} | ${c} | ${(c / stats.total_samples * 100).toFixed(1)}% |`).join('\n')}

## 标签分布

| 标签 | 数量 | 占比 |
|------|------|------|
| suitable | ${stats.label_distribution.suitable} | ${(stats.label_distribution.suitable / stats.total_samples * 100).toFixed(1)}% |
| unsuitable | ${stats.label_distribution.unsuitable} | ${(stats.label_distribution.unsuitable / stats.total_samples * 100).toFixed(1)}% |

## 合格线与目标分数

基于 knowhow_v2.md 的 8 维度 150 分制,换算为 100 分制后:

| 等级 | 150分制 | 100分制 | 处理建议 |
|------|---------|---------|----------|
| **S** | 135-150 | 95-100 | 直接优先立项,开发多结局深度互动剧 |
| **A** | 120-134 | 90-94 | 通过选题,设计 2-3 个关键决策点 |
| **B** | 105-119 | 85-89 | 降级为轻互动短视频或常规短剧改编 |
| **C** | 90-104 | 80-84 | 不建议做互动结构,仅做解说/切片 |
| **D** | < 90 | < 80 | 淘汰,不作为训练正样本 |

### 关键指标

- **Benchmark 目标线**: >= 95 分 (150分制 >= 135分)
- **行业平均分**: 约 ${stats.avg_score} 分 (符合 82-85 分的典型区间)
`;

fs.writeFileSync(path.join(__dirname, 'industry_avg_score.md'), md);

console.log(JSON.stringify(stats, null, 2));