dump-voc-summary.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env node
  2. /**
  3. * 从 3 份 VOC JSON 中提炼用于"复合体方向"分析的关键数据
  4. * - 用户真实原声(Top 评论)
  5. * - VOC 主题聚类(痛点/亮点/场景)
  6. * - Amazon 对标(品牌分布 / 价位 / 详细产品成分卖点)
  7. * - TikTok 关键词 + 作者画像
  8. * - 市场情报:TT Ads 关键词洞察 + Douyin 热榜
  9. *
  10. * 输出到 data/voc-summary-for-product-dev.txt,供后续分析
  11. */
  12. const fs = require('fs');
  13. const path = require('path');
  14. const { analyze } = require('./voc-analyze.js');
  15. const PRODUCTS = [
  16. { key: 'liver', label: '江中肝纯片(OTC 保肝片剂)', file: 'jiangzhong-liver-voc.json' },
  17. { key: 'probiotic', label: '江中儿童乳酸菌片', file: 'jiangzhong-probiotic-voc.json' },
  18. { key: 'monkey', label: '江中猴姑饼干', file: 'jiangzhong-monkey-voc.json' },
  19. ];
  20. function formatList(arr, max = 10) {
  21. if (!arr || !arr.length) return '(无)';
  22. return arr.slice(0, max).map((x, i) => ` ${String(i + 1).padStart(2, '0')}. ${x}`).join('\n');
  23. }
  24. function out(s) { buffer.push(s); }
  25. const buffer = [];
  26. for (const p of PRODUCTS) {
  27. const fp = path.join('data', p.file);
  28. if (!fs.existsSync(fp)) {
  29. out(`\n\n========== ${p.label} ========== [JSON 不存在]`);
  30. continue;
  31. }
  32. const voc = JSON.parse(fs.readFileSync(fp, 'utf-8'));
  33. const A = analyze(voc);
  34. out(`\n\n${'='.repeat(80)}`);
  35. out(`【${p.label}】 · ${p.file}`);
  36. out('='.repeat(80));
  37. // === 基础规模 ===
  38. out('\n● 规模:');
  39. out(` - ${A.metrics.notes} 篇小红书笔记 · ${A.metrics.sampleComments} 条真实评论 · ${A.kols.length} 位作者画像`);
  40. out(` - 点赞 ${A.metrics.totalLikes} · 评论 ${A.metrics.totalComments} · 收藏 ${A.metrics.totalCollected}`);
  41. // === 核心洞察 ===
  42. if (A.coreInsights && A.coreInsights.length) {
  43. out('\n● 三条核心洞察:');
  44. A.coreInsights.forEach((ins, i) => {
  45. out(` ${i + 1}. ${ins.title || ins.headline || ins}`);
  46. if (ins.body || ins.desc) out(` ${(ins.body || ins.desc).slice(0, 200)}`);
  47. });
  48. }
  49. // === 用户真实原声(高赞 top 15)===
  50. if (A.voiceWall && A.voiceWall.length) {
  51. out(`\n● 用户真实原声 (Top ${Math.min(A.voiceWall.length, 12)}):`);
  52. A.voiceWall.slice(0, 12).forEach((c, i) => {
  53. const content = (c.content || c.text || '').replace(/\s+/g, ' ').slice(0, 120);
  54. out(` ${String(i + 1).padStart(2, '0')}. [${c.likes || 0}赞] ${content}`);
  55. });
  56. }
  57. // === VOC 主题聚类(痛点/亮点/场景)===
  58. if (A.themes) {
  59. const renderTheme = (key, title) => {
  60. const arr = A.themes[key];
  61. if (!arr || !arr.length) return;
  62. out(`\n● ${title}:`);
  63. arr.slice(0, 10).forEach((t, i) => {
  64. const label = t.label || t.name || t.theme || '';
  65. const freq = t.freq || t.count || t.frequency || 0;
  66. const sample = (t.samples || t.examples || [])[0] || '';
  67. out(` ${i + 1}. [${freq}次] ${label}${sample ? ' · 代表原声: ' + String(sample).slice(0, 100) : ''}`);
  68. });
  69. };
  70. renderTheme('painPoints', 'VOC 痛点簇');
  71. renderTheme('brightSpots', 'VOC 亮点簇');
  72. renderTheme('scenes', 'VOC 场景簇');
  73. }
  74. // === Amazon 对标 ===
  75. if (A.amazon && !A.amazon.empty) {
  76. out('\n● Amazon 对标:');
  77. out(` - ${A.amazon.keywords?.length || 0} 组英文关键词 × ${A.amazon.total || 0} 款产品`);
  78. if (A.amazon.topBrands) {
  79. out(` - Top 6 品牌:`);
  80. (A.amazon.topBrands || []).slice(0, 6).forEach((b, i) => {
  81. out(` ${i + 1}. ${b.brand || b[0]} · 销量 ${b.sales || b[1] || 0} · 份额 ${b.share ? b.share.toFixed(1) + '%' : '-'}`);
  82. });
  83. }
  84. if (A.amazon.priceBuckets) {
  85. out(` - 价位分布:`);
  86. Object.entries(A.amazon.priceBuckets).forEach(([k, v]) => out(` ${k}: ${v} 款`));
  87. }
  88. // Top 6 产品的详细成分/卖点
  89. if (A.amazon.detailProducts) {
  90. out(` - 深度详情 Top 6:`);
  91. A.amazon.detailProducts.slice(0, 6).forEach((prod, i) => {
  92. const title = (prod.title || '').slice(0, 80);
  93. const brand = prod.brand || '-';
  94. const price = prod.price || '-';
  95. const rating = prod.rating || '-';
  96. const reviews = prod.reviewCount || prod.ratingsCount || 0;
  97. out(` ${i + 1}. [${brand}] ${title}`);
  98. out(` $${price} · ★${rating} · ${reviews} 评 · BSR ${prod.bsrRank || '-'}`);
  99. });
  100. }
  101. // 品牌机会评估 dossier
  102. if (A.amazon.dossiers) {
  103. out(` - 竞品档案 (dossiers):`);
  104. A.amazon.dossiers.slice(0, 4).forEach((d, i) => {
  105. out(` ${i + 1}. ${d.brand} (${d.positioning}) · 份额 ${d.share?.toFixed(1)}% · 均价 $${d.avgPrice?.toFixed(1)}`);
  106. if (d.strengths?.length) out(` 优势: ${d.strengths.slice(0, 3).map(s => s.label).join(' / ')}`);
  107. if (d.gaps?.length) out(` 缝隙: ${d.gaps.slice(0, 3).map(g => g.label).join(' / ')}`);
  108. });
  109. }
  110. // 机会评估
  111. if (A.amazon.opportunityAssessment) {
  112. const op = A.amazon.opportunityAssessment;
  113. out(` - 品类机会评估: ${op.grade} (${op.totalScore}/${op.maxScore})`);
  114. if (op.dimensions) {
  115. op.dimensions.slice(0, 6).forEach((d) => out(` · ${d.name}: ${d.level} · ${(d.note || '').slice(0, 80)}`));
  116. }
  117. }
  118. }
  119. // === TikTok ===
  120. if (A.tiktok && !A.tiktok.empty) {
  121. out('\n● TikTok 海外内容端:');
  122. out(` - 关键词: ${(A.tiktok.keywords || []).join(' / ')}`);
  123. out(` - ${A.tiktok.totalVideos || 0} 视频 · ${A.tiktok.totalAuthors || 0} 作者 · ${A.tiktok.totalComments || 0} 评论`);
  124. if (A.tiktok.topHashtags) {
  125. out(` - 高频 hashtag: ${(A.tiktok.topHashtags || []).slice(0, 10).map(h => '#' + (h.tag || h[0])).join(' ')}`);
  126. }
  127. if (A.tiktok.topAuthors) {
  128. out(` - Top 作者:`);
  129. (A.tiktok.topAuthors || []).slice(0, 5).forEach((a, i) => {
  130. out(` ${i + 1}. ${a.nickname || a.unique_id || '-'} · ${a.follower_count || 0} 粉 · ${a.video_count || 0} 视频`);
  131. });
  132. }
  133. }
  134. // === Market Intel ===
  135. if (A.marketIntel && !A.marketIntel.empty) {
  136. const mi = A.marketIntel;
  137. out('\n● 市场情报 (TT Ads + Douyin 基线):');
  138. if (mi.ttAds?.hasData) {
  139. out(` - TT Ads 关键词洞察:`);
  140. (mi.ttAds.keywordInsights || []).slice(0, 5).forEach((ki, i) => {
  141. out(` ${i + 1}. "${ki.keyword}"`);
  142. out(` · 曝光 ${ki.impression || 0} · CTR ${ki.ctr ? (ki.ctr * 100).toFixed(2) + '%' : '-'} · CVR ${ki.cvr ? (ki.cvr * 100).toFixed(2) + '%' : '-'}`);
  143. out(` · posts ${ki.postCount || 0} · 变化 ${ki.postChange ? (ki.postChange > 0 ? '+' : '') + (ki.postChange * 100).toFixed(1) + '%' : '-'}`);
  144. out(` · 平均 like ${ki.avgLike || 0} · comment ${ki.avgComment || 0} · share ${ki.avgShare || 0}`);
  145. });
  146. if (mi.ttAds.relatedKeywords) {
  147. out(` - TT Ads 相关关键词网络 (Top 20):`);
  148. const rel = mi.ttAds.relatedKeywords.slice(0, 20).map(r => r.keyword || r[0]);
  149. out(` ${rel.join(', ')}`);
  150. }
  151. }
  152. if (mi.douyin?.hotWords?.length) {
  153. out(` - Douyin 抖音热词 Top 6: ${mi.douyin.hotWords.slice(0, 6).map(w => w.word || w[0]).join(' / ')}`);
  154. }
  155. if (mi.douyin?.hotChallenges?.length) {
  156. out(` - Douyin 热挑战 Top 5: ${mi.douyin.hotChallenges.slice(0, 5).map(c => c.title || c[0]).join(' / ')}`);
  157. }
  158. if (mi.douyin?.categoryFilterHit !== undefined) {
  159. out(` - Douyin 品类过滤命中: ${mi.douyin.categoryFilterHit} 条 (${mi.douyin.categoryFilterHit === 0 ? '品牌蓝海' : '有内容沉淀'})`);
  160. }
  161. }
  162. }
  163. const outPath = 'data/voc-summary-for-product-dev.txt';
  164. fs.writeFileSync(outPath, buffer.join('\n'), 'utf-8');
  165. console.log(`\n✅ 摘要写入: ${outPath} (${(fs.statSync(outPath).size / 1024).toFixed(1)}KB)`);