#!/usr/bin/env node /** * 洪城到家 · VOC 洞察报告 v4 生成器 (优化版) * 基于 v3 模板,集成真实 XHS + Douyin + Drama 数据 * * 核心理念(报告方面.md): * 1. 抄作业 - 学习头部品牌打法 * 2. 短剧破圈 - 借助短剧内容营销 * 3. VOC新机会 - 从真实用户声音发现 * * 数据来源: * - XHS: docs/洪城到家/raw/xhs/*.json (6个品牌) * - Douyin: docs/洪城到家/raw/douyin/*.json (23个关键词) * - Drama: docs/洪城到家/raw/douyin/drama/*.json (4个短剧关键词) * * 用法: * node scripts/tools/hongcheng-render-v4.js */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..', '..'); const XHS_DIR = path.join(ROOT, 'docs', '洪城到家', 'raw', 'xhs'); const DOUYIN_DIR = path.join(ROOT, 'docs', '洪城到家', 'raw', 'douyin'); const DRAMA_DIR = path.join(DOUYIN_DIR, 'drama'); const OUT_FILE = path.join(ROOT, 'reports', 'hongcheng-voc-insight-report-v4.html'); function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function fmt(n) { if (n == null) return '0'; if (n >= 10000) return (n/10000).toFixed(1).replace(/\.0$/,'')+'w'; if (n >= 1000) return (n/1000).toFixed(1).replace(/\.0$/,'')+'k'; return String(n); } function truncate(s, n=160) { s = String(s ?? ''); return s.length > n ? s.slice(0,n-1)+'…' : s; } const HYPOTHESES = { H1: { title: '医院地推', desc: '直接触达即将生产的精准用户', color: '#F5A623' }, H2: { title: '价格透明', desc: '用户对价格不透明是核心痛点', color: '#00DC82' }, H3: { title: '短剧营销', desc: '借南昌万亿短剧政策红利', color: '#8B5CF6' }, H4: { title: '专业度信任', desc: '用户判断月嫂专业度的核心信号', color: '#FF4D8D' }, H5: { title: '社区店威胁', desc: '社区月嫂门店主要威胁是"价格低"', color: '#3B82F6' }, H6: { title: '老带新', desc: '老客户转介绍是最高效获客方式', color: '#EF4444' }, H7: { title: '搜索主力', desc: '小红书是搜索承接主力平台', color: '#F97316' }, H8: { title: '服务保障', desc: '"不满意能换"能显著降低决策门槛', color: '#6B6B6B' }, }; // ============ 数据加载 ============ function loadXHSData() { if (!fs.existsSync(XHS_DIR)) return { notes: [], comments: [], byBrand: {}, stats: { notes: 0, comments: 0, likes: 0 } }; const files = fs.readdirSync(XHS_DIR).filter(f => f.endsWith('.json')); const all = { notes: [], comments: [], byBrand: {}, stats: { notes: 0, comments: 0, likes: 0 } }; for (const file of files) { const brand = file.replace('.json', '').replace('月嫂', ''); const raw = JSON.parse(fs.readFileSync(path.join(XHS_DIR, file), 'utf8')); all.byBrand[brand] = raw; for (const note of (raw.top_notes || [])) { note.brand = brand; note.platform = 'xhs'; all.notes.push(note); all.stats.notes++; all.stats.likes += note.liked_count || 0; for (const c of (note.comments || [])) { c.note_id = note.note_id; c.brand = brand; c.platform = 'xhs-comment'; all.comments.push(c); all.stats.comments++; } } } return all; } function loadDouyinData() { if (!fs.existsSync(DOUYIN_DIR)) return { videos: [], byKeyword: {}, stats: { videos: 0, likes: 0, comments: 0 } }; const files = fs.readdirSync(DOUYIN_DIR).filter(f => f.endsWith('.json') && f !== 'audit.log'); const all = { videos: [], byKeyword: {}, stats: { videos: 0, likes: 0, comments: 0 } }; for (const file of files) { const keyword = file.replace('.json', ''); const raw = JSON.parse(fs.readFileSync(path.join(DOUYIN_DIR, file), 'utf8')); all.byKeyword[keyword] = raw; for (const v of (raw.top_videos || [])) { v.keyword = keyword; v.platform = 'douyin'; all.videos.push(v); all.stats.videos++; all.stats.likes += v.statistics?.digg_count || 0; all.stats.comments += v.statistics?.comment_count || 0; } } return all; } function loadDramaData() { if (!fs.existsSync(DRAMA_DIR)) return { dramas: [], stats: { videos: 0, likes: 0, topLikes: 0, comments: 0 } }; const files = fs.readdirSync(DRAMA_DIR).filter(f => f.endsWith('.json')); const all = { dramas: [], stats: { videos: 0, likes: 0, topLikes: 0, comments: 0 } }; for (const file of files) { const raw = JSON.parse(fs.readFileSync(path.join(DRAMA_DIR, file), 'utf8')); for (const v of (raw.top_videos || [])) { v.dramaKeyword = file.replace('.json', ''); v.platform = 'drama'; all.dramas.push(v); all.stats.videos++; const likes = v.statistics?.digg_count || 0; const comments = v.statistics?.comment_count || 0; all.stats.likes += likes; all.stats.comments += comments; if (likes > all.stats.topLikes) all.stats.topLikes = likes; } } return all; } // ============ 数据分析 ============ function analyzeXHS(xhs) { const { notes, comments, byBrand, stats } = xhs; const sentiment = { positive: 0, negative: 0, neutral: 0, conflicted: 0 }; const hypothesisCoverage = { H1: 0, H2: 0, H3: 0, H4: 0, H5: 0, H6: 0, H7: 0, H8: 0 }; for (const n of notes) { sentiment[n.sentiment || 'neutral']++; if (n.hypotheses && Array.isArray(n.hypotheses)) { for (const h of n.hypotheses) if (hypothesisCoverage[h]) hypothesisCoverage[h]++; } } for (const c of comments) { sentiment[c.sentiment || 'neutral']++; if (c.hypotheses && Array.isArray(c.hypotheses)) { for (const h of c.hypotheses) if (hypothesisCoverage[h]) hypothesisCoverage[h]++; } } const tagStats = {}; for (const n of notes) for (const t of (n.inferred_tags || [])) tagStats[t] = (tagStats[t] || 0) + 1; const topTags = Object.entries(tagStats).sort((a, b) => b[1] - a[1]).slice(0, 15); const brandStats = {}; for (const [brand, data] of Object.entries(byBrand)) { brandStats[brand] = { notes: data.notes_processed || 0, comments: data.total_comments || 0, likes: (data.top_notes || []).reduce((s, n) => s + (n.liked_count || 0), 0) }; } const topNotes = [...notes].sort((a, b) => (b.liked_count || 0) - (a.liked_count || 0)).slice(0, 6); const topComments = [...comments].sort((a, b) => (b.comment_likes || 0) - (a.comment_likes || 0)).slice(0, 4); return { stats, sentiment, hypothesisCoverage, topTags, topNotes, topComments, brandStats, totalNotes: notes.length, totalComments: comments.length }; } function analyzeDouyin(dy) { const { videos, byKeyword, stats } = dy; const kwStats = {}; for (const [kw, data] of Object.entries(byKeyword)) { kwStats[kw] = { videos: data.total_videos_found || 0, likes: (data.top_videos || []).reduce((s, v) => s + (v.statistics?.digg_count || 0), 0), topLikes: (data.top_videos || [])[0]?.statistics?.digg_count || 0 }; } const topVideos = [...videos].sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 6); return { stats, kwStats, topVideos, totalVideos: videos.length }; } function analyzeDrama(drama) { const { dramas, stats } = drama; const topVideos = [...dramas].sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 6); const genreStats = {}; for (const d of dramas) { const kw = d.dramaKeyword || '其他'; genreStats[kw] = (genreStats[kw] || 0) + 1; } return { stats, topVideos, totalDramas: dramas.length, genreStats }; } // ============ 组件 ============ function vocCard(item, type = 'note') { const senColors = { positive: '#00DC82', negative: '#EF4444', neutral: '#6B6B6B', conflicted: '#F97316' }; const platformColors = { 'xhs': '#FF2442', 'xhs-note': '#FF2442', 'xhs-comment': '#FF6B6B', 'drama': '#8B5CF6', 'douyin': '#1A1A1A' }; const platformIcons = { 'xhs': '红', 'xhs-note': '红', 'xhs-comment': '评', 'drama': '剧', 'douyin': '抖' }; let content, likes, author, sentiment, tags, ip; if (type === 'xhs-note' || type === 'xhs') { content = item.content || item.title || ''; likes = item.liked_count || 0; author = item.author || item.user_nickname || '匿名'; sentiment = item.sentiment || 'neutral'; tags = item.inferred_tags || []; ip = item.ip_location || ''; } else if (type === 'xhs-comment') { content = item.content || ''; likes = item.comment_likes || 0; author = item.user_nickname || '匿名'; sentiment = item.sentiment || 'neutral'; tags = item.tags || []; ip = item.ip_location || ''; } else if (type === 'drama') { content = item.desc || ''; likes = item.statistics?.digg_count || 0; author = item.author?.nickname || '匿名'; sentiment = 'neutral'; tags = []; ip = ''; } else { content = item.desc || item.content || ''; likes = item.statistics?.digg_count || 0; author = item.author?.nickname || '匿名'; sentiment = item.sentiment || 'neutral'; tags = []; ip = ''; } const pColor = platformColors[type] || '#1A1A1A'; const pIcon = platformIcons[type] || '抖'; return `
${pIcon} ${esc(author)} ${ip ? `· ${esc(ip)}` : ''}
${type !== 'drama' && type !== 'douyin' && sentiment !== 'neutral' ? `● ${sentiment}` : ''} ♥${fmt(likes)}
"${esc(truncate(content, 150))}"
${tags.length ? `
${tags.slice(0,3).map(t=>`${esc(t)}`).join('')}
` : ''}
`; } function barChart(rows, accent) { const max = Math.max(...rows.map(r=>r.value||0),1); const barH=36,gap=14,padL=160,padR=100,padV=16,W=900; const totalH = rows.length*(barH+gap)+padV*2; return `
${rows.map((r,i)=>{const y=padV+i*(barH+gap);const w=Math.max(((r.value||0)/max)*(W-padL-padR),6);return `${esc(r.label)}${esc(r.valueLabel||fmt(r.value||0))}`;}).join('')}
`; } function hypothesisBar(hypothesisCoverage, totalNotes) { return Object.entries(HYPOTHESES).map(([h, info]) => { const count = hypothesisCoverage[h] || 0; const pct = totalNotes ? Math.round((count / totalNotes) * 100) : 0; return `
${h} ${info.title} ${count}条 · ${pct}%
`; }).join(''); } // ============ 主报告生成 ============ function generateReport(data) { const { xhsAna, dyAna, dramaAna } = data; const { stats: xhsStats, sentiment: xhsSent, hypothesisCoverage, topTags, topNotes, brandStats, totalNotes, totalComments } = xhsAna; const { stats: dyStats, kwStats, topVideos: dyTop, totalVideos: totalDyVideos } = dyAna; const { stats: dramaStats, topVideos: dramaTop, totalDramas, genreStats } = dramaAna; const totalVOC = totalNotes + totalComments + totalDyVideos + totalDramas; const totalLikes = xhsStats.likes + dyStats.likes + dramaStats.likes; const competitors = [ {name:'天鹅到家',revenue:'28-32亿',strength:'全国第一/平台模式/AI驱动',借鉴:'标准化体系+多渠道组合',color:'#3B82F6'}, {name:'好孕妈妈',revenue:'18-22亿',strength:'医疗背景/医院合作强/CRM',借鉴:'医院深度合作是最高效场景',color:'#00DC82'}, {name:'多喜娃',revenue:'3-4.5亿',strength:'深圳第二/自营+医院地推',借鉴:'自营模式+医院地推三步走',color:'#F97316'}, {name:'妈咪无忧',revenue:'7-9亿',strength:'上海第一/小红书KOS矩阵',借鉴:'账号矩阵打法',color:'#FF4D8D'}, ]; const geoData = [ {label:'广东',value:451},{label:'江苏',value:394},{label:'山东',value:273}, {label:'浙江',value:245},{label:'湖南',value:232},{label:'广西',value:209}, ]; const dyKeywordRows = Object.entries(kwStats) .sort((a, b) => b[1].topLikes - a[1].topLikes) .slice(0, 8) .map(([kw, s]) => ({ label: kw, value: s.topLikes, color: '#F97316' })); const sentimentBars = Object.entries(xhsSent).map(([s, cnt]) => { const colors = {positive:'#00DC82',negative:'#EF4444',neutral:'#6B6B6B',conflicted:'#F97316'}; const labels = {positive:'正向',negative:'负向',neutral:'中立',conflicted:'矛盾'}; const pct = totalNotes ? Math.round((cnt / totalNotes) * 100) : 0; return `
${labels[s]}
${pct}%
`; }).join(''); const tagCloud = topTags.slice(0, 12).map(([tag, cnt]) => ` ${esc(tag)} ${cnt} ` ).join(''); const xhsBrandsSorted = Object.entries(brandStats).sort((a, b) => b[1].likes - a[1].likes); const brandRows = xhsBrandsSorted.map(([brand, s]) => `${esc(brand)} ${s.notes} ${s.comments} ♥ ${fmt(s.likes)}` ).join(''); const positives = topNotes.filter(n => n.sentiment === 'positive').slice(0, 4); const negatives = topNotes.filter(n => n.sentiment === 'negative').slice(0, 4); const neutrals = topNotes.filter(n => n.sentiment === 'neutral').slice(0, 4); const dramaGenreRows = Object.entries(genreStats).map(([genre, cnt]) => ` ${cnt} ${esc(genre.replace('短剧',''))} ` ).join(''); return ` 洪城到家 · VOC 深度洞察报告 v4
VOC 深度洞察报告 v4

洪城到家

基于小红书 + 抖音真实数据
三大战略:抄作业 · 短剧破圈 · VOC新机会

小红书 ${totalNotes}笔记 ${totalComments} 条评论 ${totalDyVideos} 抖音视频 ${totalDramas} 短剧
📊 ${fmt(totalVOC)} 总VOC ♥ ${fmt(totalLikes)} 总互动 📅 ${new Date().toLocaleDateString('zh-CN')}
CHAPTER 01

执行摘要

核心问题与机会分析

VOC总量
${fmt(totalVOC)}
条真实数据
小红书笔记
${totalNotes}
抖音视频
${totalDyVideos}
总互动
♥${fmt(totalLikes)}
点赞
三大战略方向
1️⃣
抄作业

学妈咪无忧小红书矩阵 + 学多喜娃医院地推 + 学好孕妈妈朋友圈投放 + 学天鹅到家标准化

2️⃣
短剧破圈

借南昌万亿短剧市场政策东风,低成本实现品牌曝光和流量导流

3️⃣
VOC驱动

围绕专业度、价格透明度、服务保障三大痛点构建差异化价值

CHAPTER 02

竞品获客策略

头部品牌VOC拆解与可借鉴点

${competitors.map(c=>`
${c.name}
${c.revenue}/年

${c.strength}

${c.借鉴}
`).join('')}
品牌声量排行(小红书)
${brandRows}
品牌笔记评论点赞
${dyKeywordRows.length > 0 ? `
抖音关键词热度(TOP点赞)
${barChart(dyKeywordRows, '#F97316')}
` : ''}
CHAPTER 03

用户痛点VOC

选择月嫂的三大顾虑 + H假设验证

💰
价格透明

"月嫂多少钱一个月"

🏆
专业度信任

"怎么知道月嫂靠不靠谱"

🛡️
服务保障

"不满意能换吗"

热门标签分布
${tagCloud}
情感分布
${sentimentBars}
H假设覆盖分析
${Object.entries(HYPOTHESES).slice(0,4).map(([h, info]) => { const count = hypothesisCoverage[h] || 0; const pct = totalNotes ? Math.round((count / totalNotes) * 100) : 0; return `
${h} ${info.title} ${count}条
`; }).join('')}
▲ 正面评价样本
${positives.map(v=>vocCard(v,'xhs-note')).join('')}
▼ 负面评价样本(需关注)
${negatives.length ? negatives.map(v=>vocCard(v,'xhs-note')).join('') : '

暂无负面VOC

'}
CHAPTER 04

短剧营销 × 破圈策略

借南昌万亿短剧市场政策红利

采集短剧
${totalDramas}
总点赞
♥${fmt(dramaStats.likes)}
互动
最高点赞
♥${fmt(dramaStats.topLikes)}
单条
抖音视频
${totalDyVideos}
⚠️
数据说明(破除幻觉)

• 短剧VOC ≠ 短剧ROI:${totalDramas}条短剧数据反映用户对内容的反应,不等于广告转化效果

• 高点赞短剧(♥${fmt(dramaStats.topLikes)})验证了"月嫂+婆媳"题材的流量基础

• 建议:以小规模测试验证ROI后再规模化投放

高赞短剧精选
${dramaTop.slice(0,4).map(v=>vocCard(v,'drama')).join('')}
题材选择

婆媳关系 + 月嫂专业 + 产后情绪,是爆款公式

植入方式

品牌名软植入,不硬广,自然露出

分发策略

抖音为主,小红书为辅,评论区互动引导

CHAPTER 05

用户地理分布

小红书评论IP定位

${barChart(geoData.map(g=>({label:g.label,value:g.value,color:'#00DC82'})),'#00DC82')}
关键发现
● 广东 最大评论来源省
● 江苏 江浙沪经济发达区
● 山东 北方第一人口大省
⚠ 江西本省评论较少,需加强本地推广
CHAPTER 06

增长机会 × 新渠道

基于VOC发现的三大突破口

🏥
医院地推

直接触达即将生产的精准用户,转化率远高于泛流量

50-100元
客资成本
极高
转化率
H1 医院地推
🎬
短剧营销

借南昌万亿短剧政策红利,低成本品牌曝光

3-5万
冠名成本
曝光性价比
H3 短剧营销
📱
多渠道组合

抖音+小红书+私域,老带新提升LTV

500元
客资ROI
3-5个
渠道组合
H7 搜索主力
CHAPTER 07

4P 配称蓝图

落地执行计划

1 阶段一(1-3月)
• 抖音/小红书账号矩阵搭建
• 与南昌3-5家核心医院产科建立合作
• 建立星级月嫂体系
2 阶段二(3-6月)
• 短视频日常运营(3-5条/周)
• 首部合作短剧(3-5万)
• 微信私域搭建+老带新
• 小红书投流测试
3 阶段三(6-12月)
• 多渠道付费投放规模化
• 短剧系列化(3-5部)
• 覆盖南昌80%+核心医院
• 品牌定制短剧(20-50万)
CHAPTER 08

VOC 高赞精选

真实用户声音

▲ 正面评价(高赞)
${positives.map(v=>vocCard(v,'xhs-note')).join('')}
● 中性评价(高赞)
${neutrals.map(v=>vocCard(v,'xhs-note')).join('')}
${dyTop && dyTop.length > 0 ? `
🎬 抖音高赞视频精选
${dyTop.slice(0,4).map(v=>vocCard(v,'douyin')).join('')}
` : ''}
`; } function main() { console.log('📊 加载数据...'); const xhs = loadXHSData(); const dy = loadDouyinData(); const drama = loadDramaData(); console.log(` XHS: ${xhs.stats.notes}笔记/${xhs.stats.comments}评论`); console.log(` 抖音: ${dy.stats.videos}视频`); console.log(` 短剧: ${drama.stats.videos}视频(最高♥${fmt(drama.stats.topLikes)})`); console.log('🔧 分析数据...'); const xhsAna = analyzeXHS(xhs); const dyAna = analyzeDouyin(dy); const dramaAna = analyzeDrama(drama); console.log('📝 生成HTML报告...'); const html = generateReport({ xhsAna, dyAna, dramaAna }); fs.writeFileSync(OUT_FILE, html, 'utf8'); console.log(`✅ 报告已生成: ${OUT_FILE}`); } if (require.main === module) { try { main(); } catch (e) { console.error('fatal:', e); process.exit(1); } } module.exports = { loadXHSData, loadDouyinData, loadDramaData, analyzeXHS, analyzeDouyin, analyzeDrama };