generate_synthetic_data.js 13.6 KB
const fs = require('fs');
const path = require('path');

// Reddit 数据分布模式(基于真实 Reddit 统计)
const SUBREDDIT_CONFIG = {
  'AmITheAsshole': {
    weight: 0.30, // 30% 数据
    scoreRange: [100, 25000],
    commentRange: [50, 5000],
    upvoteRatioRange: [0.65, 0.98],
    conflictTypes: ['family_conflict', 'relationship_crisis', 'workplace_drama', 'inheritance_dispute', 'wedding_drama'],
    dramaKeywords: ['AITA', 'boyfriend', 'girlfriend', 'husband', 'wife', 'mother', 'father', 'sister', 'brother', 'inlaws']
  },
  'relationship_advice': {
    weight: 0.25,
    scoreRange: [50, 15000],
    commentRange: [30, 3000],
    upvoteRatioRange: [0.70, 0.95],
    conflictTypes: ['relationship_crisis', 'breakup_drama', 'trust_issues', 'communication_breakdown'],
    dramaKeywords: ['cheating', 'trust', 'breakup', 'divorce', 'relationship', 'partner', 'communicate']
  },
  'funny': {
    weight: 0.20,
    scoreRange: [500, 50000],
    commentRange: [10, 800],
    upvoteRatioRange: [0.80, 0.99],
    conflictTypes: ['absurd_situation', 'social_awkwardness', 'unexpected_twist'],
    dramaKeywords: ['hilarious', 'lol', 'funny', 'unexpected', 'awkward']
  },
  'jokes': {
    weight: 0.15,
    scoreRange: [200, 30000],
    commentRange: [5, 500],
    upvoteRatioRange: [0.75, 0.98],
    conflictTypes: ['wordplay', 'absurd_situation', 'dark_humor'],
    dramaKeywords: ['joke', 'punchline', 'setup', 'humor', 'laugh']
  },
  'StandUpComedy': {
    weight: 0.10,
    scoreRange: [100, 10000],
    commentRange: [10, 600],
    upvoteRatioRange: [0.72, 0.96],
    conflictTypes: ['observational_humor', 'social_commentary', 'self_deprecation'],
    dramaKeywords: ['comedy', 'standup', 'joke', 'routine', 'audience', 'stage']
  }
};

// 生成随机整数
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// 生成长尾分布的随机数(更真实)
function randomLongTail(min, max, exponent = 2) {
  const u = Math.random();
  const scaled = Math.pow(u, exponent);
  return Math.floor(min + scaled * (max - min));
}

// 生成随机日期(最近30天)
function randomDate() {
  const daysAgo = randomInt(0, 30);
  const date = new Date();
  date.setDate(date.getDate() - daysAgo);
  return date.toISOString();
}

// 合成标题模板
const TITLE_TEMPLATES = {
  'AmITheAsshole': [
    'AITA for {action} when {context}?',
    'AITA for refusing to {action}?',
    'My {relation} wants me to {action}, AITA for saying no?',
    'AITA for being upset about {situation}?',
    'I {action} and now my {relation} is mad at me, AITA?'
  ],
  'relationship_advice': [
    'My {relation} {action} and I don\'t know what to do',
    'Should I {action} after {situation}?',
    'I found out my {relation} {action}, how do I proceed?',
    'My {relation} and I keep fighting about {issue}',
    'Is it normal for {relation} to {action}?'
  ],
  'funny': [
    'When you {situation}...',
    'My {relation} tried to {action} and failed spectacularly',
    'This {thing} just made my day',
    '{situation} happened today',
    'Found this gem: {description}'
  ],
  'jokes': [
    'Why did the {noun} {verb}?',
    'What do you call a {description}?',
    'My {relation} told me this one: {setup}',
    'I heard {person} say: {setup}',
    'Classic: {setup}'
  ],
  'StandUpComedy': [
    'On {topic}: "{quote}"',
    'My take on {topic}...',
    'Why is {topic} so {adjective}?',
    'The thing about {topic} nobody talks about',
    'Crowd work gone wrong: {situation}'
  ]
};

const ACTIONS = ['ignoring my calls', 'going through my phone', 'skipping the wedding', 'telling the truth', 'setting boundaries', 'moving out', 'quitting my job', 'confronting them'];
const CONTEXTS = ['my sister asked me to', 'it made me uncomfortable', 'they were being unreasonable', 'I had other plans', 'it was an emergency'];
const RELATIONS = ['boyfriend', 'girlfriend', 'husband', 'wife', 'mother', 'father', 'sister', 'brother', 'boss', 'coworker', 'friend', 'roommate'];
const SITUATIONS = ['realized I was wrong', 'spilled coffee everywhere', 'forgot my keys', 'got caught in the rain', 'tried to cook', 'attempted DIY'];
const ISSUES = ['money', 'time management', 'household chores', 'family events', 'work-life balance', 'trust', 'communication'];
const NOUNS = ['chicken', 'programmer', 'doctor', 'teacher', 'penguin', 'lawyer', 'actor', 'chef'];
const VERBS = ['cross the road', 'get fired', 'get promoted', 'fall down', 'start laughing', 'leave early'];
const TOPICS = ['dating apps', 'airplane food', 'in-laws', 'grocery shopping', 'social media', 'adulting', 'millennials'];
const ADJECTIVES = ['weird', 'funny', 'relatable', 'confusing', 'frustrating', 'hilarious'];

function generateTitle(subreddit) {
  const templates = TITLE_TEMPLATES[subreddit];
  const template = templates[randomInt(0, templates.length - 1)];
  return template
    .replace(/{action}/g, ACTIONS[randomInt(0, ACTIONS.length - 1)])
    .replace(/{context}/g, CONTEXTS[randomInt(0, CONTEXTS.length - 1)])
    .replace(/{relation}/g, RELATIONS[randomInt(0, RELATIONS.length - 1)])
    .replace(/{situation}/g, SITUATIONS[randomInt(0, SITUATIONS.length - 1)])
    .replace(/{issue}/g, ISSUES[randomInt(0, ISSUES.length - 1)])
    .replace(/{noun}/g, NOUNS[randomInt(0, NOUNS.length - 1)])
    .replace(/{verb}/g, VERBS[randomInt(0, VERBS.length - 1)])
    .replace(/{topic}/g, TOPICS[randomInt(0, TOPICS.length - 1)])
    .replace(/{adjective}/g, ADJECTIVES[randomInt(0, ADJECTIVES.length - 1)])
    .replace(/{thing}/g, ['meme', 'post', 'comment', 'video', 'story'][randomInt(0, 4)])
    .replace(/{description}/g, ['funny story', 'weird encounter', 'unexpected outcome'][randomInt(0, 2)])
    .replace(/{setup}/g, ['the setup...', 'you know what I mean', 'classic punchline'][randomInt(0, 2)])
    .replace(/{quote}/g, ['it gets better', 'trust the process', 'life is weird'][randomInt(0, 2)])
    .replace(/{person}/g, ['my dad', 'a comedian', 'my friend', 'some guy at work'][randomInt(0, 3)]);
}

// 生成正文内容
function generateSelftext(subreddit, conflictType) {
  const lengths = randomInt(100, 2000);
  const paragraphs = Math.ceil(lengths / 300);

  const dramaElements = {
    protagonist: RELATIONS[randomInt(0, RELATIONS.length - 1)],
    antagonist: RELATIONS[randomInt(0, RELATIONS.length - 1)],
    conflict_type: conflictType,
    key_quotes: [],
    dramatic_moments: []
  };

  // 生成关键台词
  const quoteCount = randomInt(1, 4);
  for (let i = 0; i < quoteCount; i++) {
    dramaElements.key_quotes.push({
      text: `"${['You never listen!', 'That\'s not fair!', 'I can\'t believe you!', 'Why would you do that?'][randomInt(0, 3)]}"`,
      speaker: dramaElements.protagonist,
      context: 'confrontation'
    });
  }

  // 生成戏剧时刻
  const momentCount = randomInt(1, 3);
  for (let i = 0; i < momentCount; i++) {
    dramaElements.dramatic_moments.push({
      type: ['reveal', 'confrontation', 'twist', 'climax'][randomInt(0, 3)],
      description: 'A pivotal moment in the narrative',
      intensity: randomInt(1, 10)
    });
  }

  const contentTypes = {
    'AmITheAsshole': `I (M${randomInt(18, 45)}) have been dealing with a situation involving my ${dramaElements.antagonist}. ` +
      `It all started when ${['I made a decision', 'something happened', 'we had a disagreement'][randomInt(0, 2)]}. ` +
      `The issue is that ${['they\'re upset with me', 'I feel guilty', 'we can\'t agree'][randomInt(0, 2)]}. ` +
      `My ${dramaElements.antagonist} thinks I should ${['apologize', 'change my behavior', 'do things differently'][randomInt(0, 2)]}, ` +
      `but I feel like ${['I\'m in the right', 'they\'re overreacting', 'we need to compromise'][randomInt(0, 2)]}. ` +
      `${'Edit: Thanks for all the comments!' }`,
    'relationship_advice': `I've been with my ${dramaElements.antagonist} for ${randomInt(1, 10)} years. ` +
      `Recently, ${['we\'ve been fighting a lot', 'I discovered something', 'things have changed'][randomInt(0, 2)]}. ` +
      `I\'m not sure if ${['I\'m overreacting', 'this is a red flag', 'we can work through this'][randomInt(0, 2)]}. ` +
      `Should I ${['talk to them', 'end things', 'seek counseling', 'give it time'][randomInt(0, 3)]}?`,
    'funny': `So this happened today... ${SITUATIONS[randomInt(0, SITUATIONS.length - 1)]} and it was absolutely hilarious. ` +
      `${['Everyone was staring', 'I wanted to disappear', 'Couldn\'t stop laughing'][randomInt(0, 2)]}!`,
    'jokes': `Why did the ${NOUNS[randomInt(0, NOUNS.length - 1)]} ${VERBS[randomInt(0, VERBS.length - 1)]}?\n\n` +
      `Because ${['it wanted to', 'it had to', 'someone told it to', 'that\'s what they do'][randomInt(0, 3)]}!`,
    'StandUpComedy': `Working on new material about ${TOPICS[randomInt(0, TOPICS.length - 1)]}. ` +
      `The crowd seemed to love it when I said: "${['You ever notice...', 'Why is it that...', 'Isn\'t it weird how...'][randomInt(0, 2)]}" ` +
      `Getting good feedback so far!`
  };

  return {
    text: contentTypes[subreddit],
    drama_elements: dramaElements,
    paragraphs: paragraphs
  };
}

// 计算戏剧性评分
function calculateDramaticScore(post, conflictType, subreddit) {
  // 基于多维度计算戏剧性
  // 1. 互动量得分 (0-4分)
  const engagementScore = Math.min(post.num_comments / 1500, 4) + Math.min(post.score / 8000, 3);

  // 2. 争议度得分 (0-3分) - upvote_ratio 越低越具争议
  const controversyScore = (1 - post.upvote_ratio) * 15;

  // 3. 冲突类型权重 (1-3分)
  const conflictWeights = {
    'family_conflict': 2.5,
    'relationship_crisis': 2.8,
    'workplace_drama': 2.0,
    'breakup_drama': 2.6,
    'trust_issues': 2.4,
    'inheritance_dispute': 2.3,
    'wedding_drama': 2.2,
    'communication_breakdown': 1.8,
    'absurd_situation': 1.5,
    'social_awkwardness': 1.0,
    'unexpected_twist': 1.4,
    'wordplay': 0.8,
    'dark_humor': 1.2,
    'observational_humor': 0.9,
    'social_commentary': 1.3,
    'self_deprecation': 0.8
  };
  const typeScore = conflictWeights[conflictType] || 1.0;

  // 4. Subreddit 基础权重调整
  const subredditBias = {
    'AmITheAsshole': 1.2,
    'relationship_advice': 1.15,
    'funny': 0.8,
    'jokes': 0.6,
    'StandUpComedy': 0.7
  };
  const bias = subredditBias[subreddit] || 1.0;

  const totalScore = (engagementScore + controversyScore + typeScore) * bias;

  // 添加一些随机波动使分布更自然
  const noise = (Math.random() - 0.5) * 1.5;
  const finalScore = Math.max(1.0, Math.min(10.0, totalScore + noise));

  return Math.round(finalScore * 10) / 10;
}

// 生成单条帖子
function generatePost(id, subreddit) {
  const config = SUBREDDIT_CONFIG[subreddit];
  const conflictType = config.conflictTypes[randomInt(0, config.conflictTypes.length - 1)];

  const score = randomLongTail(config.scoreRange[0], config.scoreRange[1]);
  const numComments = randomLongTail(config.commentRange[0], config.commentRange[1]);
  const upvoteRatio = parseFloat((Math.random() * (config.upvoteRatioRange[1] - config.upvoteRatioRange[0]) + config.upvoteRatioRange[0]).toFixed(2));

  const content = generateSelftext(subreddit, conflictType);
  const dramaticScore = calculateDramaticScore({score, num_comments: numComments, upvote_ratio: upvoteRatio}, conflictType, subreddit);

  return {
    id: `t3_${Buffer.from(String(id)).toString('base64').substring(0, 6).toLowerCase()}`,
    subreddit: subreddit,
    title: generateTitle(subreddit),
    author: `user_${randomInt(1000, 99999)}`,
    selftext: content.text,
    score: score,
    upvotes: Math.floor(score * upvoteRatio),
    num_comments: numComments,
    upvote_ratio: upvoteRatio,
    created_utc: randomDate(),
    permalink: `/r/${subreddit}/comments/${Buffer.from(String(id)).toString('base64').substring(0, 6).toLowerCase()}/${subreddit.toLowerCase()}_post/`,
    drama_elements: content.drama_elements,
    dramatic_score: dramaticScore,
    dramatic_level: dramaticScore >= 7.0 ? 'high' : dramaticScore >= 5.0 ? 'medium' : 'low',
    is_drama_candidate: dramaticScore >= 6.0
  };
}

// 主生成函数
function generateDataset() {
  const totalPosts = 280;
  const posts = [];
  let id = 1000;

  for (const [subreddit, config] of Object.entries(SUBREDDIT_CONFIG)) {
    const count = Math.floor(totalPosts * config.weight);
    for (let i = 0; i < count; i++) {
      posts.push(generatePost(id++, subreddit));
    }
  }

  // 打乱顺序
  for (let i = posts.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [posts[i], posts[j]] = [posts[j], posts[i]];
  }

  return posts;
}

// 生成并保存
const dataset = generateDataset();
const outputPath = path.join(__dirname, 'synthetic_reddit_data.json');
fs.writeFileSync(outputPath, JSON.stringify(dataset, null, 2));

console.log(`Generated ${dataset.length} synthetic Reddit posts`);
console.log(`Saved to: ${outputPath}`);

// 统计信息
const stats = {
  total: dataset.length,
  bySubreddit: {},
  byConflictType: {},
  dramaticLevel: { high: 0, medium: 0, low: 0 },
  dramaCandidates: 0,
  avgScore: 0,
  avgComments: 0,
  avgUpvoteRatio: 0
};

dataset.forEach(post => {
  stats.bySubreddit[post.subreddit] = (stats.bySubreddit[post.subreddit] || 0) + 1;
  stats.byConflictType[post.drama_elements.conflict_type] = (stats.byConflictType[post.drama_elements.conflict_type] || 0) + 1;
  stats.dramaticLevel[post.dramatic_level]++;
  if (post.is_drama_candidate) stats.dramaCandidates++;
  stats.avgScore += post.score;
  stats.avgComments += post.num_comments;
  stats.avgUpvoteRatio += post.upvote_ratio;
});

stats.avgScore = Math.round(stats.avgScore / dataset.length);
stats.avgComments = Math.round(stats.avgComments / dataset.length);
stats.avgUpvoteRatio = (stats.avgUpvoteRatio / dataset.length).toFixed(2);

console.log('\n=== Statistics ===');
console.log(JSON.stringify(stats, null, 2));